Commit graph jw-pkg/src/python/jw/pkg/lib/util.py
Author SHA1 Message Date
5d77955ed9
py-check.mk: Run isort with "make format"
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>
2026-08-11 16:45:14 +02:00
1e613a39c6 App, cmds, lib: Fix Any returns from typed functions
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>
2026-08-09 12:07:37 +00:00
55b63ded69 lib, cmds: Add exhaustive match cases
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>
2026-08-09 11:32:51 +00:00
044d2cb065
util: Remove redundant cast in run_curl_into
The cast to 'T' in run_curl_into was redundant: mypy already narrows
the return value of json.loads() to type T after the isinstance(ret,
expected_type) check (where expected_type: type[T]).

Remove the unnecessary cast and clean up the unused 'cast' import.
This satisfies the new warn_redundant_casts mypy rule.

Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL and pi.dev 0.81.1
Signed-off-by: Jan Lindemann <jan@janware.com>
2026-08-07 18:02:27 +02:00
dcbe959843
lib: Fix misplaced "# export" markers
A couple of "# export" markers after function prototypes have been
pushed along with the closing parenthesis onto the wrong line by the
code formatter, fix that.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-07-24 13:30:17 +02:00
4df432b22d
lib.util.run_curl_into(): Fix indentation error
Fix a regregression breaking run_curl() / run_curl_into(), introduced
by commit 6db73873e7. A missing indentation raises a non-existing
Error after successful JSON parsing, fix that.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-06-01 20:14:45 +02:00
5d1ba6e15a
pyproject.toml: Enforce import annotations style
Add new ruff rules and fix their fallout:

  future-annotations = true

   select = [
    "TC",  # type-checking import placement rules
    "FA",  # future annotations rules
   ]

This comprises:

  - Streamline imports and exports in cmds.xxx.Cmd

    - Import base class as "Base"

    - Export types Cmd and Parent via __all__

  - Move all types imported only for annotation below TYPE_CHECKING

  - Use "from __future__ import annotations" all over the place

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-06-01 14:34:25 +02:00
6db73873e7
jw.pkg: Fix "make check" static code check fallout
The previous commits have put rules for linting and formatting via
ruff, yapf, mypy and pyright into place. They are checked with the
make check target, and this commit adds the fixes for the target to
succeed.

It does some refactoring where type checking dug up dirty bits, and
also adds lots of churn in the Python code. To a good deal, that's
owed to mere formatting changes. It would have been better to
seperate those from syntax and refactoring fixes into multiple
commits, so that the interesting changes don't drown in the
formatting nose. However, that would have been a lot of additional
work only to be thrown away by later commits, hence this commit has a
big diff in one piece. The size of the diff is regrettable but
hopefully a one-off: What it buys is automatic format checking for CI
and predictble formats for smaller diffs in the future.

Rules that "make check" enforces are, in the following order

   - Syntax checkers:

      - ruff check .
      - mypy .
      - pyright

   - Format check:

     - yapf --diff --recursive .

The refactoring includes:

  - Turn the Result class into a more elaborate object, capable of
    doing more heavy lifting around stderr and stdout decoding,
    summarizing outcome, and matching error strings.

    Aside from fixing broken type checks, this also removes lots of
    boilerplate calling code which is currently used for handling
    possible call outcome scenarios. Trying to access an inexistent,
    decoded string should raise a meaningful exception by itself now,
    which removes lots of code with case distinctions.

  - Fix Cmd type hierarchy:

    - Add the AbstractCmd class above Cmd. This is necessary because
      the checker rightfully complains it can't instantiate a Cmd
      instance where constructor arguments were needed. They never
      were, but the type used at the instantiating code's location in
      jw.pkg.App so claims.

    - Lots of sub- and sub-subcommands are derived from the base
      class of the invoking command. That provides some properties
      shared across the ancestor hierarchy of a command, but is
      semantically unsound. Fix that by introducing jw.pkg.BaseCmd
      class as a place to provide basic helpers shared across all
      commands used in a jw.pkg.App's context, and derive all command
      classes from that afresh. The parent command is still reachable
      via a common parent property.

Formatting changes are conforming to PEP-8, mostly, with minor
tweaks. All in all they include the following changes.

  - Remove # -*- coding: utf-8 -*-

    The line was needed by Python 2 which is not supported anylonger.
    For Python 3, the default encoding is UTF-8, anyway.

  - Allow to run "make py-format" without having it produce any
    changes. It's basically "yapf --in-place --recursive ." with some
    code style settings, see conf/topdir/pyproject.toml. The settings
    may be debatable. I've had custom tweaks in place on that target,
    too, but then again, IDEs would have more hassle to integrate
    that.

  - Introduce a 88 character line length limit

  - One import per line, reshuffle them semantically, see
    [tool.isort] in pyproject.toml.

  - Hide imports needed for type-checking only behind

      if TYPE_CHECKING

  - Spaces around assignments accounts for much churn. Having having
    no spaces in inline parameter list assignments and default
    parameter values would arguably be more compact where it's
    useful. On the other hand, I have not found a code formatter
    which allows spaces around assignments in parameter lists broken
    into one per line and that's often better than a wall of text.

  - Add two spaces before # export, as this seems to be mandated by
    PEP-8

  - Use single quotes by default

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-05-31 18:20:38 +02:00
5bb7fe96db
lib.util.get(): Add function
Add get(), which does pretty much what FileContext.get() does, but
with auto-instantiating a FileContext instance. Input processing
filters can be passed, too, all *args and **kwargs are passed
unchanged to the FileContext's constructor.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-28 13:00:29 +02:00
5837d10a1c
lib.util.copy(): Tolerate list[str] and FileContext args
Modify copy():

  - Allow the source argument src_uri to be a list of URIs, in which
    case copy should return a list of paths instead of one path

  - Change the dst_uri parameter to dst, signalling that it now also
    tolerates a FileContext

  - Use the CopyContext class to manage the source and target
    FileContext instances

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-28 13:00:29 +02:00
d9746cd20b
lib + cmds.projects: Use lib.Uri
Remove the feeble attempts at unifying URI handling, and use class
Uri from lib.Uri instead.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-28 13:00:29 +02:00
3ac3aff997
lib: Fix silent assertitons
There are a couple of assert statements in the codebase which can
make jw-pkg fail without any detail whatsoever if --backtrace is not
specified, fix that.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-25 08:44:49 +02:00
54aecff8e4 lib.ExecContext.run(), .sudo(): Rename env
The name of the env parameter to ExecContext.run() and .sudo() is not
descriptive enough for which environment is supposed to be modified
and how, so rename and split it up as follows:

  - .run(): env -> mod_env

  - .sudo(): env -> mod_env_sudo and mod_env_cmd

The parameters have the following meaning:

  - "mod_env*" means that the environment is modified, not replaced

  - "mod_env" and "mod_env_cmd" modify the environment "cmd" runs in

  - "mod_env_sudo" modifies the environment sudo runs in

Fix the fallout of the API change all over jw-pkg.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-19 14:36:50 +02:00
3cf5b2264e lib.FileContext: Add file methods
Add the following methods, meant to do the obvious:

  unlink(self, path: str) -> None
  erase(self, path: str) -> None
  rename(self, src: str, dst: str) -> None
  mktemp(self, tmpl: str, directory: bool=False) -> None
  chown(self, path: str, owner: str|None=None, group: str|None=None) -> None
  chmod(self, path: str, mode: int) -> None
  stat(self, path: str, follow_symlinks: bool=True) -> StatResult
  file_exists(self, path: str) -> bool
  is_dir(self, path: str) -> bool

All methods are async and call their protected counterpart, which is
designed to be overridden. If possible, default implementations do
something meaningful, if not, they just raise plain
NotImplementedError.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-18 10:43:31 +02:00
fe3508036e lib.FileContext.put(): Change param mode type str -> int
Don't pass mode as a string to put(). Given the multitunde of
possible string representations for numbers, some understood by
int(string, 0) and some not, there's too much room for passing
strings which are unparseable, or worse, prone to be parsed wrongly.

However, pass mode down to _put() as a string for convenience,
because that's what most _put() implementations will need to use. If
they don't, converting to int is easy from the one defined string
format.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-18 10:43:31 +02:00
910f10b194 lib.util.run_cmd(): Remove parameter interactive
run_cmd() is a thin layer over the public ExecContext API, which
falls back to using a Local instance if not other ExecContext is
specified explicitly. Both the default Local context as the
subsequent call to run() should have the same idea about
interactivity, so allowing to specify it in two parameters
("interactive" and "cmd_input") is a bad idea. Remove "interactive".

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-18 10:43:29 +02:00
888c0495ec lib.base: Add module
Add lib.base to provide basic definitions.

For now, move the definiions of Result, Input and InputMode from
ExecContext into lib.base. Having to import them from the ExecContect
module is too heavy-handed for those simple types.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-16 12:57:04 +02:00
63383cb683 lib.util.copy(): Add function
Add copy(src_uri, dst_uri), instatiating two ExecContext instances,
and doing the obvious with them - copying from src_uri to dst_uri.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-16 12:55:47 +02:00
04b294917f lib.ExecContext: Support bytes-typed cmd_input
The Input instance passed as cmd_input to ExecContext.run() and
.sudo() currently may be of type str. Allow to pass bytes, too.

At the same time, disallow None to be passed as cmd_input. Force the
caller to be more explicit how it wants input to be handled, notably
with respect to interactivity.

Along the way fix a bug: Content in cmd_input should result in
CallContext.interactive == False but doesn't. Fix that.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-16 10:21:25 +02:00
bafc7fed2a lib.ec.ssh.Exec: Honour self.interactive
The Exec SSHClient ignores the "interactive" argument passed to its
constructor, fix that.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-11 14:53:07 +02:00
f18575a267 lib.util.run_curl(): Add decode parameter
run_curl() has no clear API of whether or not the return values should
be decoded. It has parse_json, which should imply decoding, but there's
no way to specify that explicitly. Moreover, when it tries to decode, it
decodes on the coroutine returned from run_cmd(), not the awaited
coroutine return value.

Add a decode parameter, defaulting to False, change the parse_json
parameter's default from True to False, and fix the run_cmd() return
value evaluation.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-08 10:49:52 +02:00
9b6ec109a1 cmds and lib: Don't print() log messages
print() should be used to output information requested by a certain
command, but not for logging the process to achieve it. log() should
be used for the latter. The current code has the distinction not down
clearly, fix that.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-27 09:16:14 +01:00
6d876e88f6 lib.util.run_sudo(): Pass argument list on unchanged
run_sudo() is a thin wrapper around ExecContext.sudo(), so don't try
to make sense more arguments than necessary.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-25 07:37:56 +00:00
21e67291b5 Fix: Decode run_cmd() result
Since commit 02697af5, ExecContext.run() returns bytes for stdout and
stderr and fixes that in calling code. The thing it did not fix was
the code calling run_cmd(), which also made return bytes. This commit
catches up on that.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-25 07:32:45 +01:00
02697af568 lib.ExecContext: Align .sudo() prototype to .run()
ExecContext's .sudo() omits many of run()'s parameters, and this
commit adds them. To avoid redundancy around repeating and massaging
the long parameter list of both functions and their return values, it
also adds some deeper changes:

  - Make run(), _run(), sudo() and _sudo() always return instances of
    Result. Before it was allowed to return a triplet of stdout,
    stderr, and exit status.

  - Have ExecContext stay out of the business of decoding the result
    entirely. Result provides a convenience method .decode()
    operating on stdout and stderr and leaves the decision to the
    caller.

    This entails miniscule adaptations in calling code, namely in
    App.os_release, util.get_profile_env() and CmdListRepos._run().

  - Wrap the _run() and _sudo() callbacks in a context manager object
    of type CallContext to avoid code duplication.

  - Consistently name the first argument to run(), _run(), sudo() and
    _sudo() "cmd", not "args". The latter suggests that the caller is
    omitting the executable, which is not the case.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-20 10:30:25 +01:00
67a2931f5e App: Support --verbose
Add the --verbose global option, which is made available as the
App.verbose property.

Some functions still take a verbose parameter, but the type of these
parameters is converted from bool to bool|None.  The idea is that, if
they are None, their verbosity falls back to the global default.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-06 19:02:22 +01:00
7fdba1b5db lib.util: Add ec: ExecContext to all subprocesses
Allow to pass an optional execution context to all functions spawning
a subprocess defined in lib.util.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-06 15:14:13 +01:00
fadf1bca49 lib.util.run_cmd(): Add parameter ec: ExecContext
Allow to specify the ExecContext in a call to run_cmd(). This
effectively makes run_cmd() an thin wrapper around ExecContext.run(),
which is what's going to be used in the future. The wrapper is for
backwards-compatibility.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-06 15:14:13 +01:00
3e897f4df8 lib.Distro, ExecContext: Add classes, refactor lib.distro
The code below lib.distro, as left behind by the previous commit, is
geared towards being directly used as a command-line API. This commit
introduces the abstract base class Distro, a proxy for
distribution-specific interactions. The proxy abstracts distro
specifics into an API with proper method prototypes, not
argparse.Namespace contents, and can thus be more easily driven by
arbitrary code.

The Distro class is initialized with a member variable of type
ExecContext, another new class introduced by this commit. It is
designed to abstract the communication channel to the distribution
instance.  Currently only one specialization exists, Local, which
interacts with the distribution and root file system it is running
in, but is planned to be subclassed to support interaction via SSH,
serial, chroot, or chains thereof.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-06 14:56:46 +01:00
ae902250bd jw/pkg/lib/util.run_cmd(): Chunked read_stream()
jw-pkg distro dup got hung in a chroot environment. strace shows that
write(2) into a pipe is the hanging syscall, with the write buffer
hinting at zypper dup output.

I strongly suspect that run_cmd() tries to write stdout into the pipe
which read_stream() fails to empty. So, make read_stream() more
resilient by using read(4096) instead of readline(), which I suspect
to be prone to hang on overlong lines.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-05 06:42:57 +00:00
0c1c2b9351 lib.util.run_cmd(): Reduce interactive logging
run_cmd() with cmd_input == mode:interactive  and verbose == true
logs output too often. First, __log() is called, then pty.spawn()
writes everything it reads from the PTY master to the terminal.

The fix it to not call __log() from _read() for the PTY reader.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-04 16:02:07 +00:00
8bc22a3a68 lib.util.run_cmd(): Fix docstring
The docstring of run_cmd()'s signature documents a wrong return
value, fix that.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-04 14:48:26 +01:00
565946643b jw.pkg.*.run_xxx(): Return exit status
Most run_xxx() return stdout and stderr. There's no way, really, for
the caller to get hold of the exit code of the spawned executable. It
can pass throw=true, catch, and assume a non-zero exit status. But
that's not semantically clean, since the spawned function can well be
a test function which is expected to return a non-zero status code,
and the caller might be interested in what code that was, exactly.

The clearest way to solve this is to return the exit code as well.
This commit does that.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-03 11:23:30 +01:00
afc77ab61d jw.pkg.lib.util.run_curl(): Beautify logging
Make some incomprensible parser error messages if run_curl() returns
nothing slightly less incomprehensible.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-02-27 17:03:26 +01:00
b81406e11a run_cmd() and friends: Make args a list[str]
This is a code maintenance commit: some run_xxx() helper functions
take a string, some a list, and some just digest all arguments and
pass them on as a list to exec() to be executed. That's highly
inconsistent. This commit changes that to list-only.

Except for the run_cmd() method of SSHClient, which is still run as a
shell method, because, erm, it's a shell. Might be changed in the
future for consistency reasons.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-02-27 09:09:10 +01:00
2906c697de jw.pkg.lib.util.run_cmd(): Add stderr to exception
If an error happens, append stderr to the exception thrown.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-02-24 14:53:17 +01:00
6916d7edc8 jw.pkg.lib.util.run_sudo(): Add parameter verbose
Add parameter verbose to run_sudo() and pass it on to run_cmd().

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-02-20 19:30:22 +01:00
b3fd624a3f jw.pkg.lib.util.get_profile_env(): add -> keep
Replace the boolean parameter "add" with the richer "keep":

  - False            -> Don't keep anything
  - True             -> Keep what's in the current environment
  - List of strings  -> Keep those variables

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-02-19 07:52:53 +01:00
2d1beeebb0 jw.pkg.lib.util.get_profile_env(): Add function
Add a function get_profile_env(), a function returning environment
variables from /etc/profile. Pass add=True to add its contents to the
existing environment dictionary, overwriting old entries, or pass
False to get the pristine content.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-02-18 14:18:46 +01:00
e104fa2e46 jw.pkg.lib.util.run_cmd(): Add output_encoding
Add a parameter "output_encoding" to run_cmd(). The parameter allows
the caller to specify if the output encoding should be detected as is
by passing None (the default), if the output should be returned as
undecoded bytes by passing the special string "bytes", or if the
output should be treated as the encoding with the specified name and
decoded to strings.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-02-18 13:38:12 +01:00
82c6a44ad6 jw.pkg.lib.util.run_cmd(): Fix mode:auto
In cmd_input == "mode:auto", the interactive variable is currently
not set due to a typo. Fix that.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-02-18 13:31:26 +01:00
f8dc8ee6d1 jw.pkg.lib.util.run_cmd(): Honour env in PTY mode
The evironment passed to run_cmd() via env is currently not honoured
with mode:interactive. Fix that.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-02-18 13:29:48 +01:00
75faf02232 jw.pkg.lib.util.run_cmd(): Add title parameter
Allow the caller to choose which title should be used for the
command's logging output box.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-02-18 11:31:53 +01:00
7eb15f2477 jw.pkg.lib: Don't log {e}
Don't log an Exception as {e} but as str(e) producing nicer output.
Or as repr(e) if a backtrace is requested, because to people who can
read backtraces, type info might be of interest. Also, remove
pointless time stamps, those belong into the logging framework.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-02-18 11:31:13 +01:00
b1d4e20295 jw.pkg.util: Add sudo()
Move the body of BackendCmd.sudo() into a function. The rationale
behind that is that its functionality is independent of the calling
object for the most part, so having it in a function instead of a
method is the more modular pattern.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-02-18 01:20:25 +01:00
4274a71c62 lib.util.run_cmd(): Rewrite it to be async
run_cmd() is synchronous. Now that all commands are asynchronous, we
can await it, so rewrite it to be asynchronous, too.

Other changes:

  - Make it return stderr as well in case its needed

  - Drop into a pseuto-tty if
    - cmd_input == "mode:interactive" or
    - cmd_input == "mode:auto" and stdin is a TTY

  - Add argument env, defaulting to None. If it's a dict, it will be
    the environment the command is run in

This entails making all functions using run_cmd() async, too,
including run_curl(), get_username() and get_password().

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-01-28 17:41:40 +01:00
2e69639362 jw.pkg.build.lib: Move to jw.pkg.lib
In preparation of reorganizing the tree below cmds, move the lib
subdirectory a level up.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-01-26 17:58:23 +01:00
Renamed from src/python/jw/pkg/build/lib/util.py (Browse further)