From ea9a9ade574eb9e1bcfc3eec4c4572d25f2f0218 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 21 Nov 2023 14:08:26 -0600 Subject: [PATCH] Change entry point group naming for subcommands and use fallback to find subcommands registered via legacy naming --- setup.cfg | 2 +- src/wuttjamaican/commands/base.py | 65 ++++++++++++++++--------------- tests/commands/test_base.py | 22 +++++++++++ 3 files changed, 57 insertions(+), 32 deletions(-) diff --git a/setup.cfg b/setup.cfg index 8bae617..516529b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -50,5 +50,5 @@ tests = pytest-cov; tox console_scripts = wutta = wuttjamaican.commands.base:main -wutta.commands = +wutta.subcommands = setup = wuttjamaican.commands.setup:Setup diff --git a/src/wuttjamaican/commands/base.py b/src/wuttjamaican/commands/base.py index 47ef92d..0fee315 100644 --- a/src/wuttjamaican/commands/base.py +++ b/src/wuttjamaican/commands/base.py @@ -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" diff --git a/tests/commands/test_base.py b/tests/commands/test_base.py index 7673f81..bc40a81 100644 --- a/tests/commands/test_base.py +++ b/tests/commands/test_base.py @@ -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'})