Move cli framework to wuttjamaican.cmd subpackage
				
					
				
			deprecate `wuttjamaican.commands`
This commit is contained in:
		
							parent
							
								
									37e42eebbc
								
							
						
					
					
						commit
						c3914738d5
					
				
					 23 changed files with 753 additions and 598 deletions
				
			
		
							
								
								
									
										6
									
								
								docs/api/wuttjamaican/cmd.base.rst
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								docs/api/wuttjamaican/cmd.base.rst
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | |||
| 
 | ||||
| ``wuttjamaican.cmd.base`` | ||||
| ========================= | ||||
| 
 | ||||
| .. automodule:: wuttjamaican.cmd.base | ||||
|    :members: | ||||
							
								
								
									
										8
									
								
								docs/api/wuttjamaican/cmd.make_appdir.rst
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								docs/api/wuttjamaican/cmd.make_appdir.rst
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,8 @@ | |||
| 
 | ||||
| ``wuttjamaican.cmd.make_appdir`` | ||||
| ================================ | ||||
| 
 | ||||
| .. automodule:: wuttjamaican.cmd.make_appdir | ||||
|    :members: | ||||
| 
 | ||||
| .. program-output:: wutta make-appdir -h | ||||
							
								
								
									
										6
									
								
								docs/api/wuttjamaican/cmd.rst
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								docs/api/wuttjamaican/cmd.rst
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | |||
| 
 | ||||
| ``wuttjamaican.cmd`` | ||||
| ==================== | ||||
| 
 | ||||
| .. automodule:: wuttjamaican.cmd | ||||
|    :members: | ||||
							
								
								
									
										8
									
								
								docs/api/wuttjamaican/cmd.setup.rst
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								docs/api/wuttjamaican/cmd.setup.rst
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,8 @@ | |||
| 
 | ||||
| ``wuttjamaican.cmd.setup`` | ||||
| ========================== | ||||
| 
 | ||||
| .. automodule:: wuttjamaican.cmd.setup | ||||
|    :members: | ||||
| 
 | ||||
| .. program-output:: wutta setup -h | ||||
|  | @ -1,6 +0,0 @@ | |||
| 
 | ||||
| ``wuttjamaican.commands.base`` | ||||
| ============================== | ||||
| 
 | ||||
| .. automodule:: wuttjamaican.commands.base | ||||
|    :members: | ||||
|  | @ -1,6 +0,0 @@ | |||
| 
 | ||||
| ``wuttjamaican.commands.make_appdir`` | ||||
| ===================================== | ||||
| 
 | ||||
| .. automodule:: wuttjamaican.commands.make_appdir | ||||
|    :members: | ||||
|  | @ -1,6 +0,0 @@ | |||
| 
 | ||||
| ``wuttjamaican.commands`` | ||||
| ========================= | ||||
| 
 | ||||
| .. automodule:: wuttjamaican.commands | ||||
|    :members: | ||||
|  | @ -1,6 +0,0 @@ | |||
| 
 | ||||
| ``wuttjamaican.commands.setup`` | ||||
| =============================== | ||||
| 
 | ||||
| .. automodule:: wuttjamaican.commands.setup | ||||
|    :members: | ||||
|  | @ -8,10 +8,10 @@ | |||
|    :maxdepth: 1 | ||||
| 
 | ||||
|    app | ||||
|    commands | ||||
|    commands.base | ||||
|    commands.make_appdir | ||||
|    commands.setup | ||||
|    cmd | ||||
|    cmd.base | ||||
|    cmd.make_appdir | ||||
|    cmd.setup | ||||
|    conf | ||||
|    db | ||||
|    db.conf | ||||
|  |  | |||
|  | @ -17,6 +17,7 @@ release = '0.1' | |||
| extensions = [ | ||||
|     'sphinx.ext.autodoc', | ||||
|     'sphinx.ext.intersphinx', | ||||
|     'sphinxcontrib.programoutput', | ||||
|     'sphinx.ext.viewcode', | ||||
|     'sphinx.ext.todo', | ||||
| ] | ||||
|  |  | |||
|  | @ -41,15 +41,15 @@ where = src | |||
| 
 | ||||
| [options.extras_require] | ||||
| db = SQLAlchemy<2 | ||||
| docs = Sphinx | ||||
| docs = Sphinx; sphinxcontrib-programoutput | ||||
| tests = pytest-cov; tox | ||||
| 
 | ||||
| 
 | ||||
| [options.entry_points] | ||||
| 
 | ||||
| console_scripts = | ||||
|         wutta = wuttjamaican.commands.base:main | ||||
|         wutta = wuttjamaican.cmd.base:main | ||||
| 
 | ||||
| wutta.subcommands = | ||||
|         make-appdir = wuttjamaican.commands.make_appdir:MakeAppDir | ||||
|         setup = wuttjamaican.commands.setup:Setup | ||||
|         make-appdir = wuttjamaican.cmd.make_appdir:MakeAppDir | ||||
|         setup = wuttjamaican.cmd.setup:Setup | ||||
|  |  | |||
							
								
								
									
										33
									
								
								src/wuttjamaican/cmd/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/wuttjamaican/cmd/__init__.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | |||
| # -*- coding: utf-8; -*- | ||||
| ################################################################################ | ||||
| # | ||||
| #  WuttJamaican -- Base package for Wutta Framework | ||||
| #  Copyright © 2023 Lance Edgar | ||||
| # | ||||
| #  This file is part of Wutta Framework. | ||||
| # | ||||
| #  Wutta Framework is free software: you can redistribute it and/or modify it | ||||
| #  under the terms of the GNU General Public License as published by the Free | ||||
| #  Software Foundation, either version 3 of the License, or (at your option) any | ||||
| #  later version. | ||||
| # | ||||
| #  Wutta Framework is distributed in the hope that it will be useful, but | ||||
| #  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||||
| #  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for | ||||
| #  more details. | ||||
| # | ||||
| #  You should have received a copy of the GNU General Public License along with | ||||
| #  Wutta Framework.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| ################################################################################ | ||||
| """ | ||||
| WuttJamaican - command line interface | ||||
| 
 | ||||
| For convenience, from this ``wuttjamaican.cmd`` namespace you can | ||||
| access the following: | ||||
| 
 | ||||
| * :class:`~wuttjamaican.cmd.base.Command` | ||||
| * :class:`~wuttjamaican.cmd.base.Subcommand` | ||||
| """ | ||||
| 
 | ||||
| from .base import Command, Subcommand | ||||
							
								
								
									
										528
									
								
								src/wuttjamaican/cmd/base.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										528
									
								
								src/wuttjamaican/cmd/base.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,528 @@ | |||
| # -*- coding: utf-8; -*- | ||||
| ################################################################################ | ||||
| # | ||||
| #  WuttJamaican -- Base package for Wutta Framework | ||||
| #  Copyright © 2023 Lance Edgar | ||||
| # | ||||
| #  This file is part of Wutta Framework. | ||||
| # | ||||
| #  Wutta Framework is free software: you can redistribute it and/or modify it | ||||
| #  under the terms of the GNU General Public License as published by the Free | ||||
| #  Software Foundation, either version 3 of the License, or (at your option) any | ||||
| #  later version. | ||||
| # | ||||
| #  Wutta Framework is distributed in the hope that it will be useful, but | ||||
| #  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||||
| #  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for | ||||
| #  more details. | ||||
| # | ||||
| #  You should have received a copy of the GNU General Public License along with | ||||
| #  Wutta Framework.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| ################################################################################ | ||||
| """ | ||||
| WuttJamaican - command framework | ||||
| """ | ||||
| 
 | ||||
| import argparse | ||||
| import logging | ||||
| import sys | ||||
| import warnings | ||||
| 
 | ||||
| from wuttjamaican import __version__ | ||||
| from wuttjamaican.conf import make_config | ||||
| from wuttjamaican.util import load_entry_points | ||||
| 
 | ||||
| 
 | ||||
| log = logging.getLogger(__name__) | ||||
| 
 | ||||
| 
 | ||||
| class Command: | ||||
|     """ | ||||
|     Primary command for the application. | ||||
| 
 | ||||
|     A primary command will usually have multiple subcommands it can | ||||
|     run.  The typical command line interface is like: | ||||
| 
 | ||||
|     .. code-block:: none | ||||
| 
 | ||||
|        <command> [command-options] <subcommand> [subcommand-options] | ||||
| 
 | ||||
|     :class:`Subcommand` will contain most of the logic, in terms of | ||||
|     what actually happens when it runs.  Top-level commands are mostly | ||||
|     a stub for sake of logically grouping the subcommands. | ||||
| 
 | ||||
|     :param config: Optional config object to use. | ||||
| 
 | ||||
|        Usually a command is being ran via actual command line, and | ||||
|        there is no config object yet so it must create one.  (It does | ||||
|        this within its :meth:`run()` method.) | ||||
| 
 | ||||
|        But if you already have a config object you can specify it here | ||||
|        and it will be used instead. | ||||
| 
 | ||||
|     :param name: Optional value to assign to :attr:`name`.  Usually | ||||
|        this is declared within the command class definition, but if | ||||
|        needed it can be provided dynamically. | ||||
| 
 | ||||
|     :param stdout: Optional replacement to use for :attr:`stdout`. | ||||
| 
 | ||||
|     :param stderr: Optional replacement to use for :attr:`stderr`. | ||||
| 
 | ||||
|     :param subcommands: Optional dictionary to use for | ||||
|        :attr:`subcommands`, instead of loading those via entry points. | ||||
| 
 | ||||
|     The base class serves as the primary ``wutta`` command for | ||||
|     WuttJamaican.  Most apps will subclass this and register their own | ||||
|     top-level command, and create subcommands as needed. | ||||
| 
 | ||||
|     To do that, first create your Command class, and a ``main()`` | ||||
|     entry point function for it (e.g. in ``poser/commands.py``):: | ||||
| 
 | ||||
|        import sys | ||||
|        from wuttjamaican.commands import Command | ||||
| 
 | ||||
|        class Poser(Command): | ||||
|            name = 'poser' | ||||
|            description = 'my custom top-level command' | ||||
|            version = '0.1' | ||||
| 
 | ||||
|        def poser_main(*args): | ||||
|            args = list(args) or sys.argv[1:] | ||||
|            cmd = Poser() | ||||
|            cmd.run(*args) | ||||
| 
 | ||||
|     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: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.: | ||||
| 
 | ||||
|     .. code-block:: sh | ||||
| 
 | ||||
|        cd /path/to/venv | ||||
|        bin/poser --help | ||||
|        bin/wutta-poser --help | ||||
| 
 | ||||
|     And see :class:`Subcommand` for info about adding those. | ||||
| 
 | ||||
|     .. attribute::  name | ||||
| 
 | ||||
|        Name of the primary command, e.g. ``wutta`` | ||||
| 
 | ||||
|     .. attribute:: description | ||||
| 
 | ||||
|        Description of the app itself or the primary command. | ||||
| 
 | ||||
|     .. attribute:: version | ||||
| 
 | ||||
|        Version string for the app or primary command. | ||||
| 
 | ||||
|     .. attribute:: stdout | ||||
| 
 | ||||
|        Reference to file-like object which should be used for writing | ||||
|        to STDOUT.  By default this is just ``sys.stdout``. | ||||
| 
 | ||||
|     .. attribute:: stderr | ||||
| 
 | ||||
|        Reference to file-like object which should be used for writing | ||||
|        to STDERR.  By default this is just ``sys.stderr``. | ||||
| 
 | ||||
|     .. attribute:: subcommands | ||||
| 
 | ||||
|        Dictionary of available subcommand classes, keyed by subcommand | ||||
|        name.  These are usually loaded from setuptools entry points. | ||||
|     """ | ||||
|     name = 'wutta' | ||||
|     version = __version__ | ||||
|     description = "Wutta Software Framework" | ||||
| 
 | ||||
|     def __init__( | ||||
|             self, | ||||
|             config=None, | ||||
|             name=None, | ||||
|             stdout=None, | ||||
|             stderr=None, | ||||
|             subcommands=None): | ||||
| 
 | ||||
|         self.config = config | ||||
|         self.name = name or self.name | ||||
|         self.stdout = stdout or sys.stdout | ||||
|         self.stderr = stderr or sys.stderr | ||||
| 
 | ||||
|         # nb. default entry point is like 'wutta_poser.subcommands' | ||||
|         safe_name = self.name.replace('-', '_') | ||||
|         self.subcommands = subcommands or load_entry_points(f'{safe_name}.subcommands') | ||||
|         if not self.subcommands: | ||||
| 
 | ||||
|             # nb. legacy entry point is like 'wutta_poser.commands' | ||||
|             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 '{safe_name}.subcommands' instead") | ||||
|                 warnings.warn(msg, DeprecationWarning, stacklevel=2) | ||||
|                 log.warning(msg) | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
| 
 | ||||
|     def sorted_subcommands(self): | ||||
|         """ | ||||
|         Get the list of subcommand classes, sorted by name. | ||||
|         """ | ||||
|         return [self.subcommands[name] | ||||
|                 for name in sorted(self.subcommands)] | ||||
| 
 | ||||
|     def print_help(self): | ||||
|         """ | ||||
|         Print usage help text for the main command. | ||||
|         """ | ||||
|         self.parser.print_help() | ||||
| 
 | ||||
|     def run(self, *args): | ||||
|         """ | ||||
|         Parse command line arguments and execute appropriate | ||||
|         subcommand. | ||||
| 
 | ||||
|         Or, if requested, or args are ambiguous, show help for either | ||||
|         the top-level or subcommand. | ||||
| 
 | ||||
|         Usually of course this method is invoked by way of command | ||||
|         line.  But if you need to run it programmatically, you must | ||||
|         specify the full command line args *except* not the top-level | ||||
|         command name.  So for example to run the equivalent of this | ||||
|         command line: | ||||
| 
 | ||||
|         .. code-block:: sh | ||||
| 
 | ||||
|            wutta setup --help | ||||
| 
 | ||||
|         You could do this in Python:: | ||||
| 
 | ||||
|            from wuttjamaican.commands import Command | ||||
| 
 | ||||
|            cmd = Command() | ||||
|            assert cmd.name == 'wutta' | ||||
|            cmd.run('setup', '--help') | ||||
|         """ | ||||
|         # build arg parser | ||||
|         self.parser = self.make_arg_parser() | ||||
|         self.add_args() | ||||
| 
 | ||||
|         # primary parser gets first pass at full args, and stores | ||||
|         # everything not used within args.argv | ||||
|         args = self.parser.parse_args(args) | ||||
|         if not args or not args.argv: | ||||
|             self.print_help() | ||||
|             sys.exit(1) | ||||
| 
 | ||||
|         # then argv should include <subcommand> [subcommand-options] | ||||
|         subcmd = args.argv[0] | ||||
|         if subcmd in self.subcommands: | ||||
|             if '-h' in args.argv or '--help' in args.argv: | ||||
|                 subcmd = self.subcommands[subcmd](self) | ||||
|                 subcmd.print_help() | ||||
|                 sys.exit(0) | ||||
|         else: | ||||
|             self.print_help() | ||||
|             sys.exit(1) | ||||
| 
 | ||||
|         # we should be done needing to print help messages. now it's | ||||
|         # safe to redirect STDOUT/STDERR, if necessary | ||||
|         if args.stdout: | ||||
|             self.stdout = args.stdout | ||||
|         if args.stderr: | ||||
|             self.stderr = args.stderr | ||||
| 
 | ||||
|         # make the config object | ||||
|         if not self.config: | ||||
|             self.config = self.make_config(args) | ||||
| 
 | ||||
|         # invoke subcommand | ||||
|         log.debug("running command line: %s", sys.argv) | ||||
|         subcmd = self.subcommands[subcmd](self) | ||||
|         self.prep_subcommand(subcmd, args) | ||||
|         subcmd._run(*args.argv[1:]) | ||||
| 
 | ||||
|         # nb. must flush these in case they are file objects | ||||
|         self.stdout.flush() | ||||
|         self.stderr.flush() | ||||
| 
 | ||||
|     def make_arg_parser(self): | ||||
|         """ | ||||
|         Must return a new :class:`argparse.ArgumentParser` instance | ||||
|         for use by the main command. | ||||
| 
 | ||||
|         This will use :class:`CommandArgumentParser` by default. | ||||
|         """ | ||||
|         subcommands = "" | ||||
|         for subcmd in self.sorted_subcommands(): | ||||
|             subcommands += f"  {subcmd.name:<20s}  {subcmd.description}\n" | ||||
| 
 | ||||
|         epilog = f"""\ | ||||
| subcommands: | ||||
| {subcommands} | ||||
| 
 | ||||
| also try: {self.name} <subcommand> -h | ||||
| """ | ||||
| 
 | ||||
|         return CommandArgumentParser( | ||||
|             prog=self.name, | ||||
|             description=self.description, | ||||
|             add_help=False, | ||||
|             usage=f"{self.name} [options] <subcommand> [subcommand-options]", | ||||
|             epilog=epilog, | ||||
|             formatter_class=argparse.RawDescriptionHelpFormatter) | ||||
| 
 | ||||
|     def add_args(self): | ||||
|         """ | ||||
|         Configure args for the main command arg parser. | ||||
| 
 | ||||
|         Anything you setup here will then be available when the | ||||
|         command runs.  You can add arguments directly to | ||||
|         ``self.parser``, e.g.:: | ||||
| 
 | ||||
|            self.parser.add_argument('--foo', default='bar', help="Foo value") | ||||
| 
 | ||||
|         See also docs for :meth:`python:argparse.ArgumentParser.add_argument()`. | ||||
|         """ | ||||
|         parser = self.parser | ||||
| 
 | ||||
|         parser.add_argument('-c', '--config', metavar='PATH', | ||||
|                             action='append', dest='config_paths', | ||||
|                             help="Config path (may be specified more than once)") | ||||
| 
 | ||||
|         parser.add_argument('--plus-config', metavar='PATH', | ||||
|                             action='append', dest='plus_config_paths', | ||||
|                             help="Extra configs to load in addition to normal config") | ||||
| 
 | ||||
|         parser.add_argument('-P', '--progress', action='store_true', default=False, | ||||
|                             help="Report progress when relevant") | ||||
| 
 | ||||
|         parser.add_argument('-V', '--version', action='version', | ||||
|                             version=f"%(prog)s {self.version}") | ||||
| 
 | ||||
|         parser.add_argument('--stdout', metavar='PATH', type=argparse.FileType('w'), | ||||
|                             help="Optional path to which STDOUT should be written.") | ||||
|         parser.add_argument('--stderr', metavar='PATH', type=argparse.FileType('w'), | ||||
|                             help="Optional path to which STDERR should be written.") | ||||
| 
 | ||||
|     def make_config(self, args): | ||||
|         """ | ||||
|         Make the config object in preparation for running a subcommand. | ||||
| 
 | ||||
|         By default this is a straightforward wrapper around | ||||
|         :func:`wuttjamaican.conf.make_config()`. | ||||
| 
 | ||||
|         :returns: The new config object. | ||||
|         """ | ||||
|         return make_config(args.config_paths, | ||||
|                            plus_files=args.plus_config_paths) | ||||
| 
 | ||||
|     def prep_subcommand(self, subcommand, args): | ||||
|         """ | ||||
|         Prepare the subcommand for running, as needed. | ||||
|         """ | ||||
| 
 | ||||
| 
 | ||||
| class CommandArgumentParser(argparse.ArgumentParser): | ||||
|     """ | ||||
|     Custom argument parser for use with :class:`Command`. | ||||
| 
 | ||||
|     This overrides some of the parsing logic which is specific to the | ||||
|     primary command object, to separate command options from | ||||
|     subcommand options. | ||||
| 
 | ||||
|     This is documented as FYI but you probably should not need to know | ||||
|     about or try to use this yourself.  It will be used automatically | ||||
|     by ``Command`` or a subclass thereof. | ||||
|     """ | ||||
| 
 | ||||
|     def parse_args(self, args=None, namespace=None): | ||||
|         args, argv = self.parse_known_args(args, namespace) | ||||
|         args.argv = argv | ||||
|         return args | ||||
| 
 | ||||
| 
 | ||||
| class Subcommand: | ||||
|     """ | ||||
|     Base class for application subcommands. | ||||
| 
 | ||||
|     Subcommands are where the real action happens.  Each must define | ||||
|     the :meth:`run()` method with whatever logic is needed.  They can | ||||
|     also define :meth:`add_args()` to expose options. | ||||
| 
 | ||||
|     Subcommands always belong to a top-level command - the association | ||||
|     is made by way of entry point registration, and the constructor | ||||
|     for this class. | ||||
| 
 | ||||
|     :param command: Reference to top-level :class:`Command` object. | ||||
| 
 | ||||
|     Note that unlike :class:`Command`, the base ``Subcommand`` does | ||||
|     not correspond to any real subcommand for WuttJamaican.  (It's | ||||
|     *only* a base class.)  For a real example see | ||||
|     :class:`~wuttjamaican.commands.setup.Setup`. | ||||
| 
 | ||||
|     In your project you can define new subcommands for any top-level | ||||
|     command.  For instance to add a ``hello`` subcommand to the | ||||
|     ``poser`` command example (cf. :class:`Command` docs): | ||||
| 
 | ||||
|     First create a Subcommand class (e.g. by adding to | ||||
|     ``poser/commands.py``):: | ||||
| 
 | ||||
|        from wuttjamaican.commands import Subcommand | ||||
| 
 | ||||
|        class Hello(Subcommand): | ||||
|            \""" | ||||
|            Say hello to the user | ||||
|            \""" | ||||
|            name = 'hello' | ||||
|            description = __doc__.strip() | ||||
| 
 | ||||
|            def add_args(self): | ||||
|                self.parser.add_argument('--foo', default='bar', help="Foo value") | ||||
| 
 | ||||
|            def run(self, args): | ||||
|                print("hello, foo value is:", args.foo) | ||||
| 
 | ||||
|     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. | ||||
| 
 | ||||
|     As with top-level commands, you can "alias" the same subcommand so | ||||
|     it appears under multiple top-level commands.  Note that if the | ||||
|     top-level command name contains a hyphen, that must be replaced | ||||
|     with underscore for sake of the subcommand entry point: | ||||
| 
 | ||||
|     .. code-block:: ini | ||||
| 
 | ||||
|        [options.entry_points] | ||||
|        poser.subcommands = | ||||
|            hello = poser.commands:Hello | ||||
|        wutta_poser.subcommands = | ||||
|            hello = poser.commands:Hello | ||||
| 
 | ||||
|     Next time your (``poser``) package is installed, the subcommand | ||||
|     will be available, so you can e.g.: | ||||
| 
 | ||||
|     .. code-block:: sh | ||||
| 
 | ||||
|        cd /path/to/venv | ||||
|        bin/poser hello --help | ||||
|        bin/wutta-poser hello --help | ||||
| 
 | ||||
|     .. attribute:: stdout | ||||
| 
 | ||||
|        Reference to file-like object which should be used for writing | ||||
|        to STDOUT.  This is inherited from :attr:`Command.stdout`. | ||||
| 
 | ||||
|     .. attribute:: stderr | ||||
| 
 | ||||
|        Reference to file-like object which should be used for writing | ||||
|        to STDERR.  This is inherited from :attr:`Command.stderr`. | ||||
|     """ | ||||
|     name = 'UNDEFINED' | ||||
|     description = "TODO: not defined" | ||||
| 
 | ||||
|     def __init__( | ||||
|             self, | ||||
|             command, | ||||
|     ): | ||||
|         self.command = command | ||||
|         self.stdout = self.command.stdout | ||||
|         self.stderr = self.command.stderr | ||||
|         self.config = self.command.config | ||||
|         if self.config: | ||||
|             self.app = self.config.get_app() | ||||
| 
 | ||||
|         # build arg parser | ||||
|         self.parser = self.make_arg_parser() | ||||
|         self.add_args() | ||||
| 
 | ||||
|     def __repr__(self): | ||||
|         return f"Subcommand(name={self.name})" | ||||
| 
 | ||||
|     def make_arg_parser(self): | ||||
|         """ | ||||
|         Must return a new :class:`argparse.ArgumentParser` instance | ||||
|         for use by the subcommand. | ||||
|         """ | ||||
|         return argparse.ArgumentParser( | ||||
|             prog=f'{self.command.name} {self.name}', | ||||
|             description=self.description) | ||||
| 
 | ||||
|     def add_args(self): | ||||
|         """ | ||||
|         Configure additional args for the subcommand arg parser. | ||||
| 
 | ||||
|         Anything you setup here will then be available within | ||||
|         :meth:`run()`.  You can add arguments directly to | ||||
|         ``self.parser``, e.g.:: | ||||
| 
 | ||||
|            self.parser.add_argument('--foo', default='bar', help="Foo value") | ||||
| 
 | ||||
|         See also docs for :meth:`python:argparse.ArgumentParser.add_argument()`. | ||||
|         """ | ||||
| 
 | ||||
|     def print_help(self): | ||||
|         """ | ||||
|         Print usage help text for the subcommand. | ||||
|         """ | ||||
|         self.parser.print_help() | ||||
| 
 | ||||
|     def _run(self, *args): | ||||
|         args = self.parser.parse_args(args) | ||||
|         return self.run(args) | ||||
| 
 | ||||
|     def run(self, args): | ||||
|         """ | ||||
|         Run the subcommand logic.  Subclass should override this. | ||||
| 
 | ||||
|         :param args: Reference to the | ||||
|            :class:`python:argparse.Namespace` object, as returned by | ||||
|            the subcommand arg parser. | ||||
| 
 | ||||
|         The ``args`` should have values for everything setup in | ||||
|         :meth:`add_args()`.  For example if you added the ``--foo`` | ||||
|         arg then here in ``run()`` you can do:: | ||||
| 
 | ||||
|            print("foo value is:", args.foo) | ||||
| 
 | ||||
|         Usually of course this method is invoked by way of command | ||||
|         line. But if you need to run it programmatically, you should | ||||
|         *not* try to invoke this method directly.  Instead create the | ||||
|         ``Command`` object and invoke its :meth:`~Command.run()` | ||||
|         method. | ||||
| 
 | ||||
|         For a command line like ``bin/poser hello --foo=baz`` then, | ||||
|         you might do this:: | ||||
| 
 | ||||
|            from poser.commands import Poser | ||||
| 
 | ||||
|            cmd = Poser() | ||||
|            assert cmd.name == 'poser' | ||||
|            cmd.run('hello', '--foo=baz') | ||||
|         """ | ||||
|         self.stdout.write("TODO: command logic not yet implemented\n") | ||||
| 
 | ||||
| 
 | ||||
| def main(*args): | ||||
|     """ | ||||
|     Primary entry point for the ``wutta`` command. | ||||
|     """ | ||||
|     args = list(args) or sys.argv[1:] | ||||
| 
 | ||||
|     cmd = Command() | ||||
|     cmd.run(*args) | ||||
							
								
								
									
										68
									
								
								src/wuttjamaican/cmd/make_appdir.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/wuttjamaican/cmd/make_appdir.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,68 @@ | |||
| # -*- coding: utf-8; -*- | ||||
| ################################################################################ | ||||
| # | ||||
| #  WuttJamaican -- Base package for Wutta Framework | ||||
| #  Copyright © 2023 Lance Edgar | ||||
| # | ||||
| #  This file is part of Wutta Framework. | ||||
| # | ||||
| #  Wutta Framework is free software: you can redistribute it and/or modify it | ||||
| #  under the terms of the GNU General Public License as published by the Free | ||||
| #  Software Foundation, either version 3 of the License, or (at your option) any | ||||
| #  later version. | ||||
| # | ||||
| #  Wutta Framework is distributed in the hope that it will be useful, but | ||||
| #  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||||
| #  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for | ||||
| #  more details. | ||||
| # | ||||
| #  You should have received a copy of the GNU General Public License along with | ||||
| #  Wutta Framework.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| ################################################################################ | ||||
| """ | ||||
| WuttJamaican - subcommand ``make-appdir`` | ||||
| """ | ||||
| 
 | ||||
| import os | ||||
| import sys | ||||
| 
 | ||||
| from .base import Subcommand | ||||
| 
 | ||||
| 
 | ||||
| class MakeAppDir(Subcommand): | ||||
|     """ | ||||
|     Make or refresh the "app dir" for virtual environment | ||||
|     """ | ||||
|     name = 'make-appdir' | ||||
|     description = __doc__.strip() | ||||
| 
 | ||||
|     def add_args(self): | ||||
|         """ """ | ||||
|         self.parser.add_argument('--path', metavar='APPDIR', | ||||
|                                  help="Optional path to desired app dir.  If not specified " | ||||
|                                  "it will be named ``app`` and  placed in the root of the " | ||||
|                                  "virtual environment.") | ||||
| 
 | ||||
|     def run(self, args): | ||||
|         """ | ||||
|         This may be used during setup to establish the :term:`app dir` | ||||
|         for a virtual environment.  This folder will contain config | ||||
|         files, log files etc. used by the app. | ||||
|         """ | ||||
|         if args.path: | ||||
|             appdir = os.path.abspath(args.path) | ||||
|         else: | ||||
|             appdir = os.path.join(sys.prefix, 'app') | ||||
| 
 | ||||
|         self.make_appdir(appdir, args) | ||||
|         self.stdout.write(f"established appdir: {appdir}\n") | ||||
| 
 | ||||
|     def make_appdir(self, appdir, args): | ||||
|         """ | ||||
|         Make the :term:`app dir` for the given path. | ||||
| 
 | ||||
|         Calls :meth:`~wuttjamaican.app.AppHandler.make_appdir()` to do | ||||
|         the heavy lifting. | ||||
|         """ | ||||
|         self.app.make_appdir(appdir) | ||||
							
								
								
									
										42
									
								
								src/wuttjamaican/cmd/setup.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/wuttjamaican/cmd/setup.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,42 @@ | |||
| # -*- coding: utf-8; -*- | ||||
| ################################################################################ | ||||
| # | ||||
| #  WuttJamaican -- Base package for Wutta Framework | ||||
| #  Copyright © 2023 Lance Edgar | ||||
| # | ||||
| #  This file is part of Wutta Framework. | ||||
| # | ||||
| #  Wutta Framework is free software: you can redistribute it and/or modify it | ||||
| #  under the terms of the GNU General Public License as published by the Free | ||||
| #  Software Foundation, either version 3 of the License, or (at your option) any | ||||
| #  later version. | ||||
| # | ||||
| #  Wutta Framework is distributed in the hope that it will be useful, but | ||||
| #  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||||
| #  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for | ||||
| #  more details. | ||||
| # | ||||
| #  You should have received a copy of the GNU General Public License along with | ||||
| #  Wutta Framework.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| ################################################################################ | ||||
| """ | ||||
| WuttJamaican - subcommand ``setup`` | ||||
| """ | ||||
| 
 | ||||
| from .base import Subcommand | ||||
| 
 | ||||
| 
 | ||||
| class Setup(Subcommand): | ||||
|     """ | ||||
|     Install and configure various software | ||||
|     """ | ||||
|     name = 'setup' | ||||
|     description = __doc__.strip() | ||||
| 
 | ||||
|     def run(self, args): | ||||
|         """ | ||||
|         TODO: Eventually this will run an interactive setup utility. | ||||
|         But it doesn't yet. | ||||
|         """ | ||||
|         self.stderr.write("TODO: setup is not yet implemented\n") | ||||
|  | @ -21,13 +21,13 @@ | |||
| # | ||||
| ################################################################################ | ||||
| """ | ||||
| WuttJamaican - command line | ||||
| 
 | ||||
| For convenience, from this ``wuttjamaican.commands`` namespace you can | ||||
| access the following: | ||||
| 
 | ||||
| * :class:`~wuttjamaican.commands.base.Command` | ||||
| * :class:`~wuttjamaican.commands.base.Subcommand` | ||||
| WuttJamaican - command line interface | ||||
| """ | ||||
| 
 | ||||
| from .base import Command, Subcommand | ||||
| import warnings | ||||
| 
 | ||||
| warnings.warn("wuttjamaican.commands package is deprecated; " | ||||
|               "please use wuttjamaican.cmd instead", | ||||
|               DeprecationWarning, stacklevel=2) | ||||
| 
 | ||||
| from wuttjamaican.cmd import * | ||||
|  |  | |||
|  | @ -21,508 +21,13 @@ | |||
| # | ||||
| ################################################################################ | ||||
| """ | ||||
| WuttJamaican - command framework | ||||
| WuttJamaican - base classes for cli | ||||
| """ | ||||
| 
 | ||||
| import argparse | ||||
| import logging | ||||
| import sys | ||||
| import warnings | ||||
| 
 | ||||
| from wuttjamaican import __version__ | ||||
| from wuttjamaican.conf import make_config | ||||
| from wuttjamaican.util import load_entry_points | ||||
| warnings.warn("wuttjamaican.commands package is deprecated; " | ||||
|               "please use wuttjamaican.cmd instead", | ||||
|               DeprecationWarning, stacklevel=2) | ||||
| 
 | ||||
| 
 | ||||
| log = logging.getLogger(__name__) | ||||
| 
 | ||||
| 
 | ||||
| class Command: | ||||
|     """ | ||||
|     Primary command for the application. | ||||
| 
 | ||||
|     A primary command will usually have multiple subcommands it can | ||||
|     run.  The typical command line interface is like: | ||||
| 
 | ||||
|     .. code-block:: none | ||||
| 
 | ||||
|        <command> [command-options] <subcommand> [subcommand-options] | ||||
| 
 | ||||
|     :class:`Subcommand` will contain most of the logic, in terms of | ||||
|     what actually happens when it runs.  Top-level commands are mostly | ||||
|     a stub for sake of logically grouping the subcommands. | ||||
| 
 | ||||
|     :param config: Optional config object to use. | ||||
| 
 | ||||
|        Usually a command is being ran via actual command line, and | ||||
|        there is no config object yet so it must create one.  (It does | ||||
|        this within its :meth:`run()` method.) | ||||
| 
 | ||||
|        But if you already have a config object you can specify it here | ||||
|        and it will be used instead. | ||||
| 
 | ||||
|     :param name: Optional value to assign to :attr:`name`.  Usually | ||||
|        this is declared within the command class definition, but if | ||||
|        needed it can be provided dynamically. | ||||
| 
 | ||||
|     :param stdout: Optional replacement to use for :attr:`stdout`. | ||||
| 
 | ||||
|     :param stderr: Optional replacement to use for :attr:`stderr`. | ||||
| 
 | ||||
|     :param subcommands: Optional dictionary to use for | ||||
|        :attr:`subcommands`, instead of loading those via entry points. | ||||
| 
 | ||||
|     The base class serves as the primary ``wutta`` command for | ||||
|     WuttJamaican.  Most apps will subclass this and register their own | ||||
|     top-level command, and create subcommands as needed. | ||||
| 
 | ||||
|     To do that, first create your Command class, and a ``main()`` | ||||
|     entry point function for it (e.g. in ``poser/commands.py``):: | ||||
| 
 | ||||
|        import sys | ||||
|        from wuttjamaican.commands import Command | ||||
| 
 | ||||
|        class Poser(Command): | ||||
|            name = 'poser' | ||||
|            description = 'my custom top-level command' | ||||
|            version = '0.1' | ||||
| 
 | ||||
|        def poser_main(*args): | ||||
|            args = list(args) or sys.argv[1:] | ||||
|            cmd = Poser() | ||||
|            cmd.run(*args) | ||||
| 
 | ||||
|     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: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.: | ||||
| 
 | ||||
|     .. code-block:: sh | ||||
| 
 | ||||
|        cd /path/to/venv | ||||
|        bin/poser --help | ||||
|        bin/wutta-poser --help | ||||
| 
 | ||||
|     And see :class:`Subcommand` for info about adding those. | ||||
| 
 | ||||
|     .. attribute::  name | ||||
| 
 | ||||
|        Name of the primary command, e.g. ``wutta`` | ||||
| 
 | ||||
|     .. attribute:: description | ||||
| 
 | ||||
|        Description of the app itself or the primary command. | ||||
| 
 | ||||
|     .. attribute:: version | ||||
| 
 | ||||
|        Version string for the app or primary command. | ||||
| 
 | ||||
|     .. attribute:: stdout | ||||
| 
 | ||||
|        Reference to file-like object which should be used for writing | ||||
|        to STDOUT.  By default this is just ``sys.stdout``. | ||||
| 
 | ||||
|     .. attribute:: stderr | ||||
| 
 | ||||
|        Reference to file-like object which should be used for writing | ||||
|        to STDERR.  By default this is just ``sys.stderr``. | ||||
| 
 | ||||
|     .. attribute:: subcommands | ||||
| 
 | ||||
|        Dictionary of available subcommand classes, keyed by subcommand | ||||
|        name.  These are usually loaded from setuptools entry points. | ||||
|     """ | ||||
|     name = 'wutta' | ||||
|     version = __version__ | ||||
|     description = "Wutta Software Framework" | ||||
| 
 | ||||
|     def __init__( | ||||
|             self, | ||||
|             config=None, | ||||
|             name=None, | ||||
|             stdout=None, | ||||
|             stderr=None, | ||||
|             subcommands=None): | ||||
| 
 | ||||
|         self.config = config | ||||
|         self.name = name or self.name | ||||
|         self.stdout = stdout or sys.stdout | ||||
|         self.stderr = stderr or sys.stderr | ||||
| 
 | ||||
|         # nb. default entry point is like 'wutta_poser.subcommands' | ||||
|         safe_name = self.name.replace('-', '_') | ||||
|         self.subcommands = subcommands or load_entry_points(f'{safe_name}.subcommands') | ||||
|         if not self.subcommands: | ||||
| 
 | ||||
|             # nb. legacy entry point is like 'wutta_poser.commands' | ||||
|             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 '{safe_name}.subcommands' instead") | ||||
|                 warnings.warn(msg, DeprecationWarning, stacklevel=2) | ||||
|                 log.warning(msg) | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
| 
 | ||||
|     def sorted_subcommands(self): | ||||
|         """ | ||||
|         Get the list of subcommand classes, sorted by name. | ||||
|         """ | ||||
|         return [self.subcommands[name] | ||||
|                 for name in sorted(self.subcommands)] | ||||
| 
 | ||||
|     def print_help(self): | ||||
|         """ | ||||
|         Print usage help text for the main command. | ||||
|         """ | ||||
|         self.parser.print_help() | ||||
| 
 | ||||
|     def run(self, *args): | ||||
|         """ | ||||
|         Parse command line arguments and execute appropriate | ||||
|         subcommand. | ||||
| 
 | ||||
|         Or, if requested, or args are ambiguous, show help for either | ||||
|         the top-level or subcommand. | ||||
| 
 | ||||
|         Usually of course this method is invoked by way of command | ||||
|         line.  But if you need to run it programmatically, you must | ||||
|         specify the full command line args *except* not the top-level | ||||
|         command name.  So for example to run the equivalent of this | ||||
|         command line: | ||||
| 
 | ||||
|         .. code-block:: sh | ||||
| 
 | ||||
|            wutta setup --help | ||||
| 
 | ||||
|         You could do this in Python:: | ||||
| 
 | ||||
|            from wuttjamaican.commands import Command | ||||
| 
 | ||||
|            cmd = Command() | ||||
|            assert cmd.name == 'wutta' | ||||
|            cmd.run('setup', '--help') | ||||
|         """ | ||||
|         # build arg parser | ||||
|         self.parser = self.make_arg_parser() | ||||
|         self.add_args() | ||||
| 
 | ||||
|         # primary parser gets first pass at full args, and stores | ||||
|         # everything not used within args.argv | ||||
|         args = self.parser.parse_args(args) | ||||
|         if not args or not args.argv: | ||||
|             self.print_help() | ||||
|             sys.exit(1) | ||||
| 
 | ||||
|         # then argv should include <subcommand> [subcommand-options] | ||||
|         subcmd = args.argv[0] | ||||
|         if subcmd in self.subcommands: | ||||
|             if '-h' in args.argv or '--help' in args.argv: | ||||
|                 subcmd = self.subcommands[subcmd](self) | ||||
|                 subcmd.print_help() | ||||
|                 sys.exit(0) | ||||
|         else: | ||||
|             self.print_help() | ||||
|             sys.exit(1) | ||||
| 
 | ||||
|         # we should be done needing to print help messages. now it's | ||||
|         # safe to redirect STDOUT/STDERR, if necessary | ||||
|         if args.stdout: | ||||
|             self.stdout = args.stdout | ||||
|         if args.stderr: | ||||
|             self.stderr = args.stderr | ||||
| 
 | ||||
|         # make the config object | ||||
|         if not self.config: | ||||
|             self.config = self.make_config(args) | ||||
| 
 | ||||
|         # invoke subcommand | ||||
|         log.debug("running command line: %s", sys.argv) | ||||
|         subcmd = self.subcommands[subcmd](self) | ||||
|         self.prep_subcommand(subcmd, args) | ||||
|         subcmd._run(*args.argv[1:]) | ||||
| 
 | ||||
|         # nb. must flush these in case they are file objects | ||||
|         self.stdout.flush() | ||||
|         self.stderr.flush() | ||||
| 
 | ||||
|     def make_arg_parser(self): | ||||
|         """ | ||||
|         Must return a new :class:`argparse.ArgumentParser` instance | ||||
|         for use by the main command. | ||||
| 
 | ||||
|         This will use :class:`CommandArgumentParser` by default. | ||||
|         """ | ||||
|         subcommands = "" | ||||
|         for subcmd in self.sorted_subcommands(): | ||||
|             subcommands += f"  {subcmd.name:<20s}  {subcmd.description}\n" | ||||
| 
 | ||||
|         epilog = f"""\ | ||||
| subcommands: | ||||
| {subcommands} | ||||
| 
 | ||||
| also try: {self.name} <subcommand> -h | ||||
| """ | ||||
| 
 | ||||
|         return CommandArgumentParser( | ||||
|             prog=self.name, | ||||
|             description=self.description, | ||||
|             add_help=False, | ||||
|             usage=f"{self.name} [options] <subcommand> [subcommand-options]", | ||||
|             epilog=epilog, | ||||
|             formatter_class=argparse.RawDescriptionHelpFormatter) | ||||
| 
 | ||||
|     def add_args(self): | ||||
|         """ | ||||
|         Configure args for the main command arg parser. | ||||
| 
 | ||||
|         Anything you setup here will then be available when the | ||||
|         command runs.  You can add arguments directly to | ||||
|         ``self.parser``, e.g.:: | ||||
| 
 | ||||
|            self.parser.add_argument('--foo', default='bar', help="Foo value") | ||||
| 
 | ||||
|         See also docs for :meth:`python:argparse.ArgumentParser.add_argument()`. | ||||
|         """ | ||||
|         parser = self.parser | ||||
| 
 | ||||
|         parser.add_argument('-c', '--config', metavar='PATH', | ||||
|                             action='append', dest='config_paths', | ||||
|                             help="Config path (may be specified more than once)") | ||||
| 
 | ||||
|         parser.add_argument('--plus-config', metavar='PATH', | ||||
|                             action='append', dest='plus_config_paths', | ||||
|                             help="Extra configs to load in addition to normal config") | ||||
| 
 | ||||
|         parser.add_argument('-P', '--progress', action='store_true', default=False, | ||||
|                             help="Report progress when relevant") | ||||
| 
 | ||||
|         parser.add_argument('-V', '--version', action='version', | ||||
|                             version=f"%(prog)s {self.version}") | ||||
| 
 | ||||
|         parser.add_argument('--stdout', metavar='PATH', type=argparse.FileType('w'), | ||||
|                             help="Optional path to which STDOUT should be written.") | ||||
|         parser.add_argument('--stderr', metavar='PATH', type=argparse.FileType('w'), | ||||
|                             help="Optional path to which STDERR should be written.") | ||||
| 
 | ||||
|     def make_config(self, args): | ||||
|         """ | ||||
|         Make the config object in preparation for running a subcommand. | ||||
| 
 | ||||
|         By default this is a straightforward wrapper around | ||||
|         :func:`wuttjamaican.conf.make_config()`. | ||||
| 
 | ||||
|         :returns: The new config object. | ||||
|         """ | ||||
|         return make_config(args.config_paths, | ||||
|                            plus_files=args.plus_config_paths) | ||||
| 
 | ||||
|     def prep_subcommand(self, subcommand, args): | ||||
|         """ | ||||
|         Prepare the subcommand for running, as needed. | ||||
|         """ | ||||
| 
 | ||||
| 
 | ||||
| class CommandArgumentParser(argparse.ArgumentParser): | ||||
|     """ | ||||
|     Custom argument parser for use with :class:`Command`. | ||||
| 
 | ||||
|     This overrides some of the parsing logic which is specific to the | ||||
|     primary command object, to separate command options from | ||||
|     subcommand options. | ||||
| 
 | ||||
|     This is documented as FYI but you probably should not need to know | ||||
|     about or try to use this yourself.  It will be used automatically | ||||
|     by ``Command`` or a subclass thereof. | ||||
|     """ | ||||
| 
 | ||||
|     def parse_args(self, args=None, namespace=None): | ||||
|         args, argv = self.parse_known_args(args, namespace) | ||||
|         args.argv = argv | ||||
|         return args | ||||
| 
 | ||||
| 
 | ||||
| class Subcommand: | ||||
|     """ | ||||
|     Base class for application subcommands. | ||||
| 
 | ||||
|     Subcommands are where the real action happens.  Each must define | ||||
|     the :meth:`run()` method with whatever logic is needed.  They can | ||||
|     also define :meth:`add_args()` to expose options. | ||||
| 
 | ||||
|     Subcommands always belong to a top-level command - the association | ||||
|     is made by way of entry point registration, and the constructor | ||||
|     for this class. | ||||
| 
 | ||||
|     :param command: Reference to top-level :class:`Command` object. | ||||
| 
 | ||||
|     Note that unlike :class:`Command`, the base ``Subcommand`` does | ||||
|     not correspond to any real subcommand for WuttJamaican.  (It's | ||||
|     *only* a base class.)  For a real example see | ||||
|     :class:`~wuttjamaican.commands.setup.Setup`. | ||||
| 
 | ||||
|     In your project you can define new subcommands for any top-level | ||||
|     command.  For instance to add a ``hello`` subcommand to the | ||||
|     ``poser`` command example (cf. :class:`Command` docs): | ||||
| 
 | ||||
|     First create a Subcommand class (e.g. by adding to | ||||
|     ``poser/commands.py``):: | ||||
| 
 | ||||
|        from wuttjamaican.commands import Subcommand | ||||
| 
 | ||||
|        class Hello(Subcommand): | ||||
|            \""" | ||||
|            Say hello to the user | ||||
|            \""" | ||||
|            name = 'hello' | ||||
|            description = __doc__.strip() | ||||
| 
 | ||||
|            def add_args(self): | ||||
|                self.parser.add_argument('--foo', default='bar', help="Foo value") | ||||
| 
 | ||||
|            def run(self, args): | ||||
|                print("hello, foo value is:", args.foo) | ||||
| 
 | ||||
|     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. | ||||
| 
 | ||||
|     As with top-level commands, you can "alias" the same subcommand so | ||||
|     it appears under multiple top-level commands.  Note that if the | ||||
|     top-level command name contains a hyphen, that must be replaced | ||||
|     with underscore for sake of the subcommand entry point: | ||||
| 
 | ||||
|     .. code-block:: ini | ||||
| 
 | ||||
|        [options.entry_points] | ||||
|        poser.subcommands = | ||||
|            hello = poser.commands:Hello | ||||
|        wutta_poser.subcommands = | ||||
|            hello = poser.commands:Hello | ||||
| 
 | ||||
|     Next time your (``poser``) package is installed, the subcommand | ||||
|     will be available, so you can e.g.: | ||||
| 
 | ||||
|     .. code-block:: sh | ||||
| 
 | ||||
|        cd /path/to/venv | ||||
|        bin/poser hello --help | ||||
|        bin/wutta-poser hello --help | ||||
| 
 | ||||
|     .. attribute:: stdout | ||||
| 
 | ||||
|        Reference to file-like object which should be used for writing | ||||
|        to STDOUT.  This is inherited from :attr:`Command.stdout`. | ||||
| 
 | ||||
|     .. attribute:: stderr | ||||
| 
 | ||||
|        Reference to file-like object which should be used for writing | ||||
|        to STDERR.  This is inherited from :attr:`Command.stderr`. | ||||
|     """ | ||||
|     name = 'UNDEFINED' | ||||
|     description = "TODO: not defined" | ||||
| 
 | ||||
|     def __init__( | ||||
|             self, | ||||
|             command, | ||||
|     ): | ||||
|         self.command = command | ||||
|         self.stdout = self.command.stdout | ||||
|         self.stderr = self.command.stderr | ||||
|         self.config = self.command.config | ||||
|         if self.config: | ||||
|             self.app = self.config.get_app() | ||||
| 
 | ||||
|         # build arg parser | ||||
|         self.parser = self.make_arg_parser() | ||||
|         self.add_args() | ||||
| 
 | ||||
|     def __repr__(self): | ||||
|         return f"Subcommand(name={self.name})" | ||||
| 
 | ||||
|     def make_arg_parser(self): | ||||
|         """ | ||||
|         Must return a new :class:`argparse.ArgumentParser` instance | ||||
|         for use by the subcommand. | ||||
|         """ | ||||
|         return argparse.ArgumentParser( | ||||
|             prog=f'{self.command.name} {self.name}', | ||||
|             description=self.description) | ||||
| 
 | ||||
|     def add_args(self): | ||||
|         """ | ||||
|         Configure additional args for the subcommand arg parser. | ||||
| 
 | ||||
|         Anything you setup here will then be available within | ||||
|         :meth:`run()`.  You can add arguments directly to | ||||
|         ``self.parser``, e.g.:: | ||||
| 
 | ||||
|            self.parser.add_argument('--foo', default='bar', help="Foo value") | ||||
| 
 | ||||
|         See also docs for :meth:`python:argparse.ArgumentParser.add_argument()`. | ||||
|         """ | ||||
| 
 | ||||
|     def print_help(self): | ||||
|         """ | ||||
|         Print usage help text for the subcommand. | ||||
|         """ | ||||
|         self.parser.print_help() | ||||
| 
 | ||||
|     def _run(self, *args): | ||||
|         args = self.parser.parse_args(args) | ||||
|         return self.run(args) | ||||
| 
 | ||||
|     def run(self, args): | ||||
|         """ | ||||
|         Run the subcommand logic.  Subclass should override this. | ||||
| 
 | ||||
|         :param args: Reference to the | ||||
|            :class:`python:argparse.Namespace` object, as returned by | ||||
|            the subcommand arg parser. | ||||
| 
 | ||||
|         The ``args`` should have values for everything setup in | ||||
|         :meth:`add_args()`.  For example if you added the ``--foo`` | ||||
|         arg then here in ``run()`` you can do:: | ||||
| 
 | ||||
|            print("foo value is:", args.foo) | ||||
| 
 | ||||
|         Usually of course this method is invoked by way of command | ||||
|         line. But if you need to run it programmatically, you should | ||||
|         *not* try to invoke this method directly.  Instead create the | ||||
|         ``Command`` object and invoke its :meth:`~Command.run()` | ||||
|         method. | ||||
| 
 | ||||
|         For a command line like ``bin/poser hello --foo=baz`` then, | ||||
|         you might do this:: | ||||
| 
 | ||||
|            from poser.commands import Poser | ||||
| 
 | ||||
|            cmd = Poser() | ||||
|            assert cmd.name == 'poser' | ||||
|            cmd.run('hello', '--foo=baz') | ||||
|         """ | ||||
|         self.stdout.write("TODO: command logic not yet implemented\n") | ||||
| 
 | ||||
| 
 | ||||
| def main(*args): | ||||
|     """ | ||||
|     Primary entry point for the ``wutta`` command. | ||||
|     """ | ||||
|     args = list(args) or sys.argv[1:] | ||||
| 
 | ||||
|     cmd = Command() | ||||
|     cmd.run(*args) | ||||
| from wuttjamaican.cmd.base import * | ||||
|  |  | |||
|  | @ -21,42 +21,13 @@ | |||
| # | ||||
| ################################################################################ | ||||
| """ | ||||
| WuttJamaican - subcommand: make-appdir | ||||
| WuttJamaican - subcommand ``make-appdir`` | ||||
| """ | ||||
| 
 | ||||
| import os | ||||
| import sys | ||||
| import warnings | ||||
| 
 | ||||
| from .base import Subcommand | ||||
| warnings.warn("wuttjamaican.commands package is deprecated; " | ||||
|               "please use wuttjamaican.cmd instead", | ||||
|               DeprecationWarning, stacklevel=2) | ||||
| 
 | ||||
| 
 | ||||
| class MakeAppDir(Subcommand): | ||||
|     """ | ||||
|     Make or refresh the "app dir" for virtual environment | ||||
|     """ | ||||
|     name = 'make-appdir' | ||||
|     description = __doc__.strip() | ||||
| 
 | ||||
|     def add_args(self): | ||||
|         """ """ | ||||
|         self.parser.add_argument('--path', metavar='APPDIR', | ||||
|                                  help="Optional path to desired app dir.  If not specified " | ||||
|                                  "it will be named ``app`` and  placed in the root of the " | ||||
|                                  "virtual environment.") | ||||
| 
 | ||||
|     def run(self, args): | ||||
|         """ | ||||
|         This may be used during setup to establish the :term:`app dir` | ||||
|         for a virtual environment.  This folder will contain config | ||||
|         files, log files etc. used by the app. | ||||
| 
 | ||||
|         Calls :meth:`~wuttjamaican.app.AppHandler.make_appdir()` to do | ||||
|         the heavy lifting. | ||||
|         """ | ||||
|         if args.path: | ||||
|             appdir = os.path.abspath(args.path) | ||||
|         else: | ||||
|             appdir = os.path.join(sys.prefix, 'app') | ||||
| 
 | ||||
|         self.app.make_appdir(appdir) | ||||
|         self.stdout.write(f"established appdir: {appdir}\n") | ||||
| from wuttjamaican.cmd.make_appdir import * | ||||
|  |  | |||
|  | @ -21,22 +21,13 @@ | |||
| # | ||||
| ################################################################################ | ||||
| """ | ||||
| WuttJamaican - setup command | ||||
| WuttJamaican - subcommand ``setup`` | ||||
| """ | ||||
| 
 | ||||
| from .base import Subcommand | ||||
| import warnings | ||||
| 
 | ||||
| warnings.warn("wuttjamaican.commands package is deprecated; " | ||||
|               "please use wuttjamaican.cmd instead", | ||||
|               DeprecationWarning, stacklevel=2) | ||||
| 
 | ||||
| class Setup(Subcommand): | ||||
|     """ | ||||
|     Install and configure various software | ||||
|     """ | ||||
|     name = 'setup' | ||||
|     description = __doc__.strip() | ||||
| 
 | ||||
|     def run(self, args): | ||||
|         """ | ||||
|         TODO: Eventually this will run an interactive setup utility. | ||||
|         But it doesn't yet. | ||||
|         """ | ||||
|         self.stderr.write("TODO: setup is not yet implemented\n") | ||||
| from wuttjamaican.cmd.setup import * | ||||
|  |  | |||
|  | @ -5,11 +5,15 @@ import sys | |||
| from unittest import TestCase | ||||
| from unittest.mock import MagicMock, patch | ||||
| 
 | ||||
| from wuttjamaican.commands import base | ||||
| from wuttjamaican.commands.setup import Setup | ||||
| from wuttjamaican.cmd import base | ||||
| from wuttjamaican.cmd.setup import Setup | ||||
| from wuttjamaican.testing import FileConfigTestCase | ||||
| 
 | ||||
| 
 | ||||
| # nb. do this just for coverage | ||||
| from wuttjamaican.commands.base import Command as legacy | ||||
| 
 | ||||
| 
 | ||||
| class TestCommand(FileConfigTestCase): | ||||
| 
 | ||||
|     def test_base(self): | ||||
|  | @ -20,7 +24,7 @@ class TestCommand(FileConfigTestCase): | |||
|         self.assertEqual(str(cmd), 'wutta') | ||||
| 
 | ||||
|     def test_subcommand_entry_points(self): | ||||
|         with patch('wuttjamaican.commands.base.load_entry_points') as load_entry_points: | ||||
|         with patch('wuttjamaican.cmd.base.load_entry_points') as load_entry_points: | ||||
| 
 | ||||
|             # empty entry points | ||||
|             load_entry_points.side_effect = lambda group: {} | ||||
|  | @ -144,7 +148,7 @@ class TestCommand(FileConfigTestCase): | |||
|                 self.stdout.write("hello world") | ||||
|                 self.stderr.write("error text") | ||||
| 
 | ||||
|         with patch('wuttjamaican.commands.base.sys') as sys: | ||||
|         with patch('wuttjamaican.cmd.base.sys') as sys: | ||||
| 
 | ||||
|             cmd = base.Command(subcommands={'hello': Hello}) | ||||
| 
 | ||||
|  | @ -226,7 +230,7 @@ class TestMain(TestCase): | |||
|         def true_exit(*args): | ||||
|             sys.exit(*args) | ||||
| 
 | ||||
|         with patch('wuttjamaican.commands.base.sys') as mocksys: | ||||
|         with patch('wuttjamaican.cmd.base.sys') as mocksys: | ||||
|             mocksys.argv = ['wutta', '--help'] | ||||
|             mocksys.exit = true_exit | ||||
| 
 | ||||
|  | @ -7,7 +7,11 @@ from unittest import TestCase | |||
| from unittest.mock import patch | ||||
| 
 | ||||
| from wuttjamaican.conf import WuttaConfig | ||||
| from wuttjamaican.commands import Command, make_appdir | ||||
| from wuttjamaican.cmd import Command, make_appdir | ||||
| 
 | ||||
| 
 | ||||
| # nb. do this just for coverage | ||||
| from wuttjamaican.commands.make_appdir import MakeAppDir as legacy | ||||
| 
 | ||||
| 
 | ||||
| class TestMakeAppDir(TestCase): | ||||
|  | @ -38,7 +42,7 @@ class TestMakeAppDir(TestCase): | |||
|         shutil.rmtree(tempdir) | ||||
| 
 | ||||
|         # mock out sys.prefix to get coverage | ||||
|         with patch('wuttjamaican.commands.make_appdir.sys') as sys: | ||||
|         with patch('wuttjamaican.cmd.make_appdir.sys') as sys: | ||||
|             tempdir = tempfile.mkdtemp() | ||||
|             appdir = os.path.join(tempdir, 'app') | ||||
|             sys.prefix = tempdir | ||||
|  | @ -2,7 +2,11 @@ | |||
| 
 | ||||
| from unittest import TestCase | ||||
| 
 | ||||
| from wuttjamaican.commands import setup, Command | ||||
| from wuttjamaican.cmd import Command, setup | ||||
| 
 | ||||
| 
 | ||||
| # nb. do this just for coverage | ||||
| from wuttjamaican.commands.setup import Setup as legacy | ||||
| 
 | ||||
| 
 | ||||
| class TestSetup(TestCase): | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Lance Edgar
						Lance Edgar