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
PY_CHECK_ROOTS = $(wildcard $(TOPDIR)/src $(TOPDIR)/tools)
ifneq ($(wildcard dirs-all.done),)
PY_CHECK_ROOTS += $(wildcard $(TOPDIR)/test)
endif
endif
all:

View file

@ -35,17 +35,25 @@ module_path()
cmd_create_conftest_py()
{
cat <<-'EOT'
from __future__ import annotations
import os
import sys
import shutil
import sys
def pytest_configure(config):
import _pytest.terminal
import _pytest.timing as timing
from typing import TYPE_CHECKING
# 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
d = os.path.basename(os.getcwd())
text = f"Running pytest for {d}"
@ -59,9 +67,9 @@ cmd_create_conftest_py()
banner = text
print(banner, file = sys.stderr)
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
}

View file

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

View file

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

View file

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