import asyncio import sys from jw.pkg.lib.App import App from jw.pkg.lib.Cmd import Cmd # -- A minimal command tree: a root command hosting one child command. The # root mimics the "run + opts" unit the App can now mount at the top level: # it adds options, sets up, dispatches to the selected child (or runs its own # default), and tears down. class ChildCmd(Cmd): def __init__(self, parent): super().__init__(parent, 'child', 'A child command') self.ran = False self.got_opt = None def add_arguments(self, parser): super().add_arguments(parser) parser.add_argument('--child-opt', default = 'child-default') async def _run(self, args): self.ran = True self.got_opt = args.child_opt class RootCmd(Cmd): made = [] def __init__(self, parent): super().__init__(parent, 'root', 'Root command') self.child = ChildCmd(self) self.add_subcommands(self.child) self.setup = False self.teardown = False self.default = False RootCmd.made.append(self) def add_arguments(self, parser): super().add_arguments(parser) parser.add_argument('--root-opt', default = 'root-default') async def _run(self, args): self.setup = True try: if hasattr(args, 'func'): await args.func(args) else: self.default = True finally: self.teardown = True # -- App.__init__ builds the parser from sys.argv, so point it at a clean # command line while constructing the app. saved_argv = sys.argv sys.argv = ['jw-pkg-test'] try: app = App(description = 'Root slot test', root = RootCmd) finally: sys.argv = saved_argv root = RootCmd.made[0] child = root.child # -- Parent chain: the child is parented to the root, the root to the app, # and both resolve their application through the chain. assert child.parent is root, 'child.parent should be the root' assert root.parent is app, 'root.parent should be the app' assert child.app is app, 'child.app should resolve to the app' assert root.app is app, 'root.app should resolve to the app' # -- The root's options are registered on the top-level parser, and the child # is wired as a top-level subcommand. args = app.parser.parse_args(['--root-opt', 'RV', 'child', '--child-opt', 'CV']) assert args.root_opt == 'RV', args assert args.child_opt == 'CV', args assert hasattr(args, 'func'), 'selecting a subcommand must set the dispatch target' # -- Dispatching with a selected child runs the child, wrapped by the root's # setup and teardown. asyncio.run(app._run(args)) assert child.ran is True, 'the selected child must run' assert child.got_opt == 'CV', child.got_opt assert root.setup is True, 'root setup must run' assert root.teardown is True, 'root teardown must run' assert root.default is False, 'default must not run when a child is selected' # -- Dispatching with no subcommand runs the root's default, still wrapped by # setup and teardown, and does not run the child. root.setup = root.teardown = root.default = False child.ran = False args = app.parser.parse_args(['--root-opt', 'RV2']) assert not hasattr(args, 'func'), 'no subcommand must not set a dispatch target' asyncio.run(app._run(args)) assert root.setup is True, 'root setup must run' assert root.teardown is True, 'root teardown must run' assert root.default is True, 'root default must run when no child is selected' assert child.ran is False, 'the child must not run when no child is selected' print('All App root-slot tests passed')