__run() only accepts a return value from _run() as the process exit
status if it is an int between 0 and 255, and silently drops any other
value. A command that returns, for instance, 300 therefore exits with
status 0, which presents a failure as a success to the caller without
any trace of the mistake.
Log an error when the returned exit status is out of range so that the
programming error is visible, while still exiting with 0 instead of
passing an invalid status to the shell.
Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.84.2
Signed-off-by: Jan Lindemann <jan@janware.com>
The subcommand registration in _build_parser() defines a SubCommand
helper class inside the add_cmds_to_parser() closure, so a fresh class
object is created on every call. It also stores command names and
aliases in a dictionary without checking for duplicates, so a colliding
name or alias is silently overwritten, and it relies on every subparser
level sharing the dest = 'command' attribute to descend one level per
re-parse, an invariant that is not documented anywhere.
Hoist the helper to a module-level _SubCommand NamedTuple, log a
warning when a subcommand name or alias collides with an earlier one at
the same level, and document the dest = 'command' invariant next to the
re-parse.
Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.84.2
Signed-off-by: Jan Lindemann <jan@janware.com>
_add_arguments() adds the global options to self.__parser instead of
to the parser it receives. The two are the same object, because the
only caller passes self.__parser, so the change is not observable.
Use the parser parameter instead, so that the method honors its
argument the way Cmd.add_arguments() does, and so that the global
options can be shared with other parsers, e.g. the subcommand
parsers, without rewriting this method.
Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.84.2
Signed-off-by: Jan Lindemann <jan@janware.com>
_run() receives the parsed arguments as its args parameter, but then
checks the private __args attribute for the func attribute and resolves
the command function through the args property. Both refer to the same
object today, so the mixing is harmless, but it obscures the data flow
and would silently diverge if a caller ever passed a namespace other
than the stored one.
Use the args parameter consistently in _run().
Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.84.2
Signed-off-by: Jan Lindemann <jan@janware.com>
__init__() builds the parser and the lazy subcommand registration
inside it decides which subcommands to register by re-parsing
sys.argv. run() then parses a different argv, so if the caller passes
an argv that is deeper than the one in sys.argv, the required
subparsers have not been registered and the invocation fails with an
"unrecognized arguments" error. run_sub_commands() passes argv to
run(), so the mismatch is reachable from the public API.
Move the parser construction from __init__() into _build_parser() and
call it from run() when an argv is given, so that registration and
parsing are driven by the same command line. The top-level command
instances are created once in __init__() and reused when the parser
is rebuilt.
Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.84.2
Signed-off-by: Jan Lindemann <jan@janware.com>
When run() creates an event loop, it installs it with
set_event_loop() but never restores the thread's previous loop, so
after run() returns, the thread is left with the now-closed loop
created by run(). Any code that calls get_event_loop() afterwards
gets a closed loop, and on Python 3.13+ a thread that had no loop at
all starts emitting or raising deprecation errors that run() caused.
Capture the thread's current loop with _get_current_event_loop()
before installing a new one, and restore it in the finally block. If
there was no previous loop, unset the loop with set_event_loop(None)
so that the thread is left without a loop instead of with the closed
one.
Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.84.2
Signed-off-by: Jan Lindemann <jan@janware.com>
close() closes the application's own event loop, but the AsyncRunner
is only released in the finally block of run(). An application that
creates a runner through call_async() and then calls close(), for
instance through the async context manager, therefore leaks the
runner, and close() does not fulfill its contract of releasing all
resources.
Move the AsyncRunner cleanup from the finally block of run() into
close() and reset the own-loop flag when the loop is closed, so that
close() releases everything and run() only has to call it.
Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.84.2
Signed-off-by: Jan Lindemann <jan@janware.com>
__aenter__() and __aexit__() are empty stubs. Using the application
as an async context manager therefore binds None in the as clause,
and releases nothing on exit: an AsyncRunner created through
call_async() keeps running in its thread, and since that thread is
not a daemon, the process does not exit after the block.
Return self from __aenter__(), and call close() from __aexit__(), so
that the context manager binds the application and releases all
resources on exit, whether the block exits normally or with an
exception.
Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.84.2
Signed-off-by: Jan Lindemann <jan@janware.com>
The --log-level and --log-flags options are added without a type
converter, so argparse never validates their values. _build_parser()
then hands the raw string to set_log_level() and set_log_flags() during
its first parse, and an unparseable value such as "INVALID" crashes
__init__() with a raw ValueError traceback instead of a usage error.
Pass type = parse_log_level() and type = parse_log_flags() when adding
the options, so that argparse reports invalid values with the standard
usage error and exit status 2. argparse only applies the converter to
command-line strings, so the int and LogFlag defaults are unaffected.
Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.84.2
Signed-off-by: Jan Lindemann <jan@janware.com>
asyncio.get_event_loop() is removed in Python 3.14 when called from
outside an async context. The current code calls it in __init__(),
which crashes on 3.14+.
To fix this, drop the eager loop creation from __init__(). Instead,
lazily create a loop in run() via asyncio.new_event_loop() when no
external loop was provided, and close it in the finally block. This
makes the lifecycle symmetric: run() owns the full create-use-close
cycle and supports re-entrant calls.
Replace __del__() with an explicit close() method, guarding against
double-close via is_closed(). close() always clears __eloop to None
so a closed loop never lingers.
Signed-off-by: Jan Lindemann <jan@janware.com>
The diff-all and diff targets diff all projects in the workspace
without filtering.
Add a target "diff-projects". It sets PGIT_SH_PROJECTS to the list of
projects from build-order, limiting the diff to the dependency
closure around $(PROJECTS).
Signed-off-by: Jan Lindemann <jan@janware.com>
If /usr/bin/isort is found, run it during "make format" to get a
defined way the imports are sorted. tool.isort in pyproject.toml is
updated to match the other fixers.
Commit the fallout of this change. Running the other fixers alone
doesn't change the formatting, so this should be safe.
Signed-off-by: Jan Lindemann <jan@janware.com>
Some uses of $(JW_PKG_PY) need to ensure their own topdir format,
which results in two options on jw-pkg.py's command line, with the
second overwriting the first. This works but is a minor uglyness.
Since the default built into jw-pkg works for all other cases, remove
--topdir-format from the JW_PKG_PY variable.
Signed-off-by: Jan Lindemann <jan@janware.com>
BUILD_PKG_CONFIG installs files across project directories into
$(JWBDIR) because of this:
BUILD_PKG_CONFIGDIR = $(JWBDIR)/pkgconfig
The concept is currently not used by any downstream project anymore,
hence it lacks sufficient testing at this point. Moreover, it breaks
building repos which are sandboxed to their own root directory,
because it tries to install the generated .pc file back into
$(JWBDIR). The concept itself isn't entirely off, so leave the code
in, disabled, with a comment.
Signed-off-by: Jan Lindemann <jan@janware.com>
If $(CACHE_PROJECT_MK) AKA .cache-project.mk already exists but needs
to be remade because of dependencies, it is included, its content
read, then the content cached again to the same file. That way stale
variables can leak into the refreshed cache.
This commit fixes the situation by removing it, then invoking make
again to remake it, thereby clearing the variable database and
forcing make to fill the variables afresh.
Signed-off-by: Jan Lindemann <jan@janware.com>
This reverts commit 11ccaef832.
PYTHONPATH was actually produced correctly by CmdPythonpath before
this commit. This was a red herring, suggested by a buggy downstream
project, revert the change.
Signed-off-by: Jan Lindemann <jan@janware.com>
The __init__() method reads default log configuration directly
from environment variables, making it impossible for subclasses to
change defaults.
Extract the defaults into _default_log_flags(), _default_log_level(),
_default_log_file(), and _default_show_backtrace() methods, then call
them from __init__(). Subclasses can now override these methods to
customize defaults without needing to override the entire __init__()
method.
Signed-off-by: Jan Lindemann <jan@janware.com>
Add _default_*_env() helper methods that return the environment variable
names, and use them in __init__() instead of hard-coded strings. This
allows subclasses to override the names.
Signed-off-by: Jan Lindemann <jan@janware.com>
Replace the set of flag strings with a Flag enum for better type
safety and bitwise operations. Add parse_flags() to convert
comma-separated strings into a LogFlag value. Update set_flags() and
set_log_flags() to accept str | LogFlag | None.
set_flags() and set_log_flags() don't return str anylonger which is a
breaking change, but shouldn't be a problem because it's not used
anywhere.
Signed-off-by: Jan Lindemann <jan@janware.com>
The argument string builder in log() previously iterated with
enumerate and checked per-iteration whether to prepend a space.
Replace the loop with ' '.join(str(a) for a in args), which is a
single C-level operation.
Move the leading-space logic after the only_printable block so the
regex transformations see the raw joined content.
Signed-off-by: Jan Lindemann <jan@janware.com>
When prefix flags (position, prio, date) are combined with message
arguments, log() would double-space the output (e.g. '<N> Created')
or omit the separator entirely (e.g. '<N>Created'). The root cause
was that log() prepended a space to every argument, regardless of
whether the prefix already ended with one.
Fix: only prepend a space before the first argument when the prefix
doesn't already end with one. This produces exactly one separator
between prefix and content regardless of which flags are active.
Also fix log_m(): skip empty strings (the sentinel '') so it doesn't
contribute a space. And fix the early return that dropped messages
when no prefix flags were set (previous commit).
Signed-off-by: Jan Lindemann <jan@janware.com>
When position, prio, and date are all absent from the log flags, the
msg variable is empty. The early return 'if not len(msg): return'
would then skip printing the actual message in margs.
Fix by checking both msg and margs before returning. This ensures
messages are still printed even when no prefix flags are set.
Signed-off-by: Jan Lindemann <jan@janware.com>
Remove 8 redundant mypy settings that are already enabled by strict
mode: warn_redundant_casts, strict_equality, disallow_any_generics,
warn_unused_ignores, disallow_untyped_calls, warn_return_any,
disallow_incomplete_defs, disallow_untyped_defs.
Keep warn_unreachable explicitly since it's not part of strict mode.
Also remove the duplicate exhaustive-match entry from enable_error_code.
Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL and pi.dev 0.81.1
Signed-off-by: Jan Lindemann <jan@janware.com>
Fix fallout created by enabling the "strict" option:
lib/App.py:196: error: Class cannot subclass
"BaseCompleter" (has type "Any") [misc]
Signed-off-by: Jan Lindemann <jan@janware.com>
Add disallow_untyped_defs = true to the mypy configuration. This
requires all function definitions to have complete type annotations
for both parameters and return types. Since disallow_incomplete_defs
was already satisfied (all functions have partial annotations), this
final rule validates that no function is left without any type hints.
Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL and pi.dev 0.81.1
Signed-off-by: Jan Lindemann <jan@janware.com>
Add disallow_incomplete_defs = true to the mypy configuration. This
requires all function definitions to have complete parameter and return
type annotations, catching partially-annotated function signatures.
Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL and pi.dev 0.81.1
Signed-off-by: Jan Lindemann <jan@janware.com>
Add warn_return_any = true to the mypy configuration. This detects
functions that return Any from contexts where a specific return type
is declared, encouraging more precise type annotations.
Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL and pi.dev 0.81.1
Signed-off-by: Jan Lindemann <jan@janware.com>
Add type annotations and casts to functions that were returning Any
where a specific type was declared, satisfying the new warn_return_any
mypy rule.
Fixes:
- log.py: get_caller_pos return type via cast
- AsyncRunner.py: cast T for fut.result()
- util.py: cast for getattr result, str() for args.username
- FileContext.py: verbose_default bool annotation
- SSHClient.py: cast SSHClient for dynamic import
- lib/App.py: cast ArgumentParser, add return types to inner funcs
- pm/rpm.py, dpkg.py: cast Iterable[Package]
- App.py: cast for self.args.func(), add return types to inner funcs
- BaseCmdPkgRelations.py: cast str for args.delimiter
Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL and pi.dev 0.81.1
Signed-off-by: Jan Lindemann <jan@janware.com>
Add disallow_untyped_calls = true to the mypy configuration. This
requires all function calls from typed code to target typed functions,
ensuring better type safety across the codebase.
Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL and pi.dev 0.81.1
Signed-off-by: Jan Lindemann <jan@janware.com>
Add missing type annotations to functions that are called from typed
contexts, satisfying the new disallow_untyped_calls mypy rule.
Fixes:
- Local.py: __log() with typed parameters
- lib/App.py: _add_arguments(), add_cmd_to_parser(), add_cmds_to_parser()
- pm/rpm.py, dpkg.py: meta_map() return type
- Exec.py: __init_askpass() return type
- App.py: strip_module_from_spec(), __get_project_refs_cached(),
ResultCache.__init__ and run(), _add_arguments()
- Added Collection type for truthy-iterable compliance
Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL and pi.dev 0.81.1
Signed-off-by: Jan Lindemann <jan@janware.com>
Add warn_unreachable = true to the mypy configuration. This detects
statements that mypy proves are unreachable, such as code after
an unconditional return or assertions that can never be true.
Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL and pi.dev 0.81.1
Signed-off-by: Jan Lindemann <jan@janware.com>
Add the exhaustive-match error code to the mypy configuration. This
requires all match statements to be exhaustive, handling all possible
values of the matched expression.
The fix is to add 'case _: pass' to each match statement that is
intended to be non-exhaustive, making the non-exhaustiveness explicit.
Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL and pi.dev 0.81.1
Signed-off-by: Jan Lindemann <jan@janware.com>
Add 'case _: pass' to match statements that are intentionally
non-exhaustive, satisfying the new exhaustive-match mypy rule.
Also replaced 'case '_':' (a string literal) with 'case _: pass' in
pkg_relations.py since it was an unreachable case (syntax is a
VersionSyntax enum, not a str).
Added 'case VersionSyntax.names_only:' to the match in pkg_relations.py
to handle the missing enum value.
Files modified:
- util.py: Two match statements for askpass env vars
- Distro.py: Three match statements for backend/os detection
- pkg_relations.py: Match on VersionSyntax enum
- CmdListRepos.py: Match on URL scheme
Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL and pi.dev 0.81.1
Signed-off-by: Jan Lindemann <jan@janware.com>
Add the truthy-iterable error code to the mypy configuration. This
detects Iterable parameters that are used in boolean contexts (if not
names, etc.) since Iterable values are always truthy.
The fix is to change Iterable[str] parameters to Collection[str] when
they are tested for emptiness, since Collection guarantees __len__.
Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL and pi.dev 0.81.1
Signed-off-by: Jan Lindemann <jan@janware.com>
Change parameter types from Iterable[str] to Collection[str] wherever
the parameter is tested for emptiness (if not names). This satisfies
the new truthy-iterable mypy rule, since bare Iterable values are
always truthy even when empty.
Affected files:
- Distro.py: install, delete, select, _select, _select_by_name
- rpm.py: query_packages
- suse/Distro.py: _select_by_name
- Cmd.py (secrets): _match_files, _list_template_files, etc.
- DistroContext.py: list_template_files, list_secret_paths, etc.
Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL and pi.dev 0.81.1
Signed-off-by: Jan Lindemann <jan@janware.com>
Add warn_unused_ignores = true to the mypy configuration. This warns
when a '# type: ignore' comment has no associated error code,
ensuring all type ignores are explicit about which error they suppress.
Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL and pi.dev 0.81.1
Signed-off-by: Jan Lindemann <jan@janware.com>
Some '# type: ignore' comments are needed because they complain about missing
but optional third-party packages: argcomplete, paramiko, asyncssh. The next
commit will enable warn_unused_ignores, and since nor mypy nor pyright have a
way of knowing that this is a tolerable lack of packages, this commit teaches
them in advance.
Signed-off-by: Jan Lindemann <jan@janware.com>
Add disallow_any_generics = true to the mypy configuration. This
requires all generic types (dict, list, Iterable, Types, etc.) to
have explicit type arguments instead of using bare generic aliases.
Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL and pi.dev 0.81.1
Signed-off-by: Jan Lindemann <jan@janware.com>
Add explicit type arguments to all generic type annotations that were
previously bare, satisfying the new disallow_any_generics mypy rule.
Fixes:
- Distro.py: Iterable[str] for expand_macros fmt parameter
- Cmd.py: Types[Any] for add_subcommands cmds parameter
- AsyncSSH.py: dict[str, Any] for _connect_kwargs return type
Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL and pi.dev 0.81.1
Signed-off-by: Jan Lindemann <jan@janware.com>