Change entry point group naming for subcommands
and use fallback to find subcommands registered via legacy naming
This commit is contained in:
parent
d8252f029d
commit
ea9a9ade57
|
@ -50,5 +50,5 @@ tests = pytest-cov; tox
|
||||||
console_scripts =
|
console_scripts =
|
||||||
wutta = wuttjamaican.commands.base:main
|
wutta = wuttjamaican.commands.base:main
|
||||||
|
|
||||||
wutta.commands =
|
wutta.subcommands =
|
||||||
setup = wuttjamaican.commands.setup:Setup
|
setup = wuttjamaican.commands.setup:Setup
|
||||||
|
|
|
@ -27,6 +27,7 @@ WuttJamaican - command framework
|
||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
import warnings
|
||||||
|
|
||||||
from wuttjamaican import __version__
|
from wuttjamaican import __version__
|
||||||
from wuttjamaican.util import load_entry_points
|
from wuttjamaican.util import load_entry_points
|
||||||
|
@ -75,7 +76,7 @@ class Command:
|
||||||
top-level command, and create subcommands as needed.
|
top-level command, and create subcommands as needed.
|
||||||
|
|
||||||
To do that, first create your Command class, and a ``main()``
|
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
|
import sys
|
||||||
from wuttjamaican.commands import Command
|
from wuttjamaican.commands import Command
|
||||||
|
@ -85,18 +86,25 @@ class Command:
|
||||||
description = 'my custom top-level command'
|
description = 'my custom top-level command'
|
||||||
version = '0.1'
|
version = '0.1'
|
||||||
|
|
||||||
def main(*args):
|
def poser_main(*args):
|
||||||
args = list(args) or sys.argv[1:]
|
args = list(args) or sys.argv[1:]
|
||||||
cmd = Poser()
|
cmd = Poser()
|
||||||
cmd.run(*args)
|
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
|
.. code-block:: ini
|
||||||
|
|
||||||
[options.entry_points]
|
[options.entry_points]
|
||||||
console_scripts =
|
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
|
Next time your (``poser``) package is installed, the command will be
|
||||||
available, so you can e.g.:
|
available, so you can e.g.:
|
||||||
|
@ -105,12 +113,10 @@ class Command:
|
||||||
|
|
||||||
cd /path/to/venv
|
cd /path/to/venv
|
||||||
bin/poser --help
|
bin/poser --help
|
||||||
|
bin/wutta-poser --help
|
||||||
|
|
||||||
And see :class:`Subcommand` for info about adding those.
|
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
|
.. attribute:: name
|
||||||
|
|
||||||
Name of the primary command, e.g. ``wutta``
|
Name of the primary command, e.g. ``wutta``
|
||||||
|
@ -154,7 +160,19 @@ class Command:
|
||||||
self.name = name or self.name
|
self.name = name or self.name
|
||||||
self.stdout = stdout or sys.stdout
|
self.stdout = stdout or sys.stdout
|
||||||
self.stderr = stderr or sys.stderr
|
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):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
@ -316,12 +334,17 @@ class Subcommand:
|
||||||
|
|
||||||
You may notice there is nothing in that subcommand definition
|
You may notice there is nothing in that subcommand definition
|
||||||
which ties it to the ``poser`` top-level command. That is done by
|
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
|
.. code-block:: ini
|
||||||
|
|
||||||
[options.entry_points]
|
[options.entry_points]
|
||||||
poser.commands =
|
poser.subcommands =
|
||||||
|
hello = poser.commands:Hello
|
||||||
|
wutta-poser.subcommands =
|
||||||
hello = poser.commands:Hello
|
hello = poser.commands:Hello
|
||||||
|
|
||||||
Next time your (``poser``) package is installed, the subcommand
|
Next time your (``poser``) package is installed, the subcommand
|
||||||
|
@ -331,27 +354,7 @@ class Subcommand:
|
||||||
|
|
||||||
cd /path/to/venv
|
cd /path/to/venv
|
||||||
bin/poser hello --help
|
bin/poser hello --help
|
||||||
|
bin/wutta-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
|
|
||||||
"""
|
"""
|
||||||
name = 'UNDEFINED'
|
name = 'UNDEFINED'
|
||||||
description = "TODO: not defined"
|
description = "TODO: not defined"
|
||||||
|
|
|
@ -6,6 +6,7 @@ from unittest import TestCase
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from wuttjamaican.commands import base
|
from wuttjamaican.commands import base
|
||||||
|
from wuttjamaican.commands.setup import Setup
|
||||||
|
|
||||||
|
|
||||||
class TestCommand(TestCase):
|
class TestCommand(TestCase):
|
||||||
|
@ -17,6 +18,27 @@ class TestCommand(TestCase):
|
||||||
self.assertIn('setup', cmd.subcommands)
|
self.assertIn('setup', cmd.subcommands)
|
||||||
self.assertEqual(str(cmd), 'wutta')
|
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):
|
def test_sorted_subcommands(self):
|
||||||
cmd = base.Command(subcommands={'foo': 'FooSubcommand',
|
cmd = base.Command(subcommands={'foo': 'FooSubcommand',
|
||||||
'bar': 'BarSubcommand'})
|
'bar': 'BarSubcommand'})
|
||||||
|
|
Loading…
Reference in a new issue