jw-pkg/test/unit/python/jw/pkg/lib/App/test.py
Jan Lindemann 64f1a73650
test: Fix mypy strict errors in unit tests
The unit test files below test/unit/python/jw/pkg/lib/ define Cmd and App
subclasses with unannotated methods: __init__(), add_arguments(), _run()
and close() lack signatures, the made class attribute lacks a type, and the
_cleanup() and exit_context() helpers are untyped, so a strict mypy run
over the test tree fails on them. Likewise, the ExecApp and version tests
carry list formatting that yapf rejects.

Annotate the test classes following the library conventions: parent is App
| Cmd, the parser is ArgumentParser, and args is Namespace, and mark every
overridden method with @override. Guard the imports that are only used for
annotations behind TYPE_CHECKING, and add the future annotations import so
they stay out of the runtime path. Then reformat the test tree with yapf.
That folds the opts list of the ExecApp test and re-indents the rejected
operator list of the version test.

Signed-off-by: Jan Lindemann <jan@janware.com>
Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.85.1
2026-09-13 15:42:29 +02:00

113 lines
3.8 KiB
Python

from __future__ import annotations
import asyncio
import sys
from typing import TYPE_CHECKING, override
from jw.pkg.lib.App import App
from jw.pkg.lib.Cmd import Cmd
if TYPE_CHECKING:
from argparse import ArgumentParser, Namespace
# -- 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: App | Cmd) -> None:
super().__init__(parent, 'child', 'A child command')
self.ran = False
self.got_opt = None
@override
def add_arguments(self, parser: ArgumentParser) -> None:
super().add_arguments(parser)
parser.add_argument('--child-opt', default = 'child-default')
@override
async def _run(self, args: Namespace) -> None:
self.ran = True
self.got_opt = args.child_opt
class RootCmd(Cmd):
made: list['RootCmd'] = []
def __init__(self, parent: App | Cmd) -> None:
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)
@override
def add_arguments(self, parser: ArgumentParser) -> None:
super().add_arguments(parser)
parser.add_argument('--root-opt', default = 'root-default')
@override
async def _run(self, args: Namespace) -> None:
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')