2
0
Fork 0

Change entry point group naming for subcommands

and use fallback to find subcommands registered via legacy naming
This commit is contained in:
Lance Edgar 2023-11-21 14:08:26 -06:00
parent d8252f029d
commit ea9a9ade57
3 changed files with 57 additions and 32 deletions

View file

@ -50,5 +50,5 @@ tests = pytest-cov; tox
console_scripts =
wutta = wuttjamaican.commands.base:main
wutta.commands =
wutta.subcommands =
setup = wuttjamaican.commands.setup:Setup

View file

@ -27,6 +27,7 @@ WuttJamaican - command framework
import argparse
import logging
import sys
import warnings
from wuttjamaican import __version__
from wuttjamaican.util import load_entry_points
@ -75,7 +76,7 @@ class Command:
top-level command, and create subcommands as needed.
To do that, first create your Command class, and a ``main()``
entry point (e.g. in ``poser/commands.py``)::
entry point function for it (e.g. in ``poser/commands.py``)::
import sys
from wuttjamaican.commands import Command
@ -85,18 +86,25 @@ class Command:
description = 'my custom top-level command'
version = '0.1'
def main(*args):
def poser_main(*args):
args = list(args) or sys.argv[1:]
cmd = Poser()
cmd.run(*args)
Then register the ``main()`` entry point (in your ``setup.cfg``):
Then register the ``main()`` entry point(s) in your ``setup.cfg``.
The command name should be "one word" (no spaces) but may include
hyphens or underscore etc.
You can register more than one top-level command if needed; these
could refer to the same ``main()`` function (in which case they
are really aliases) or can use different functions:
.. code-block:: ini
[options.entry_points]
console_scripts =
poser = poser.commands:main
poser = poser.commands:poser_main
wutta-poser = poser.commands:wutta_poser_main
Next time your (``poser``) package is installed, the command will be
available, so you can e.g.:
@ -105,12 +113,10 @@ class Command:
cd /path/to/venv
bin/poser --help
bin/wutta-poser --help
And see :class:`Subcommand` for info about adding those.
Note that you can add as many top-level commands as needed. Most
apps only need one of course, but there is no actual limit.
.. attribute:: name
Name of the primary command, e.g. ``wutta``
@ -154,7 +160,19 @@ class Command:
self.name = name or self.name
self.stdout = stdout or sys.stdout
self.stderr = stderr or sys.stderr
self.subcommands = subcommands or load_entry_points(f'{self.name}.commands')
# nb. default entry point is like 'wutta-poser.subcommands'
self.subcommands = subcommands or load_entry_points(f'{self.name}.subcommands')
if not self.subcommands:
# nb. legacy entry point is like 'wutta_poser.commands'
safe_name = self.name.replace('-', '_')
self.subcommands = load_entry_points(f'{safe_name}.commands')
if self.subcommands:
msg = (f"entry point group '{safe_name}.commands' uses deprecated name; "
f"please define '{self.name}.subcommands' instead")
warnings.warn(msg, DeprecationWarning, stacklevel=2)
log.warning(msg)
def __str__(self):
return self.name
@ -316,12 +334,17 @@ class Subcommand:
You may notice there is nothing in that subcommand definition
which ties it to the ``poser`` top-level command. That is done by
way of another entry point in your ``setup.cfg`` file:
way of another entry point in your ``setup.cfg`` file.
As with top-level commands, you can "alias" the same subcommand so
it appears under multiple top-level commands:
.. code-block:: ini
[options.entry_points]
poser.commands =
poser.subcommands =
hello = poser.commands:Hello
wutta-poser.subcommands =
hello = poser.commands:Hello
Next time your (``poser``) package is installed, the subcommand
@ -331,27 +354,7 @@ class Subcommand:
cd /path/to/venv
bin/poser hello --help
Since the connection between command and subcommand is only "real"
if there is an entry point, this means a) you can add the
subcommand under *any* top-level command, but also b) you could
technically add it to multiple top-level commands. So for
instance in addition to the above entry point you might also do:
.. code-block:: ini
[options.entry_points]
wutta.commands =
hello = poser.commands:Hello
After re-installing your package then these commands would do the
same thing:
.. code-block:: sh
cd /path/to/venv
bin/poser hello
bin/wutta hello
bin/wutta-poser hello --help
"""
name = 'UNDEFINED'
description = "TODO: not defined"

View file

@ -6,6 +6,7 @@ from unittest import TestCase
from unittest.mock import MagicMock, patch
from wuttjamaican.commands import base
from wuttjamaican.commands.setup import Setup
class TestCommand(TestCase):
@ -17,6 +18,27 @@ class TestCommand(TestCase):
self.assertIn('setup', cmd.subcommands)
self.assertEqual(str(cmd), 'wutta')
def test_subcommand_entry_points(self):
with patch('wuttjamaican.commands.base.load_entry_points') as load_entry_points:
# empty entry points
load_entry_points.side_effect = lambda group: {}
cmd = base.Command()
self.assertEqual(cmd.subcommands, {})
# typical entry points
load_entry_points.side_effect = lambda group: {'setup': Setup}
cmd = base.Command()
self.assertEqual(cmd.subcommands, {'setup': Setup})
self.assertEqual(cmd.subcommands['setup'].name, 'setup')
# legacy entry points
# nb. mock returns entry points only when legacy name is used
load_entry_points.side_effect = lambda group: {} if 'subcommands' in group else {'setup': Setup}
cmd = base.Command()
self.assertEqual(cmd.subcommands, {'setup': Setup})
self.assertEqual(cmd.subcommands['setup'].name, 'setup')
def test_sorted_subcommands(self):
cmd = base.Command(subcommands={'foo': 'FooSubcommand',
'bar': 'BarSubcommand'})