From 7d8994bcfe58ed9f541cbe57145b5204053f92f5 Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Wed, 1 Jul 2026 16:03:02 +0200 Subject: [PATCH 1/8] lib.CopyContext._run(): Fix infinite recursion The CopyContext._run() calls await self._run(), which would create infinite recursion raising RecursionError at runtime. Make it a pure virtual. Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL with pi.dev v0.80.2 Signed-off-by: Jan Lindemann --- src/python/jw/pkg/lib/CopyContext.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/python/jw/pkg/lib/CopyContext.py b/src/python/jw/pkg/lib/CopyContext.py index cae43bac..5e731736 100644 --- a/src/python/jw/pkg/lib/CopyContext.py +++ b/src/python/jw/pkg/lib/CopyContext.py @@ -66,7 +66,7 @@ class CopyContext: return self.__dst async def _run(self) -> None: - await self._run() + raise NotImplementedError('CopyContext._run() must be overridden') async def run(self) -> None: await self._run() -- 2.55.0 From 62803a4a1bb524238f0cce317aa3e141706ff72b Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Wed, 1 Jul 2026 16:03:24 +0200 Subject: [PATCH 2/8] lib.Uri: Return 'file' scheme for local paths The scheme property returns 'file://' for local paths instead of 'file', causing scheme_plus_authority to produce 'file://://' with a triple slash. Also fix the test to expect 'file' instead of 'file://'. Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL with pi.dev v0.80.2 Signed-off-by: Jan Lindemann --- src/python/jw/pkg/lib/Uri.py | 2 +- test/unit/python/jw/pkg/lib/Uri/test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/python/jw/pkg/lib/Uri.py b/src/python/jw/pkg/lib/Uri.py index 13e7d1a5..fa48245d 100644 --- a/src/python/jw/pkg/lib/Uri.py +++ b/src/python/jw/pkg/lib/Uri.py @@ -63,7 +63,7 @@ class Uri: def scheme(self) -> str: ret = self.__p.scheme if not ret: - return 'file://' + return 'file' return ret @cached_property diff --git a/test/unit/python/jw/pkg/lib/Uri/test.py b/test/unit/python/jw/pkg/lib/Uri/test.py index c88fe364..a435b8b2 100644 --- a/test/unit/python/jw/pkg/lib/Uri/test.py +++ b/test/unit/python/jw/pkg/lib/Uri/test.py @@ -19,7 +19,7 @@ assert '' in u.safe_full_with_username # safe version hides password # Scheme defaults to file u = Uri('/local/path') -assert u.scheme == 'file://' +assert u.scheme == 'file' assert u.protocol == 'file' # Pimp returns existing Uri unchanged -- 2.55.0 From a45af032f403887bba026437b683308d8417a18a Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Wed, 1 Jul 2026 16:03:39 +0200 Subject: [PATCH 3/8] lib.CmdBuild.add_dep_tree(): Fix broken f-string log() uses a regular string instead of an f-string, fix that. Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL with pi.dev v0.80.2 Signed-off-by: Jan Lindemann --- src/python/jw/pkg/cmds/projects/CmdBuild.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/python/jw/pkg/cmds/projects/CmdBuild.py b/src/python/jw/pkg/cmds/projects/CmdBuild.py index ca8c8bf7..3c83ad83 100644 --- a/src/python/jw/pkg/cmds/projects/CmdBuild.py +++ b/src/python/jw/pkg/cmds/projects/CmdBuild.py @@ -112,7 +112,7 @@ class CmdBuild(Cmd): # export return ret def add_dep_tree(cur, prereq_types, tree, all_deps): - log(DEBUG, 'Adding deps "{" ".join(prereq_types)}" of module {cur)') + log(DEBUG, f'Adding deps "{" ".join(prereq_types)}" of module {cur}') if cur in all_deps: log(DEBUG, 'Already handled module "{cur}"') return 0 -- 2.55.0 From 95a2d8a8677acc18d39759086c7ef6f2f82dd9b9 Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Wed, 1 Jul 2026 16:05:08 +0200 Subject: [PATCH 4/8] lib.DistroContext: Remove dead code from install() Remove unused helper functions from install(): _matches_host_prefix(), _crop_host_prefix(), _crop_default_prefix(), and _matches_default_prefix(). Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL with pi.dev v0.80.2 Signed-off-by: Jan Lindemann --- src/python/jw/pkg/cmds/secrets/lib/DistroContext.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/python/jw/pkg/cmds/secrets/lib/DistroContext.py b/src/python/jw/pkg/cmds/secrets/lib/DistroContext.py index f1958d93..37780baf 100644 --- a/src/python/jw/pkg/cmds/secrets/lib/DistroContext.py +++ b/src/python/jw/pkg/cmds/secrets/lib/DistroContext.py @@ -112,18 +112,6 @@ class DistroContext(FilesContext): ec = Local() return (await get(src_uri, content_filter = ProcFilterGpg(ec = ec))).stdout - def _matches_host_prefix(path: str) -> bool: - return re.match(r'^' + host_root_in_tar, path) is not None - - def _crop_host_prefix(path: str) -> bool: - return re.sub(r'^' + host_root_in_tar, '', path) is not None - - def _crop_default_prefix(path: str) -> bool: - return re.sub(default_rx, '', path) is not None - - def _matches_default_prefix(path: str) -> bool: - return re.match(r'^default', path) is not None - def _is_needed_secret(path: str) -> bool: return path in secret_paths -- 2.55.0 From 00b632d1cd72ac4130d5d03b9247115a22ec0aaf Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Wed, 1 Jul 2026 16:05:24 +0200 Subject: [PATCH 5/8] lib.Distro: Fix typo instaniate to instantiate Two error messages in Distro.__init__() contain the typo 'instaniate' instead of 'instantiate'. Fix the spelling. Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL with pi.dev v0.80.2 Signed-off-by: Jan Lindemann --- src/python/jw/pkg/lib/Distro.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/python/jw/pkg/lib/Distro.py b/src/python/jw/pkg/lib/Distro.py index 76a01571..84c9e5bb 100644 --- a/src/python/jw/pkg/lib/Distro.py +++ b/src/python/jw/pkg/lib/Distro.py @@ -29,10 +29,10 @@ class Distro(abc.ABC): default_pkg_filter: PackageFilter | None = None, ) -> None: if id is None: - raise ValueError('Tried to instaniate Distro without id') + raise ValueError('Tried to instantiate Distro without id') if ec is None: raise ValueError( - f'Tried to instaniate Distro "{id}" without execution context' + f'Tried to instantiate Distro "{id}" without execution context' ) self.__exec_context = ec self.__id: str | None = None -- 2.55.0 From ad9c5b581a5dd7d96586c0512aac9b0272832d95 Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Fri, 3 Jul 2026 10:14:32 +0200 Subject: [PATCH 6/8] lib.ProjectConf: Clean up temp files The _load() helper in test/unit/python/jw/pkg/lib/ProjectConf/test.py uses tempfile.NamedTemporaryFile(..., delete=False) but never removes the created files. Each test call leaked a .conf file in /tmp. Track all created paths in a _tmpfiles list and call _cleanup() at the end of the test to unlink them. Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL with pi.dev v0.80.2 Signed-off-by: Jan Lindemann --- test/unit/python/jw/pkg/lib/ProjectConf/test.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/unit/python/jw/pkg/lib/ProjectConf/test.py b/test/unit/python/jw/pkg/lib/ProjectConf/test.py index 36a3f558..6f359ece 100644 --- a/test/unit/python/jw/pkg/lib/ProjectConf/test.py +++ b/test/unit/python/jw/pkg/lib/ProjectConf/test.py @@ -1,14 +1,25 @@ +import os import tempfile from jw.pkg.lib.ProjectConf import ProjectConf # -- Helper: create temp file and read it -- +_tmpfiles: list[str] = [] + def _load(text: str) -> ProjectConf: with tempfile.NamedTemporaryFile(mode='w', suffix='.conf', delete=False) as f: f.write(text) f.flush() + _tmpfiles.append(f.name) return ProjectConf.read(f.name) + +def _cleanup(): + for _p in _tmpfiles: + os.unlink(_p) + del _p + _tmpfiles.clear() + # -- Basic construction -- pc = _load('[section]\nkey = value\n') @@ -250,4 +261,5 @@ assert pc.get_str('section', 'not_a_key') == '[bracket]' pc = _load('[section]\nlist = "a", "b", "c"\n') assert pc.get_list('section', 'list') == ['a', 'b', 'c'] +_cleanup() print('All ProjectConf tests passed') -- 2.55.0 From 8734b90f72bb8ea6f1c38f95f20661ea733fcebf Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Fri, 3 Jul 2026 14:17:50 +0200 Subject: [PATCH 7/8] test: apply yapf formatting to test.py files Yapf complains about spacing inconsistencies around assignment operators in function calls, fix that. Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL with pi.dev v0.80.2 Signed-off-by: Jan Lindemann --- .../python/jw/pkg/lib/ProjectConf/test.py | 2 +- test/unit/python/jw/pkg/lib/Result/test.py | 60 +++++++++---------- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/test/unit/python/jw/pkg/lib/ProjectConf/test.py b/test/unit/python/jw/pkg/lib/ProjectConf/test.py index 6f359ece..86aeb1be 100644 --- a/test/unit/python/jw/pkg/lib/ProjectConf/test.py +++ b/test/unit/python/jw/pkg/lib/ProjectConf/test.py @@ -7,7 +7,7 @@ from jw.pkg.lib.ProjectConf import ProjectConf _tmpfiles: list[str] = [] def _load(text: str) -> ProjectConf: - with tempfile.NamedTemporaryFile(mode='w', suffix='.conf', delete=False) as f: + with tempfile.NamedTemporaryFile(mode = 'w', suffix = '.conf', delete = False) as f: f.write(text) f.flush() _tmpfiles.append(f.name) diff --git a/test/unit/python/jw/pkg/lib/Result/test.py b/test/unit/python/jw/pkg/lib/Result/test.py index b8f25aa9..bee09116 100644 --- a/test/unit/python/jw/pkg/lib/Result/test.py +++ b/test/unit/python/jw/pkg/lib/Result/test.py @@ -2,34 +2,34 @@ from jw.pkg.lib.Result import Result # -- Basic construction -- -r = Result(stdout=b'hello', stderr=b'world', status=0) +r = Result(stdout = b'hello', stderr = b'world', status = 0) assert r.status == 0 assert r.stdout == b'hello' assert r.stderr == b'world' # -- stdout property -- -r = Result(stdout=b'test', status=0) +r = Result(stdout = b'test', status = 0) assert r.stdout == b'test' # stdout_or_none returns raw value -r = Result(stdout=b'test', status=0) +r = Result(stdout = b'test', status = 0) assert r.stdout_or_none == b'test' # stdout_str_or_none returns decoded string -r = Result(stdout=b'test', status=0) +r = Result(stdout = b'test', status = 0) assert r.stdout_str_or_none == 'test' # stdout_str returns decoded string -r = Result(stdout=b'test', status=0) +r = Result(stdout = b'test', status = 0) assert r.stdout_str == 'test' # stdout returns b'' when stdout is None and status == 0 -r = Result(stdout=None, status=0) +r = Result(stdout = None, status = 0) assert r.stdout == b'' # stdout raises when stdout is None and status != 0 -r = Result(stdout=None, status=1) +r = Result(stdout = None, status = 1) try: _ = r.stdout assert False, 'Should have raised' @@ -38,27 +38,27 @@ except Exception: # -- stderr property -- -r = Result(stderr=b'error', status=1) +r = Result(stderr = b'error', status = 1) assert r.stderr == b'error' # stderr_or_none returns raw value -r = Result(stderr=b'err', status=1) +r = Result(stderr = b'err', status = 1) assert r.stderr_or_none == b'err' # stderr_str_or_none returns decoded string -r = Result(stderr=b'err', status=1) +r = Result(stderr = b'err', status = 1) assert r.stderr_str_or_none == 'err' # stderr_str returns decoded string -r = Result(stderr=b'err', status=1) +r = Result(stderr = b'err', status = 1) assert r.stderr_str == 'err' # stderr returns b'' when stderr is None and status != 0 (error case) -r = Result(stderr=None, status=1) +r = Result(stderr = None, status = 1) assert r.stderr == b'' # stderr raises when stderr is None and status == 0 (success case) -r = Result(stderr=None, status=0) +r = Result(stderr = None, status = 0) try: _ = r.stderr assert False, 'Should have raised' @@ -67,7 +67,7 @@ except Exception: # -- None outputs -- -r = Result(stdout=None, stderr=None, status=1) +r = Result(stdout = None, stderr = None, status = 1) assert r.stdout_or_none is None assert r.stderr_or_none is None assert r.stdout_str_or_none is None @@ -75,28 +75,28 @@ assert r.stderr_str_or_none is None # -- encoding -- -r = Result(stdout=b'caf\xc3\xa9', encoding='UTF-8') +r = Result(stdout = b'caf\xc3\xa9', encoding = 'UTF-8') assert r.encoding == 'UTF-8' assert r.stdout_str == 'café' -r = Result(stdout=b'test', encoding='UTF-8') +r = Result(stdout = b'test', encoding = 'UTF-8') r.encoding = 'ASCII' assert r.encoding == 'ASCII' # -- strip_output -- -r = Result(stdout=b' spaces ', status=0) +r = Result(stdout = b' spaces ', status = 0) assert r.stdout_str_or_none == ' spaces ' -r = Result(stdout=b' spaces ', status=0, strip_output=True) +r = Result(stdout = b' spaces ', status = 0, strip_output = True) assert r.stdout_str_or_none == 'spaces' -r = Result(stdout=b' lines \n', status=0, strip_output=True) +r = Result(stdout = b' lines \n', status = 0, strip_output = True) assert r.stdout_str_or_none == 'lines' # -- cmd and wd setters -- -r = Result(cmd=['echo', 'hello'], wd='/tmp') +r = Result(cmd = ['echo', 'hello'], wd = '/tmp') assert r.cmd == ['echo', 'hello'] assert r.wd == '/tmp' @@ -107,49 +107,49 @@ assert r.wd == '/home' # -- summary -- -r = Result(stdout=b'output', status=0) +r = Result(stdout = b'output', status = 0) assert 'status 0' in r.summary -r = Result(stderr=b'error', status=1) +r = Result(stderr = b'error', status = 1) assert 'status 1' in r.summary assert 'stderr' in r.summary # -- matches_error -- -r = Result(stderr=b'fatal error: missing file', status=128) +r = Result(stderr = b'fatal error: missing file', status = 128) assert r.matches_error('fatal error') -r = Result(stderr=b'fatal error: missing file', status=128) +r = Result(stderr = b'fatal error: missing file', status = 128) assert not r.matches_error('success') -r = Result(stdout=b'ok', stderr=b'', status=0) +r = Result(stdout = b'ok', stderr = b'', status = 0) assert not r.matches_error('.*') -r = Result(stdout=b'ok', stderr=None, status=0) +r = Result(stdout = b'ok', stderr = None, status = 0) assert not r.matches_error('.*') # -- summarize with custom cmd/wd -- -r = Result(stdout=b'out', status=0, cmd=['echo', 'test'], wd='/tmp') +r = Result(stdout = b'out', status = 0, cmd = ['echo', 'test'], wd = '/tmp') assert 'echo' in r.summary or 'status 0' in r.summary # -- __repr__ -- -r = Result(stdout=b'repr test', status=0) +r = Result(stdout = b'repr test', status = 0) s = repr(r) assert '0' in s # verbose=False uses numeric status assert 'repr test' in s # -- encoding setter -- -r = Result(stdout=b'test', status=0, encoding='UTF-8') +r = Result(stdout = b'test', status = 0, encoding = 'UTF-8') assert r.encoding == 'UTF-8' r.encoding = 'ASCII' assert r.encoding == 'ASCII' # -- strip_output setter -- -r = Result(stdout=b' x ', status=0, strip_output=False) +r = Result(stdout = b' x ', status = 0, strip_output = False) assert r.stdout_str_or_none == ' x ' r.strip_output = True # strip_output is a property setter, the value is stored -- 2.55.0 From 7820fde4e63bea7fc223464e9cea424780bd1b28 Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Fri, 3 Jul 2026 14:28:13 +0200 Subject: [PATCH 8/8] test: Fix test.py duplicate module name error Mypy scans both src/python/ and test/ under the same jw namespace, and finds three test files named test.py. Without a package root at the test/ level, mypy resolves all of them to the bare module name test, and rejects them as duplicates. Add __init__.py at test/ marks it as a package root, so each test.py resolves to a unique full path, e.g. test.unit.python.jw.pkg.lib.Result.test Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL with pi.dev v0.80.2 Signed-off-by: Jan Lindemann --- test/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/__init__.py diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 00000000..e69de29b -- 2.55.0