py-topdir.mk: Add test tree to default check roots #96

Merged
Jan Lindemann merged 3 commits from jan/feature/20260913-py-topdir-mk-add-test-tree-to-default-check-roots into master 2026-09-13 16:26:59 +02:00 AGit
6 changed files with 65 additions and 35 deletions

View file

@ -9,6 +9,9 @@ MYPY_PATH_DIRECTIVE = mypy_path = "$(MYPY_CONFIG_PATH)"
ifndef PY_CHECK_ROOTS ifndef PY_CHECK_ROOTS
PY_CHECK_ROOTS = $(wildcard $(TOPDIR)/src $(TOPDIR)/tools) PY_CHECK_ROOTS = $(wildcard $(TOPDIR)/src $(TOPDIR)/tools)
ifneq ($(wildcard dirs-all.done),)
PY_CHECK_ROOTS += $(wildcard $(TOPDIR)/test)
endif
endif endif
all: all:

View file

@ -35,17 +35,25 @@ module_path()
cmd_create_conftest_py() cmd_create_conftest_py()
{ {
cat <<-'EOT' cat <<-'EOT'
from __future__ import annotations
import os import os
import sys
import shutil import shutil
import sys
def pytest_configure(config): from typing import TYPE_CHECKING
import _pytest.terminal
import _pytest.timing as timing
# orig = _pytest.terminal.TerminalReporter.pytest_sessionstart from _pytest.terminal import TerminalReporter
from _pytest.timing import Instant
def patched(self, session): if TYPE_CHECKING:
from _pytest.config import Config
from _pytest.main import Session
def pytest_configure(config: Config) -> None:
"""Print a banner centered in the terminal when the session starts."""
def patched(self: TerminalReporter, session: Session) -> None:
width = shutil.get_terminal_size().columns width = shutil.get_terminal_size().columns
d = os.path.basename(os.getcwd()) d = os.path.basename(os.getcwd())
text = f"Running pytest for {d}" text = f"Running pytest for {d}"
@ -59,9 +67,9 @@ cmd_create_conftest_py()
banner = text banner = text
print(banner, file = sys.stderr) print(banner, file = sys.stderr)
self._session = session self._session = session
self._session_start = timing.Instant() self._session_start = Instant()
_pytest.terminal.TerminalReporter.pytest_sessionstart = patched TerminalReporter.pytest_sessionstart = patched # type: ignore[method-assign]
EOT EOT
} }

View file

@ -1,10 +1,16 @@
import asyncio from __future__ import annotations
import asyncio
import sys import sys
from typing import TYPE_CHECKING, override
from jw.pkg.lib.App import App from jw.pkg.lib.App import App
from jw.pkg.lib.Cmd import Cmd 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 # -- 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: # 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 # it adds options, sets up, dispatches to the selected child (or runs its own
@ -12,24 +18,26 @@ from jw.pkg.lib.Cmd import Cmd
class ChildCmd(Cmd): class ChildCmd(Cmd):
def __init__(self, parent): def __init__(self, parent: App | Cmd) -> None:
super().__init__(parent, 'child', 'A child command') super().__init__(parent, 'child', 'A child command')
self.ran = False self.ran = False
self.got_opt = None self.got_opt = None
def add_arguments(self, parser): @override
def add_arguments(self, parser: ArgumentParser) -> None:
super().add_arguments(parser) super().add_arguments(parser)
parser.add_argument('--child-opt', default = 'child-default') parser.add_argument('--child-opt', default = 'child-default')
async def _run(self, args): @override
async def _run(self, args: Namespace) -> None:
self.ran = True self.ran = True
self.got_opt = args.child_opt self.got_opt = args.child_opt
class RootCmd(Cmd): class RootCmd(Cmd):
made = [] made: list['RootCmd'] = []
def __init__(self, parent): def __init__(self, parent: App | Cmd) -> None:
super().__init__(parent, 'root', 'Root command') super().__init__(parent, 'root', 'Root command')
self.child = ChildCmd(self) self.child = ChildCmd(self)
self.add_subcommands(self.child) self.add_subcommands(self.child)
@ -38,11 +46,13 @@ class RootCmd(Cmd):
self.default = False self.default = False
RootCmd.made.append(self) RootCmd.made.append(self)
def add_arguments(self, parser): @override
def add_arguments(self, parser: ArgumentParser) -> None:
super().add_arguments(parser) super().add_arguments(parser)
parser.add_argument('--root-opt', default = 'root-default') parser.add_argument('--root-opt', default = 'root-default')
async def _run(self, args): @override
async def _run(self, args: Namespace) -> None:
self.setup = True self.setup = True
try: try:
if hasattr(args, 'func'): if hasattr(args, 'func'):

View file

@ -1,18 +1,27 @@
import asyncio from __future__ import annotations
import asyncio
import sys import sys
from typing import TYPE_CHECKING, override
from jw.pkg.lib.Cmd import Cmd from jw.pkg.lib.Cmd import Cmd
from jw.pkg.lib.ExecApp import ExecApp from jw.pkg.lib.ExecApp import ExecApp
if TYPE_CHECKING:
from argparse import Namespace
from jw.pkg.lib.App import App
# -- A minimal root command so the app can be built without a command tree. # -- A minimal root command so the app can be built without a command tree.
class RootCmd(Cmd): class RootCmd(Cmd):
def __init__(self, parent): def __init__(self, parent: App | Cmd) -> None:
super().__init__(parent, 'root', 'Root command') super().__init__(parent, 'root', 'Root command')
async def _run(self, args): @override
async def _run(self, args: Namespace) -> None:
pass pass
# -- Counts the close() calls made by App.__aexit__() when the async context # -- Counts the close() calls made by App.__aexit__() when the async context
@ -22,7 +31,8 @@ class RecordingExecApp(ExecApp):
closed = 0 closed = 0
def close(self): @override
def close(self) -> None:
RecordingExecApp.closed += 1 RecordingExecApp.closed += 1
super().close() super().close()
@ -38,16 +48,14 @@ finally:
# -- ExecApp registers the exec-related options on the top-level parser. # -- ExecApp registers the exec-related options on the top-level parser.
opts = [ opts = [o for a in app.parser._actions for o in getattr(a, 'option_strings', ())]
o for a in app.parser._actions for o in getattr(a, 'option_strings', ())
]
for opt in ('--interactive', '--verbose', '--target'): for opt in ('--interactive', '--verbose', '--target'):
assert opt in opts, f'{opt} must be registered by ExecApp' assert opt in opts, f'{opt} must be registered by ExecApp'
# -- Exiting the async context must close the app, through the # -- Exiting the async context must close the app, through the
# App.__aexit__() that ExecApp.__aexit__() chains to. # App.__aexit__() that ExecApp.__aexit__() chains to.
async def exit_context(): async def exit_context() -> None:
async with app: async with app:
pass pass

View file

@ -1,5 +1,6 @@
import os import os
import tempfile import tempfile
from jw.pkg.lib.ProjectConf import ProjectConf from jw.pkg.lib.ProjectConf import ProjectConf
# -- Helper: create temp file and read it -- # -- Helper: create temp file and read it --
@ -13,7 +14,7 @@ def _load(text: str) -> ProjectConf:
_tmpfiles.append(f.name) _tmpfiles.append(f.name)
return ProjectConf.read(f.name) return ProjectConf.read(f.name)
def _cleanup(): def _cleanup() -> None:
for _p in _tmpfiles: for _p in _tmpfiles:
os.unlink(_p) os.unlink(_p)
del _p del _p