Package: argcmdr
The thinargparsewrapper for quick, clear and easy declaration of (hierarchical) console command interfaces via Python.argcmdr:handles the boilerplate of CLIwhile maintaining the clarity and extensibility of your codewithout requiring you to learn Yet Another argument-definition syntaxwithout reinventing the wheel or sacrificing the flexibility ofargparseenables invocation viaexecutable script (__name__ == '__main__')setuptoolsentrypointcommand-defining module (like theMakefileofmake)determines command hierarchy flexibly and cleanlycommand declarations are nested to indicate CLI hierarchyorcommands are decorated to indicate their hierarchyincludes support for elegant interaction with the operating system, viaplumbumSetupargcmdris developed for Python version 3.6.3 and above, and may be built viasetuptools.PythonIf Python 3.6.3 or greater is not installed on your system, it is available frompython.org.However, depending on your system, you might prefer to install Python via a package manager, such asHomebrewon Mac OS X or APT on Debian-based Linux systems.Alternatively,pyenvis highly recommended to manage arbitrary installations of Python, and may be most easily installed via thepyenv installer.argcmdrTo install from PyPI:pip install argcmdrTo install from Github:pip install git+https://github.com/dssg/argcmdr.gitTo install from source:python setup.py installTutorialThe CommandThe command decoratorLocal executionManaging executionHandling exit codesInspecting resultsHandling exceptionsModifying executionThe local decoratorCommand invocation signatureAccess to the parsed argument namespaceAccess to the parserCommand hierarchyNested commandsRoot commandsMethod commandsWalking the hierachyCommand delegationThe call() methodThe management fileEntrypoint definitionThe management packageBootstrappingShell completionThe Commandargcmdris built around the base classCommand. Your console command extendsCommand, and optionally defines:__init__(parser), which adds to the parser the arguments that your command requires, as supported byargparse(seeargparse)__call__([args,parser,...]), which is invoked when your console command is invoked, and which is expected to implement your command’s functionalityFor example, let’s define the executable filelistdir, a trivial script which prints the current directory’s contents:#!/usr/bin/env python

import os

from argcmdr import Command, main

class Main(Command):
    """print the current directory's contents"""

    def __call__(self):
        print(*os.listdir())

if __name__ == '__main__':
    main(Main)Should we execute this script, it will perform much the same asls-A.Let’s say, however, that we would like to optionally print each item of the directory’s contents on a separate line:class Main(Command):
    """print the current directory's contents"""

    def __init__(self, parser):
        parser.add_argument(
            '-1',
            action='store_const',
            const='\n',
            default=' ',
            dest='sep',
            help='list one file per line',
        )

    def __call__(self, args):
        print(*os.listdir(), sep=args.sep)We now optionally support execution similar tols-A1, vialistdir-1.Fittingly, this is reflected in the script’s autogenerated usage text –listdir-hprints:usage: listdir [-h] [--tb] [-1]

print the current directory's contents

optional arguments:
  -h, --help         show this help message and exit
  --tb, --traceback  print error tracebacks
  -1                 list one file per lineThe command decoratorFor particularly trivial commands, the class declaration syntax may be considered verbose and unnecessary. The@cmddecorator manufactures the appropriateCommandfrom a decorated function or method.The first command may be rewritten to produce an identical result:from argcmdr import cmd

@cmd
def main():
    """print the current directory's contents"""
    print(*os.listdir())and, for the second,cmdoptionally accepts anargparseargument definition:@cmd('-1', action='store_const', const='\n', default=' ', dest='sep', help='list one file per line')
def main(args):
    """print the current directory's contents"""
    print(*os.listdir(), sep=args.sep)Further arguments may be added via additional decoration:@cmd('-a', ...)
@cmd('-1', ...)
def main(args):
    ...Local executionAs much as we gain from Python and its standard library, it’s quite typical to need to spawn non-Python subprocesses, and for that matter for your script’s purpose to be entirely to orchestrate workflows built from operating system commands. Python’s – and argcmdr’s – benefit is to make this work easier, debuggable, testable and scalable.In fact, our above, trivial example could be accomplished easily with direct execution ofls:import argparse

from argcmdr import Local, main

class Main(Local):
    """list directory contents"""

    def __init__(self, parser):
        parser.add_argument(
            'remainder',
            metavar='arguments for ls',
            nargs=argparse.REMAINDER,
        )

    def __call__(self, args):
        print(self.local['ls'](args.remainder))local, bound to theLocalbase class, is a dictionary which caches path look-ups for system executables.This could, however, still be cleaner. For this reason, theLocalcommand features a parallel invocation interface,prepare([args,parser, local,...]):class Main(Local):
    """list directory contents"""

    def __init__(self, parser):
        parser.add_argument(
            'remainder',
            metavar='arguments for ls',
            nargs=argparse.REMAINDER,
        )

    def prepare(self, args):
        return self.local['ls'][args.remainder]Via theprepareinterface, standard output is printed by default, and your command logic may be tested in a “dry run,” as reflected in the usage output of the above:usage: listdir [-h] [--tb] [-q] [-d] [-s] [--no-show] ...

list directory contents

positional arguments:
  arguments for ls

optional arguments:
  -h, --help         show this help message and exit
  --tb, --traceback  print error tracebacks
  -q, --quiet        do not print command output
  -d, --dry-run      do not execute commands, but print what they are (unless
                     --no-show is provided)
  -s, --show         print command expressions (by default not printed unless
                     dry-run)
  --no-show          do not print command expressions (by default not printed
                     unless dry-run)To execute multiple local subprocesses,preparemay either return an iterable (e.g.list) of the aboveplumbumbound commands, orpreparemay be defined as a generator function, (i.e.make repeated use ofyield– see below).Managing executionHandling exit codesSubprocess commands emitted byLocal.prepareare executed in order and, by default, failed execution is interrupted by a raised exception:class Release(Local):
    """release the package to pypi"""

    def __init__(self, parser):
        parser.add_argument(
            'part',
            choices=('major', 'minor', 'patch'),
            help="part of the version to be bumped",
        )

    def prepare(self, args):
        yield self.local['bumpversion'][args.part]
        yield self.local['python']['setup.py', 'sdist', 'bdist_wheel']
        yield self.local['twine']['upload', 'dist/*']Should thebumpversioncommand fail, thedeploycommand will not proceed.In some cases, however, we might like to disable this functionality, and proceed regardless of a subprocess’s exit code. We may pass arguments such asretcodetoplumbumby setting this attribute on thepreparemethod:def prepare(self, args):
    yield self.local['bumpversion'][args.part]
    yield self.local['python']['setup.py', 'sdist', 'bdist_wheel']
    yield self.local['twine']['upload', 'dist/*']

prepare.retcode = NoneSubprocess commands emitted by the above method will not raise execution exceptions, regardless of their exit code. (To allow only certain exit code(s), setretcodeas appropriate – seeplumbum.)Inspecting resultsHaving disabled execution exceptions – and regardless – we might need to inspect a subprocess command’s exit code, standard output or standard error. As such, (whether we manipulateretcodeor not),argcmdrcommunicates these command results withpreparegenerator methods:def prepare(self, args):
    (code, out, err) = yield self.local['bumpversion']['--list', args.part]

    yield self.local['python']['setup.py', 'sdist', 'bdist_wheel']

    if out is None:
        version = 'DRY-RUN'
    else:
        (version_match,) = re.finditer(
            r'^new_version=([\d.]+)$',
            out,
            re.M,
        )
        version = version_match.group(1)

    yield self.local['twine']['upload', f'dist/*{version}*']In the above,preparestores the results ofbumpversionexecution, in order to determine from its standard output the version to be released.Handling exceptionsMoreover, we might like to define special handling for execution errors; and, perhaps rather than manipulateretcodefor all commands emitted by our method, we might like to handle them separately. As such, execution exceptions are also communicated back topreparegenerators:def prepare(self, args):
    try:
        (_code, out, _err) = yield self.local['bumpversion']['--list', args.part]
    except self.local.ProcessExecutionError:
        print("execution failed but here's a joke ...")
        ...Modifying executionCommands are run in the foreground by default, their outputs printed, as well as recorded for inspection, via theplumbummodifier,TEE.To execute a command in the background (and continue), we may specify theBGmodifier:def prepare(self, args):
    future = yield (self.local.BG, self.local['bumpversion']['--list', args.part])Alternatively, we may wish to execute a command in the foregroundonly, (and not record its output) –e.g.to best support processes which require TTY:def prepare(self):
    return (self.local.FG, self.local['ipython']['-i', 'startup.py'])The local decoratorLocalis an alternate command base class, and a subclass ofCommand. Any base class may be substituted forCommandwhen using the command decorator:@cmd(base=CustomCommand)
def main():
    ...Moreover,Localfunctionality may be requested via keyword flaglocal:@cmd(local=True)
def main(self):
    ...And in support of the above, common case, the@localdecorator is provided:from argcmdr import local

@local
def main(self):
    ...Note that in the last two examples, our command function’s call signature includedself.Decorated command functions are in fact replaced with manufactured subclasses ofCommand, and the function is invoked as this command’s functionality – either__call__orprepare. It is assumed that, by default, this function should be treated as astaticmethod, and given no reference to the manufacturedCommandinstance. However, in the case oflocaldecoration, this is not the case; the binding is left up to the decorated object, which, according to Python descriptor rules, means that a decorated function is treated as a “method” and receives the instance. This way,localcommand functions may access the instance’slocaldictionary of operating system executables.Binding may be explicitly controlled via the decorator keywordbinding,e.g.:@cmd(binding=True, base=CustomCommand)
def main(self):
    ...SeeMethod commandsfor further examples of decorator-defined commands and alternative bindings.Command invocation signatureNote that in our last trivial examples of listing directory contents, we made our script dependent upon thelscommand in the operating environment.argcmdrwill not, by default, print tracebacks, and it will colorize unhandled exceptions; however, we might prefer to print a far friendlier error message.One easy way of printing friendly error messages is to make use ofargparse.ArgumentParser.error(). As we’ve seen,Commandinvocation, via either__call__orprepare, may accept zero arguments, or it may require the parsed argumentsargparse.Namespace. Moreover, it may require a second argument to receive the argument parser, and a third argument to receive thelocaldictionary:class Main(Local):
    """list directory contents"""

    def __init__(self, parser):
        parser.add_argument(
            'remainder',
            metavar='arguments for ls',
            nargs=argparse.REMAINDER,
        )

    def prepare(self, args, parser, local):
        try:
            local_exec = local['ls']
        except local.CommandNotFound:
            parser.error('command not available')

        yield local_exec[args.remainder]Iflsis not available, the user is presented the following message upon executing the above:usage: listdir [-h] [--tb] [-q] [-d] [-s] [--no-show] ...
listdir: error: command not availableAccess to the parsed argument namespaceThe command invocation’s parsed arguments are most straight-forwardly accessible as the first argument of theCommandinvocation signature, either__call__orprepare. However, in less-than-trivial implementations, wherein command methods are factored for reusability, passing the argument namespace from method to method may become tedious. To support such scenarios, this object is made additionally available via theCommandproperty,args.Consider a class of commands which require a database password. We don’t want to store this password anywhere in plain text; rather, we expect it to be input, either via (piped) standard input or the TTY:class DbSync(Command):
    """sync databases"""

    def __init__(self, parser):
        parser.add_argument(
            '-p', '--password',
            action='store_true',
            dest='stdin_password',
            default=False,
            help="read database password from standard input",
        )

    def __call__(self, args):
        engine = self.dbengine(args)
        ...

    def dbcreds(self, args):
        dbcreds = {
            'username': os.getenv('PGUSER'),
            'host': os.getenv('PGHOST'),
            'port': os.getenv('PGPORT'),
            'database': os.getenv('PGDATABASE'),
        }

        missing = [key for (key, value) in dbcreds.items() if not value]
        if missing:
            raise RuntimeError(
                "database connection information missing from "
                "environmental configuration: " + ', '.join(missing)
            )

        if args.stdin_password:
            dbcreds['password'] = sys.stdin.read().rstrip('\n\r')

            # we're done with the (pipe) stdin, so force it back to TTY for
            # any subsequent input()
            sys.stdin = open('/dev/tty')
        else:
            dbcreds['password'] = os.getenv('PGPASSWORD')
            if not dbcreds['password']:
                dbcreds['password'] = getpass.getpass(
                    'enter password for '
                    + ('{username}@{host}:{port}'.format_map(dbcreds) | colors.bold)
                    + ': '
                    | colors.yellow
                )

        return dbcreds

    def dburi(self, args):
        return sqlalchemy.engine.url.URL('postgres', **self.dbcreds(args))

    def dbengine(self, args):
        return sqlalchemy.create_engine(self.dburi(args))Not only were we forced to verbosely daisy-chain the arguments namespace,args, from method to method; moreover, we were prevented from (trivially) caching the result ofdbcreds, to ensure that the password isn’t ever requested more than once.Now, let’s reimplement the above, making use of the propertyargs:class DbSync(Command):
    """sync databases"""

    def __init__(self, parser):
        parser.add_argument(
            '-p', '--password',
            action='store_true',
            dest='stdin_password',
            default=False,
            help="read database password from standard input",
        )

    def __call__(self):
        engine = self.dbengine
        ...

    @cachedproperty
    def dbcreds(self):
        dbcreds = {
            'username': os.getenv('PGUSER'),
            'host': os.getenv('PGHOST'),
            'port': os.getenv('PGPORT'),
            'database': os.getenv('PGDATABASE'),
        }

        missing = [key for (key, value) in dbcreds.items() if not value]
        if missing:
            raise RuntimeError(
                "database connection information missing from "
                "environmental configuration: " + ', '.join(missing)
            )

        if self.args.stdin_password:
            dbcreds['password'] = sys.stdin.read().rstrip('\n\r')

            # we're done with the (pipe) stdin, so force it back to TTY for
            # any subsequent input()
            sys.stdin = open('/dev/tty')
        else:
            dbcreds['password'] = os.getenv('PGPASSWORD')
            if not dbcreds['password']:
                dbcreds['password'] = getpass.getpass(
                    'enter password for '
                    + ('{username}@{host}:{port}'.format_map(dbcreds) | colors.bold)
                    + ': '
                    | colors.yellow
                )

        return dbcreds

    @property
    def dburi(self):
        return sqlalchemy.engine.url.URL('postgres', **self.dbcreds)

    @property
    def dbengine(self):
        return sqlalchemy.create_engine(self.dburi)In this form,argsneedn’t be passed from method to method; in fact, methods of theDbSynccommand needn’t worry about arguments which don’t directly interest them at all. And, usingcachedpropertyfromDickens, the database credentials are trivially cached, ensuring they aren’t needlessly re-requested.Note that attempting to access theargsproperty before invocation arguments have been parsed –e.g.within__init__– is not allowed, and will raiseRuntimeError.Access to the parserIn addition toargs, theparserassociated with the command may alternatively be retrieved via itsparserproperty.Similar toargs, theparseris not available until the command has been initialized; however, this propertymaybe usedwithin__init__, so long as the base__init__has been invoked (e.g.viasuper().__init__).Command hierarchyOur tools should be modular and composable, favoring atomicity over monolithism. Nevertheless, well-designed, -structured and -annotated code and application interfaces pay their users and developers tremendous dividends over time – no less in the case of more extensive interfaces, and particularly so for project management libraries (consider theMakefile).argcmdrintends to facilitate the definition ofargparse-based interfaces regardless of their structure. But it’s in multi-level, or hierarchical, command argumentation thatargcmdrshines.Nested commandsRather than procedurally defining subparsers,Commandclass declarations may simply be nested.Let’s define an executable filemanagefor managing a codebase:#!/usr/bin/env python

import os

from argcmdr import Local, main

class Management(Local):
    """manage deployment"""

    def __init__(self, parser):
        parser.add_argument(
            '-e', '--env',
            choices=('development', 'production'),
            default='development',
            help="target environment",
        )

    class Build(Local):
        """build app"""

        def prepare(self, args):
            req_path = os.path.join('requirements', f'{args.env}.txt')
            yield self.local['pip']['-r', req_path]

    class Deploy(Local):
        """deploy app"""

        def prepare(self, args):
            yield self.local['eb']['deploy', args.env]

if __name__ == '__main__':
    main(Management)LocalcommandManagement, above, defines no functionality of its own. As such, executingmanagewithout arguments prints its autogenerated usage:usage: manage [-h] [--tb] [-q] [-d] [-s] [--no-show]
              [-e {development,production}]
              {build,deploy} ...BecauseManagementextendsLocal, it inherits argumentation controlling whether standard output is printed and offering to run commands in “dry” mode. (Note, however, that it could have omitted these options by extendingCommand. Moreover, it may override class methodbase_parser().)Managementadds to the basic interface the optional argument--env. Most important, however, are the related, nested commandsBuildandDeploy, which define functionality viaprepare. Neither nested command extends its subparser – though they could; but rather, they depend upon the common argumentation defined byManagement.Exploring the interface via--helptells us a great deal, for examplemanage-h:usage: manage [-h] [--tb] [-q] [-d] [-s] [--no-show]
              [-e {development,production}]
              {build,deploy} ...

manage deployment

optional arguments:
  -h, --help            show this help message and exit
  --tb, --traceback     print error tracebacks
  -q, --quiet           do not print command output
  -d, --dry-run         do not execute commands, but print what they are
                        (unless --no-show is provided)
  -s, --show            print command expressions (by default not printed
                        unless dry-run)
  --no-show             do not print command expressions (by default not
                        printed unless dry-run)
  -e {development,production}, --env {development,production}
                        target environment

management commands:
  {build,deploy}        available commands
    build               build app
    deploy              deploy appAndmanage deploy-h:usage: manage deploy [-h]

deploy app

optional arguments:
  -h, --help  show this help message and exitAs such, a “dry run”:manage -de production deployprints the following:> /home/user/.local/bin/eb deploy productionand without the dry-run flag the above operating system command is executed.Root commandsThere is no artificial limit to the number of levels you may add to your command hierarchy. However, application interfaces are commonly “wider” than they are “deep”. For this reason, as an alternative to class-nesting, the hierarchical relationship may be defined by a class decorator provided by theRootCommand.Let’s define the executable filegitwith no particular purpose whatsoever:#!/usr/bin/env python

from argcmdr import Command, RootCommand, main

class Git(RootCommand):
    """another stupid content tracker"""

    def __init__(self, parser):
        parser.add_argument(
            '-C',
            default='.',
            dest='path',
            help="run as if git was started in <path> instead of the current "
                 "working directory.",
        )

@Git.register
class Stash(Command):
    """stash the changes in a dirty working directory away"""

    def __call__(self, args):
        self['save'].delegate()

    class Save(Command):
        """save your local modifications to a new stash"""

        def __init__(self, parser):
            parser.add_argument(
                '-p', '--patch',
                dest='interactive',
                action='store_true',
                default=False,
                help="interactively select hunks from the diff between HEAD "
                     "and the working tree to be stashed",
            )

        def __call__(self, args):
            print("stash save", f"(interactive: {args.interactive})")

    class List(Command):
        """list the stashes that you currently have"""

        def __call__(self):
            print("stash list")

if __name__ == '__main__':
    main(Git)We anticipate adding many subcommands togitbeyondstash; and so, rather than nest all of these command classes underGit:we’ve definedGitas aRootCommandwe’ve declaredStashat the module rootwe’ve decoratedStashwithGit.registerTheRootCommandfunctions identically to theCommand; it only adds this ability to extend the listing of its subcommands by those registered via its decorator. (Notably,LocalRootcomposes the functionaliy ofLocalandRootCommandvia multiple inheritance.)Thestashcommand, on the other hand, has opted to contain the entirety of its hierarchical functionality, nesting its own subcommandslistandsave.Nevertheless, you are not limited to a singleRootCommand. Any command whose hierarchy you would like to extend via theregisterdecorator may inherit it. Moreover, the@cmddecorator accepts the keyword flagroot.Method commandsDecorator-manufactured commands are no less capable than those derived from class declaration syntax,exceptin that other commands cannot, syntactically, be nested beneath them. (For that reason the@cmddecorator’srootflag is of note.) Decorator-manufactured commands can nonetheless themselves extend hierarchies, either by being further decorated byregisteror nested under command class declarations:@Git.register
class Stash(Command):
    """stash the changes in a dirty working directory away"""

    def __call__(self, args):
        self['save'].delegate()

    @cmd('-p', '--patch', dest='interactive', action='store_true', default=False,
         help="interactively select hunks from the diff between HEAD "
              "and the working tree to be stashed")
    def save(args):
        """save your local modifications to a new stash"""
        print("stash save", f"(interactive: {args.interactive})")

    @cmd
    def list():
        """list the stashes that you currently have"""
        print("stash list")Above we’ve rewritten the trivialstashcommandssaveandlistas@cmd-decorated functions.Say, however, that we needed to invert the factoring ofsavelogic between that command and its parent:@Git.register
class Stash(Command):
    """stash the changes in a dirty working directory away"""

    def perform_save(self, interactive=False):
        print("stash save", f"(interactive: {interactive})")

    def __call__(self):
        self.perform_save()

    @cmd('-p', '--patch', dest='interactive', action='store_true', default=False,
         help="interactively select hunks from the diff between HEAD "
              "and the working tree to be stashed")
    @cmd(binding=True)
    def save(self, args):
        """save your local modifications to a new stash"""
        self[-1].perform_save(args.interactive)

    @cmd
    def list():
        """list the stashes that you currently have"""
        print("stash list")(Note thatcmdcan accept both anargparseargument specification and command feature-defining arguments at once; however, this is of use mainly to the definition of helpers such as thelocaldecorator, as this style is difficult to read and otherwise discouraged. Moreover, only thefirst–i.e.inner-most –cmddecorator’s command features are respected.)In this version,savefunctionality is shared as a method ofStash.saveis able to access this method only by ascending the command hierarchy. This might make particular sense when multiple nested commands must share functionality, which is defined on the command class under which they are nested. (Note, however, that in such a case as this one, where the shared methodcouldbe defined as astaticmethod, it is no less advisable to do so, and for nested commands to access it directly as,e.g.Stash.perform_save.)Our above reference toselfinsave, however, is at first glance misleading. This commandlookslike an instance method ofStash; yet, it’s its ownCommand, and thesavefunction receives as its first invocation argument an instance of theCommandclasssave. Moreover, in this case,savegains nothing from this self-reference; its class defines no special attributes or functionality of its own beyond argument-parsing.To improve on the above, we may instead decorate our command function withcmdmethod:@Git.register
class Stash(Command):
    """stash the changes in a dirty working directory away"""

    def perform_save(self, interactive=False):
        print("stash save", f"(interactive: {interactive})")

    def __call__(self):
        self.perform_save()

    @cmdmethod('-p', '--patch', dest='interactive', action='store_true', default=False,
               help="interactively select hunks from the diff between HEAD "
                    "and the working tree to be stashed")
    def save(self, args):
        """save your local modifications to a new stash"""
        self.perform_save(args.interactive)Thecmdmethoddecorator – as well as the complementarylocalmethoddecorator – alter the binding of the decorated function such that it receives the instance of its parent command – not itself – upon invocation. Much cleaner.As with thelocaldecorator,cmdmethodis merely a wrapper ofcmd. Identical functionality can be achieved via thebindingkeyword, though far more verbosely:from argcmdr import CommandDecorator

@cmd(binding=CommandDecorator.Binding.parent)
def save(self, args):
    ...Walking the hierachyUnlike the base commandgitin the example above, the commandgit stash– despite defining its own subcommands – also defines its own functionality, via__call__. This functionality, however, is merely a shortcut to thestashcommandsave. Rather than repeat the definition of this functionality,Stash“walks” its hierarchy to access the instantiation ofSave, and invokes this command by reference.Much ofargcmdris defined at the class level, and as such manyCommandmethods areclassmethod. In the static or class context, we might walk the command hierarchy by reference,e.g.toStash.Save; or, from a class method ofStash, ascls.Save. Moreover,Commanddefines the class-level “property”subcommands, which returns a list ofCommandclasses immediately “under” it in the hierarchy.The hierarchy of executable command objects, however, is instantiated at runtime and cached within theCommandinstance. To facilitate navigation of this hierarchy, theCommandobject is itself subscriptable. Look-up keys may be:strings – descend the hierarchy to the named commandnegative integers – ascend the hierarchy this many levelsa sequence combining the above – to combine “steps” into a single actionIn the above example,Stashmay have (redundantly) accessedSavewith the look-up key:(-1, 'stash', 'save')that is with the full expression:self[-1, 'stash', 'save'](The single key'save', however, was far more to the point.)Because command look-ups are relative to the current command,Commandalso offers thepropertyroot, which returns the base command. As such, our redundant expression could be rewritten:self.root['stash', 'save']Finally, a command instance’s immediate subcommands may be iterated by iteration of the command,e.g.:def __call__(self):
    for subcommand in self:
        subcommand.delegate()Command delegationAs you’ve seen above, command instance subscription enables access to ancestor and descendent commands from the command hierarchy.And, simpleCommandinstances may be executed directly via__call__. However, above, we instead invoked thedelegatemethod. Why?__call__must be invoked as defined – including its argument signature – which may or may not includeargsand/orparser(and which may change during development)Theargsandparserin the scope of the delegating command – (generally the command actually selected by user argumentation) – reflect the arguments defined for that command,notthose of the delegated command.ForLocalcommand instances, the situation, withoutdelegate, is worse:To generate system commands (rather than executing them immediately), we must know to targetpreparerather than__call__For example, above, ourStashcommand might look like the following withoutdelegate:class Stash(Command):
    """stash the changes in a dirty working directory away"""

    def __call__(self, args):
        self['save'](args)

    class Save(Command):
        """save your local modifications to a new stash"""

        def __init__(self, parser):
            parser.add_argument(
                '-p', '--patch',
                dest='interactive',
                action='store_true',
                default=False,
                help="interactively select hunks from the diff between HEAD "
                     "and the working tree to be stashed",
            )

        def __call__(self, args):
            interactive = getattr(args, 'interactive', False)
            print("stash save", f"(interactive: {interactive})")Note, inStash.__call__, the passing through ofargs; and, inStash.Save.__call__, the use ofgetattr. Withdelegate, neither is required.The call() methodYou’llalsofind that there’s the command methodcall(without underscores)!This is a shorcut fordelegate('__call__',…):i.e.it will only delegate to the bound command by invoking its__call__method, (even if it’s aLocalcommand definingprepare).Whereasdelegateis useful for switching between commands via their default invocation methods (either__call__orprepare), and for switching between execution methods of a single command,callis useful for ensuring that the bound command will be executed –i.e.that its__call__method will be invoked – regardless of its type. This is important to argcmdr itself (inargcmdr.main), and useful for command delegation across disparate base classes.The management fileIn addition to the interface of custom executables,argcmdrendeavors to improve the generation and maintainability of non-executable but standardized files, intended for management of code development projects and operations.Similar to a project’sMakefile, we might define our previous codebase-management file as the following Python module,manage.py:import os

from argcmdr import Local, main

class Management(Local):
    """manage deployment"""

    def __init__(self, parser):
        parser.add_argument(
            '-e', '--env',
            choices=('development', 'production'),
            default='development',
            help="target environment",
        )

    class Build(Local):
        """build app"""

        def prepare(self, args):
            req_path = os.path.join('requirements', f'{args.env}.txt')
            yield self.local['pip']['-r', req_path]

    class Deploy(Local):
        """deploy app"""

        def prepare(self, args):
            yield self.local['eb']['deploy', args.env]Unlike our original script,manage,manage.pyis not executable, and need define neither an initial shebang line nor a final__name__ == '__main__'block.Rather,argcmdrsupplies its own, general-purposemanageexecutable command, which loads Commands from anymanage.pyin the current directory, or as specified by option--manage-filePATH. As such, the usage and functionality of ourmanage.py, as invoked via argcmdr’s installedmanagecommand, is identical to our originalmanage. We need only ensure thatargcmdris installed, in order to make use of it to manage any or all project tasks, in a standard way, with even less boilerplate.Entrypoint definitionIn lieu of an explicitly defined execution path,manageinfers the base command – and hence the entrypoint – of themanage.pymanagement file module.The entrypoint of a management file defining – at the module level – only oneCommand, or multiple commands but only oneRootCommand, is assumed to be this one command. Otherwise, the intended entrypoint must be decorated with@entrypoint:from argcmdr import entrypoint, RootCommand

class GoodCommand(RootCommand):

    def good_function(self):
        ...

@entrypoint
class CommandEhh(GoodCommand):

    def __call__(self):
        self.good_function()
        ...

@CommandEhh.register
class CommandBeh(GoodCommand):

    def __call__(self):
        self.good_function()
        ...We may infer from the above thatGoodCommandis merely a base class extension, and that the module’s CLI begins with the most “root” command,CommandEhh, which is extended byCommandBeh. However, rather than go out on a limb, when presented with these three subclasses ofCommandandRootCommand,argcmdrrequires that the intended entrypoint is explicitly marked.Note, however, that only commands declared at the module, or “top” level, are considered potential entrypoints:class CommandEhh(Command):

    class CommandBeh(Command):

        ...Presented with a module containing only the above commands,argcmdrwould identifyCommandEhhas the entrypoint;CommandBehwould never be considered, even if decorated@entrypoint.The management packagePythonpackages, no less than stand-alone modules, may also be defined for use with themanagecommand, to aid in maintenance and development.Consider the following example directory layout:manage/
├── __init__.py
├── cloud.py
├── db.py
├── main.py
├── morale.py
├── server.py
└── util.pyargcmdrwill load the above top-level Python module,manage, just as well as it would themanagemodule defined by amanage.pyfile, (whether these are available on thePYTHONPATHor not).Furthermore, detecting thatthismanageis in fact a package,argcmdrwillautomaticallyandrecursivelyload all of the modules this package contains.This allows the developer to provideargcmdrthe minimum that it requires atmanage/__init__.py– access to an interface entrypoint,i.e.the baseCommand– and to organize the development of that interface in whatever maintainable way suits them.To wit, the developer simply might write, inmanage/__init__.py:from .main import Main  # noqa(…And they will have no need of the@entrypointdecorator, asargcmdrwill only see the one top-level command.)Of course, that top-level command might have been defined in__init__.py, or as you might prefer, inmanage/main.py:from argcmdr import RootCommand

class Main(RootCommand):
    """your one-stop shop for devops"""

    ...And, each subcommand may be defined in a submodule, such asmanage/cloud.py:from argcmdr import Command

from .main import Main

@Main.register
class Cloud(Command):
    """manage cloud computing resources"""

    ...Thanks to automatic loading, theCloudsubcommand, (which will resolve tomanage cloud), will be picked up, without additional boilerplate and without needing to consider circular imports.To disable automatic submodule loading, set the following inmanage/__init__.py:__auto_init_package__ = FalseAnd to make (custom) use of this feature, see:argcmdr.init_package().BootstrappingTo ensure that such a friendly – andrelativelyhigh-level – project requirement asargcmdris satisfied, consider the expressly low-level utilityinstall-cli, with which to guide contributors through the process of provisioning your project’s most basic requirements.Shell completionargcmdrsupports shell command argument completion viaargcomplete(seeargcomplete).As explained by its documentation, your user (perhaps in executing the installation of your command), may enable argument completion, either:specifically for your shell commandor generally for any script containing the stringPYTHON_ARGCOMPLETE_OKin its first 1024 bytesFor flexibility, (and,e.g., in support of installation into virtual environments, or otherwise where system- or user-global installation is undesirable or impossible),argcmdrdoes notcurrently insist on a particular scheme to enable argument completion.Rather, for example, to enable argument completion system-wide, specifically for themanagecommand (provisioned byargcmdr), you might execute the following from a Bash shell (as the root user):register-python-argcomplete --shell bash manage > /usr/share/bash-completion/completions/python-argcomplete-manage.sh(or depending upon your system):register-python-argcomplete --shell bash manage > /etc/bash_completion.d/python-argcomplete-manage.shAlternatively, the same argument completion may be enabled, but only for the current user:mkdir -p ~/.local/share/bash-completion/completions/
register-python-argcomplete --shell bash manage > ~/.local/share/bash-completion/completions/python-argcomplete-manage.sh(or as preferred):mkdir -p ~/.bash_completion.d
register-python-argcomplete --shell bash manage > ~/.bash_completion.d/python-argcomplete-manage.shOnly in the latter case, the user may also required the file~/.bash_completion, including contents of the following form:if [ -d ~/.bash_completion.d/ ] && [ ! -z "$(ls ~/.bash_completion.d/)" ]; then
  for bcfile in ~/.bash_completion.d/*; do
    . "$bcfile"
  done
fi(such that Bash will load the completion file automatically).In the case that neither system-wide nor user-only installation is appropriate, the same argument completion may be enabled, but only for the current shell:eval "$(register-python-argcomplete --shell bash manage)"Regardless of the method, having so enabled argument completion (for your command), in your shell,argcmdrwill handle the rest, generating completion suggestions based on your command definition.