jw-pkg/test/unit/python/jw/pkg/lib/App/test.py
Jan Lindemann 6b4bcdfaf8
All checks were successful
CI / Packaging - Kali Linux (pull_request) Successful in 4m23s
CI / Packaging - OpenSUSE Tumbleweed (pull_request) Successful in 4m28s
CI / Packaging test (pull_request) Successful in 0s
CI / Packaging - Kali Linux (push) Successful in 3m58s
CI / Packaging - OpenSUSE Tumbleweed (push) Successful in 4m16s
CI / Packaging test (push) Successful in 0s
lib.App: Add a root command slot
The application's top-level behavior is defined by overriding
App._add_arguments() and App._run(). The lightweight run-and-options
unit, Cmd, can already be mounted at any node of the command tree, but
the root is reserved for the application itself. An application that
wants to host a plain command at the top level therefore has to
subclass App and carry its full lifecycle implementation.

Add a root parameter to App.__init__(). When it is given a command
class, App instantiates it and uses it as the top level: the command's
options are registered on the top-level parser, it becomes the parent
of the top-level subcommands, and App._run() delegates the run to it.
The command's children are wired as the top-level subcommands, so the
same Cmd can now occupy the root node. When root is not given, the
previous auto-discovery behavior is preserved unchanged.

Keep the top-level subcommand heading as plain "Available subcommands"
whether it is hosted by the application or by a root command, while
nested command levels continue to qualify the heading with the parent
name. Add a unit test that mounts a root command hosting a child and
checks option registration, dispatch, setup and teardown, and
resolution of the application through the parent chain.

Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.84.2
Signed-off-by: Jan Lindemann <jan@janware.com>
2026-08-17 12:53:05 +02:00

103 lines
3.5 KiB
Python

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')