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>
This commit is contained in:
Jan Lindemann 2026-05-27 07:16:05 +02:00
commit 6db73873e7
Signed by: Jan Lindemann
GPG key ID: 3750640C9E25DD61
97 changed files with 3229 additions and 1893 deletions

View file

@ -1,5 +1,4 @@
#!/usr/bin/python3 #!/usr/bin/python3
# -*- coding: utf-8 -*-
# PYTHON_ARGCOMPLETE_OK # PYTHON_ARGCOMPLETE_OK
from jw.pkg.App import App from jw.pkg.App import App

View file

@ -1,2 +1,3 @@
from pkgutil import extend_path from pkgutil import extend_path
__path__ = extend_path(__path__, __name__) __path__ = extend_path(__path__, __name__)

View file

@ -1,29 +1,40 @@
# -*- coding: utf-8 -*-
# #
# This source code file is a merge of various build tools and a horrible mess. # This source code file is a merge of various build tools and a horrible mess.
# #
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, Iterable import argparse
import os
import pwd
import re
import sys
if TYPE_CHECKING:
from typing import TypeAlias
import os, sys, pwd, re
import os, sys, argparse, pwd, re
from functools import lru_cache, cached_property
from enum import Enum, auto from enum import Enum, auto
from functools import lru_cache
from typing import TYPE_CHECKING
from .lib.App import App as Base from .lib.App import App as Base
from .lib.log import *
from .lib.Distro import Distro from .lib.Distro import Distro
from .lib.base import InputMode from .lib.log import DEBUG, ERR, log
if TYPE_CHECKING:
import os
import pwd
import re
import sys
from typing import TypeAlias
from .lib.ExecContext import ExecContext
from .lib.PackageFilter import PackageFilter
# Meaning of pkg.requires.xxx variables # Meaning of pkg.requires.xxx variables
# build: needs to be built and installed before this can be built # build: needs to be built and installed before this can be built
# devel: needs to be installed before this-devel can be installed, i.e. before _other_ packages can be built against this # devel: needs to be installed before this-devel can be installed,
# run: needs to be installed before this-run can be installed, i.e. before this and other packages can run with this # i.e. before _other_ packages can be built against this
# run: needs to be installed before this-run can be installed,
# i.e. before this and other packages can run with this
# --------------------------------------------------------------------- Helpers # --------------------------------------------------------------------- Helpers
@ -35,32 +46,34 @@ class ResultCache(object):
def run(self, func, args): def run(self, func, args):
d = self.__cache d = self.__cache
depth = 0 depth = 0
keys = [ func.__name__ ] + args keys = [func.__name__] + args
l = len(keys) sz = len(keys)
for k in keys: for k in keys:
if k is None: if k is None:
k = 'None' k = 'None'
else: else:
k = str(k) k = str(k)
depth += 1 depth += 1
#log(DEBUG, 'depth = ', depth, 'key = ', k, 'd = ', str(d)) # log(DEBUG, 'depth = ', depth, 'key = ', k, 'd = ', str(d))
if k in d: if k in d:
if l == depth: if sz == depth:
return d[k] return d[k]
d = d[k] d = d[k]
continue continue
if l == depth: if sz == depth:
r = func(*args) r = func(*args)
d[k] = r d[k] = r
return r return r
d = d[k] = {} d = d[k] = {}
#d = d[k] # d = d[k]
raise Exception('cache algorithm failed for function', func.__name__, 'in depth', depth) raise Exception(
'cache algorithm failed for function', func.__name__, 'in depth', depth
)
class Scope(Enum): class Scope(Enum):
Self = auto() Self = auto()
One = auto() One = auto()
Subtree = auto() Subtree = auto()
Graph: TypeAlias = dict[str, set[str]] Graph: TypeAlias = dict[str, set[str]]
@ -68,27 +81,41 @@ Graph: TypeAlias = dict[str, set[str]]
class App(Base): class App(Base):
def __format_topdir(self, topdir: None|str, fmt: str) -> str: def __format_topdir(self, path: None | str, fmt: str) -> str | None:
if topdir is None: if path is None:
return None return None
match fmt: match fmt:
case 'unaltered': case 'unaltered':
return topdir return path
case None | 'absolute': case None | 'absolute':
return os.path.abspath(self.__topdir) return os.path.abspath(path)
case _: case _:
m = re.search(r'^make:(\S+)$', fmt) m = re.search(r'^make:(\S+)$', fmt)
if m is None: if m is None:
raise Exception(f'Can\'t interpret "{fmt}" as valid topdir ' + raise Exception(
'reference, expecting "unaltered", "absolute", or "make:<variable-name>"') f'Can\'t interpret "{fmt}" as valid topdir reference, '
'expecting "unaltered", "absolute", or "make:<variable-name>"'
)
return '$(' + m.group(1) + ')' return '$(' + m.group(1) + ')'
def __proj_dir(self, name: str, pretty) -> str: @property
def __topdir(self) -> str:
if self.___topdir is None:
raise Exception('Tried to access undefined top directory')
return self.___topdir
@property
def __pretty_topdir(self) -> str:
if self.___pretty_topdir is None:
raise Exception('Tried to access undefined pretty top directory')
return self.___pretty_topdir
def __proj_dir(self, name: str, pretty: bool) -> str | None:
if name == self.__top_name: if name == self.__top_name:
if pretty: if pretty:
return self.__pretty_topdir return self.__pretty_topdir
return self.__topdir return self.__topdir
for d in [ self.__projs_root, '/opt' ]: for d in [self.__projs_root, '/opt']:
ret = d + '/' + name ret = d + '/' + name
if os.path.exists(ret): if os.path.exists(ret):
return ret return ret
@ -97,7 +124,14 @@ class App(Base):
return None return None
raise Exception('No project path found for module "{}"'.format(name)) raise Exception('No project path found for module "{}"'.format(name))
def __find_dir(self, name: str, search_subdirs: list[str]=[], search_absdirs: list[str]=[], pretty: bool=True) -> str|None: def __find_dir(
self,
name: str,
search_subdirs: list[str] = [],
search_absdirs: list[str] = [],
pretty: bool = True,
) -> str | None:
def format_pd(name: str, pd: str, pretty: bool): def format_pd(name: str, pd: str, pretty: bool):
if not pretty: if not pretty:
return pd return pd
@ -107,7 +141,10 @@ class App(Base):
return pd return pd
if name == self.__top_name: if name == self.__top_name:
return self.__pretty_topdir return self.__pretty_topdir
raise NotImplementedError(f'Tried to pretty-format directory {pd}, not implemented') raise NotImplementedError(
f'Tried to pretty-format directory {pd}, not implemented'
)
pd = self.__proj_dir(name, False) pd = self.__proj_dir(name, False)
if pd is None: if pd is None:
return None return None
@ -126,11 +163,25 @@ class App(Base):
return ret return ret
return None return None
def __get_project_refs_cached(self, buf, visited, spec, section, key, add_self, scope, names_only): def __get_project_refs_cached(
return self.__res_cache.run(self.__get_project_refs, [buf, visited, spec, section, key, add_self, scope, names_only]) self, buf, visited, spec, section, key, add_self, scope, names_only
):
return self.__res_cache.run(
self.__get_project_refs,
[buf, visited, spec, section, key, add_self, scope, names_only],
)
def __get_project_refs(self, buf: list[str], visited: set[str], spec: str, def __get_project_refs(
section: str, key: str, add_self: bool, scope: Scope, names_only: bool) -> None: self,
buf: list[str],
visited: set[str],
spec: str,
section: str,
key: str,
add_self: bool,
scope: Scope,
names_only: bool,
) -> None:
name = self.strip_module_from_spec(spec) name = self.strip_module_from_spec(spec)
if names_only: if names_only:
spec = name spec = name
@ -142,59 +193,95 @@ class App(Base):
return return
visited.add(spec) visited.add(spec)
deps = self.get_value(name, section, key) deps = self.get_value(name, section, key)
log(DEBUG, 'name = ', name, 'section = ', section, 'key = ', key, 'deps = ', deps, 'scope = ', scope.name, 'visited = ', visited) log(
DEBUG,
(
f'name={name}, section={section}, key={key}, deps={deps}, '
f'scope={scope.name}, visited={visited}'
),
)
if deps and scope != Scope.Self: if deps and scope != Scope.Self:
if scope == Scope.One: if scope == Scope.One:
subscope = Scope.Self subscope = Scope.Self
else: else:
subscope = Scope.Subtree subscope = Scope.Subtree
deps = deps.split(',') for dep in deps.split(','):
for dep in deps:
dep = dep.strip() dep = dep.strip()
if not(len(dep)): if not (len(dep)):
continue continue
self.__get_project_refs_cached(buf, visited, dep, self.__get_project_refs_cached(
section, key, add_self=True, scope=subscope, buf,
names_only=names_only) visited,
dep,
section,
key,
add_self = True,
scope = subscope,
names_only = names_only,
)
if add_self: if add_self:
buf.append(spec) buf.append(spec)
def __read_dep_graph(self, projects: list[str], section: str, graph: Graph) -> None: def __read_dep_graph(
self,
projects: list[str],
sections: str | list[str],
graph: Graph,
) -> None:
if isinstance(sections, str):
sections = [sections]
for project in projects: for project in projects:
if project in graph: if project in graph:
continue continue
deps = self.get_project_refs([ project ], ['pkg.requires.jw'], section, for section in sections:
scope = Scope.One, add_self=False, names_only=True) deps = self.get_project_refs(
if not deps is None: [project],
['pkg.requires.jw'],
sections,
scope = Scope.One,
add_self = False,
names_only = True,
)
if deps is None:
continue
graph[project] = set(deps) graph[project] = set(deps)
for dep in deps: for dep in deps:
self.__read_dep_graph([ dep ], section, graph) self.__read_dep_graph([dep], sections, graph)
def __flip_dep_graph(self, graph: Graph): def __flip_dep_graph(self, graph: Graph):
ret: Graph = {} ret: Graph = {}
for project, deps in graph.items(): for project, deps in graph.items():
for d in deps: for d in deps:
if not d in ret: if d not in ret:
ret[d] = set() ret[d] = set()
ret[d].add(project) ret[d].add(project)
return ret return ret
def __find_circular_deps_recursive(self, project: str, graph: Graph, unvisited: list[str], def __find_circular_deps_recursive(
temp: set[str], path: str) -> str|None: self,
project: str,
graph: Graph,
unvisited: list[str],
temp: set[str],
path: list[str],
) -> str | None:
if project in temp: if project in temp:
log(DEBUG, 'found circular dependency at project', project) log(DEBUG, 'found circular dependency at project', project)
return project return project
if not project in unvisited: if project not in unvisited:
return None return None
temp.add(project) temp.add(project)
if project in graph: if project in graph:
for dep in graph[project]: for dep in graph[project]:
last = self.__find_circular_deps_recursive(dep, graph, unvisited, temp, path) last = self.__find_circular_deps_recursive(
dep, graph, unvisited, temp, path
)
if last is not None: if last is not None:
path.insert(0, dep) path.insert(0, dep)
return last return last
unvisited.remove(project) unvisited.remove(project)
temp.remove(project) temp.remove(project)
return None
def __find_circular_deps(self, projects: list[str], flavours: list[str]) -> bool: def __find_circular_deps(self, projects: list[str], flavours: list[str]) -> bool:
graph: Graph = {} graph: Graph = {}
@ -205,25 +292,27 @@ class App(Base):
while unvisited: while unvisited:
project = unvisited[0] project = unvisited[0]
log(DEBUG, 'Checking circular dependency of', project) log(DEBUG, 'Checking circular dependency of', project)
last = self.__find_circular_deps_recursive(project, self.__flip_dep_graph(graph), unvisited, temp, ret) last = self.__find_circular_deps_recursive(
project, self.__flip_dep_graph(graph), unvisited, temp, ret
)
if last is not None: if last is not None:
log(DEBUG, f'Found circular dependency below {project}, last is {last}') log(DEBUG, f'Found circular dependency below {project}, last is {last}')
return True return True
return False return False
def __init__(self, distro: Distro|None=None) -> None: def __init__(self, distro: Distro | None = None) -> None:
super().__init__('jw-pkg swiss army knife', modules=['jw.pkg.cmds']) super().__init__('jw-pkg swiss army knife', modules = ['jw.pkg.cmds'])
# -- Members without default values # -- Members without default values
self.__opt_interactive: bool|None = None self.__opt_interactive: bool | None = None
self.__opt_verbose: bool|None = None self.__opt_verbose: bool | None = None
self.__top_name: str|None = None self.__top_name: str | None = None
self.__distro = distro self.__distro = distro
self.__res_cache = ResultCache() self.__res_cache = ResultCache()
self.__topdir: str|None = None self.___topdir: str | None = None
self.__pretty_topdir: str|None = None self.___pretty_topdir: str | None = None
self.__exec_context: ExecContext|None = None self.__exec_context: ExecContext | None = None
# -- Members with default values # -- Members with default values
self.__topdir_fmt = 'absolute' self.__topdir_fmt = 'absolute'
@ -235,48 +324,83 @@ class App(Base):
pkg_filter_str = self.args.pkg_filter pkg_filter_str = self.args.pkg_filter
if pkg_filter_str is None: if pkg_filter_str is None:
pkg_filter_str = os.getenv('JW_DEFAULT_PKG_FILTER') pkg_filter_str = os.getenv('JW_DEFAULT_PKG_FILTER')
pkg_filter: PackageFilter|None = None pkg_filter: PackageFilter | None = None
if pkg_filter_str is not None: if pkg_filter_str is not None:
from .lib.PackageFilter import PackageFilterString from .lib.PackageFilter import PackageFilterString
pkg_filter = PackageFilterString(pkg_filter_str) pkg_filter = PackageFilterString(pkg_filter_str)
self.__distro = await Distro.instantiate( self.__distro = await Distro.instantiate(
ec = self.exec_context, ec = self.exec_context,
id = self.args.distro_id, id = self.args.distro_id,
default_pkg_filter = pkg_filter, default_pkg_filter = pkg_filter,
) )
async def __aexit__(self, exc_type, exc, tb) -> None: async def __aexit__(self, exc_type, exc, tb) -> None:
if self.__exec_context is not None: if self.__exec_context is not None:
await self.__exec_context.close() await self.__exec_context.close()
self.__exec_context = None self.__exec_context = None
return super().__aexit__(exc_type, exc, tb)
def _add_arguments(self, parser) -> None: def _add_arguments(self, parser) -> None:
super()._add_arguments(parser) super()._add_arguments(parser)
parser.add_argument('-t', '--topdir', default = None, help='Project Path') parser.add_argument('-t', '--topdir', default = None, help = 'Project Path')
parser.add_argument('--topdir-format', default = 'absolute', help='Output references to topdir as ' parser.add_argument(
+ 'one of "make:<var-name>", "unaltered", "absolute". Absolute topdir by default') '--topdir-format',
parser.add_argument('-p', '--prefix', default = None, default = 'absolute',
help='Parent directory of project source directories') help = (
parser.add_argument('--distro-id', default=None, help='Distribution ID (default is taken from /etc/os-release)') 'Output references to topdir as one of "make:<var-name>", '
parser.add_argument('--interactive', choices=['true', 'false', 'auto'], default='true', help='Wait for user input or try to proceed unattended') '"unaltered", "absolute". Absolute topdir by default'
parser.add_argument('--verbose', action='store_true', default=False, help='Be verbose on stderr about what\'s being done on the distro level') ),
parser.add_argument('--target', default='local', help='Run commands on this host') )
parser.add_argument('--pkg-filter', help='Default filter for all distribution package-related operations') parser.add_argument(
'-p',
'--prefix',
default = None,
help = 'Parent directory of project source directories',
)
parser.add_argument(
'--distro-id',
default = None,
help = 'Distribution ID (default is taken from /etc/os-release)',
)
parser.add_argument(
'--interactive',
choices = ['true', 'false', 'auto'],
default = 'true',
help = 'Wait for user input or try to proceed unattended',
)
parser.add_argument(
'--verbose',
action = 'store_true',
default = False,
help = "Be verbose on stderr about what's being done on the distro level",
)
parser.add_argument(
'--target', default = 'local', help = 'Run commands on this host'
)
parser.add_argument(
'--pkg-filter',
help = 'Default filter for all distribution package-related operations',
)
async def _run(self, args: argparse.Namespace) -> None: async def _run(self, args: argparse.Namespace) -> None:
self.__topdir = args.topdir self.___topdir = args.topdir
self.__pretty_topdir = self.__format_topdir(self.__topdir, args.topdir_format) self.___pretty_topdir = self.__format_topdir(self.___topdir, args.topdir_format)
self.__topdir_fmt = args.topdir_format self.__topdir_fmt = args.topdir_format
if self.__topdir is not None: if self.___topdir is not None:
self.__top_name = self.read_value(self.__topdir + '/make/project.conf', 'build', 'name') self.__top_name = self.read_value(
self.___topdir + '/make/project.conf', 'build', 'name'
)
if not self.__top_name: if not self.__top_name:
self.__top_name = re.sub('-[0-9.-]*$', '', os.path.basename(os.path.realpath(self.__topdir))) self.__top_name = re.sub(
'-[0-9.-]*$',
'',
os.path.basename(os.path.realpath(self.___topdir))
)
if args.prefix is not None: if args.prefix is not None:
self.__projs_root = args.prefix self.__projs_root = args.prefix
self.__pretty_projs_root = args.prefix self.__pretty_projs_root = args.prefix
await self.__init_async() await self.__init_async()
return await super()._run(args) await super()._run(args)
@property @property
def interactive(self) -> bool: def interactive(self) -> bool:
@ -288,20 +412,32 @@ class App(Base):
self.__opt_interactive = False self.__opt_interactive = False
case 'auto': case 'auto':
self.__opt_interactive = sys.stdin.isatty() self.__opt_interactive = sys.stdin.isatty()
case _:
raise ValueError(
f'Unknown --interactive value: {self.args.interactive}'
)
# Not logically possible to fail, but this keeps pyright happy
assert self.__opt_interactive is not None
return self.__opt_interactive return self.__opt_interactive
@property @property
def verbose(self) -> bool: def verbose(self) -> bool:
if self.__opt_verbose is None: if self.__opt_verbose is None:
self.__opt_verbose = self.args.verbose self.__opt_verbose = self.args.verbose
# Not logically possible to fail, but this keeps pyright happy
assert self.__opt_verbose is not None
return self.__opt_verbose return self.__opt_verbose
@property @property
def exec_context(self) -> str: def exec_context(self) -> ExecContext:
if self.__exec_context is None: if self.__exec_context is None:
from .lib.ExecContext import ExecContext from .lib.ExecContext import ExecContext
self.__exec_context = ExecContext.create(self.args.target, interactive=self.interactive,
verbose_default=self.verbose) self.__exec_context = ExecContext.create(
self.args.target,
interactive = self.interactive,
verbose_default = self.verbose,
)
return self.__exec_context return self.__exec_context
@property @property
@ -318,29 +454,51 @@ class App(Base):
raise Exception('No distro object') raise Exception('No distro object')
return self.__distro return self.__distro
def find_dir(self, name: str, search_subdirs: list[str]=[], search_absdirs: list[str]=[], pretty: bool=True): def find_dir(
return self.__find_dir(name, search_subdirs, search_absdirs, pretty) self,
name: str,
search_subdirs: list[str] = [],
search_absdirs: list[str] = [],
pretty: bool = True,
) -> str:
ret = self.__find_dir(name, search_subdirs, search_absdirs, pretty)
if ret is None:
msg = f'Failed to find directory for "{name}"'
log(ERR, msg)
for search_name, search in [
('subdirs', search_subdirs),
('absdirs', search_absdirs),
]:
if search:
log(ERR, f' Searched {search_name} in:')
for d in search:
log(ERR, f' - {d}')
raise FileNotFoundError(msg)
return ret
# TODO: add support for customizing this in project.conf # TODO: add support for customizing this in project.conf
def htdocs_dir(self, project: str) -> str: def htdocs_dir(self, project: str) -> str:
return self.find_dir(project, ['/src/html/htdocs', '/tools/html/htdocs', '/htdocs'], return self.find_dir(
['/srv/www/proj/' + project]) project,
['/src/html/htdocs', '/tools/html/htdocs', '/htdocs'],
['/srv/www/proj/' + project],
)
# TODO: add support for customizing this in project.conf # TODO: add support for customizing this in project.conf
def tmpl_dir(self, name: str) -> str: def tmpl_dir(self, name: str) -> str:
return self.find_dir(name, ['/tmpl'], ['/opt/' + name + '/share/tmpl']) return self.find_dir(name, ['/tmpl'], ['/opt/' + name + '/share/tmpl'])
def strip_module_from_spec(self, mod): def strip_module_from_spec(self, mod):
return re.sub(r'-dev$|-devel$|-run$', '', re.split('([=><]+)', mod)[0].strip()) return re.sub(r'-dev$|-devel$|-run$', '', re.split('([=><]+)', mod)[0].strip())
@lru_cache(maxsize=None) @lru_cache(maxsize = None)
def get_section(self, path: str, section: str) -> str: def get_section(self, path: str, section: str) -> str:
ret = '' ret = ''
pat = '[' + section + ']' pat = '[' + section + ']'
in_section = False in_section = False
file = open(path) file = open(path)
for line in file: for line in file:
if (line.rstrip() == pat): if line.rstrip() == pat:
in_section = True in_section = True
continue continue
if in_section: if in_section:
@ -350,10 +508,10 @@ class App(Base):
file.close() file.close()
return ret.rstrip() return ret.rstrip()
@lru_cache(maxsize=None) @lru_cache(maxsize = None)
def read_value(self, path: str, section: str, key: str) -> str|None: def read_value(self, path: str, section: str, key: str) -> str | None:
def scan_section(f, key: str) -> str|None: def scan_section(f, key: str) -> str | None:
if key is None: if key is None:
ret = '' ret = ''
for line in f: for line in f:
@ -374,43 +532,44 @@ class App(Base):
cont_line = '' cont_line = ''
rx = re.compile(r'^\s*' + key + r'\s*=\s*(.*)\s*$') rx = re.compile(r'^\s*' + key + r'\s*=\s*(.*)\s*$')
for line in lines: for line in lines:
#log(DEBUG, ' looking for "%s" in line="%s"' % (key, line)) # log(DEBUG, ' looking for "%s" in line="%s"' % (key, line))
m = re.search(rx, line) m = re.search(rx, line)
if m is not None: if m is not None:
return m.group(1) return m.group(1)
return None return None
def scan_section_debug(f, key: str) -> str|None: def scan_section_debug(f, key: str) -> str | None:
ret = scan_section(f, key) ret = scan_section(f, key)
#log(DEBUG, ' returning', rr) # log(DEBUG, ' returning', rr)
return ret return ret
try: try:
#log(DEBUG, 'looking for {}::[{}].{}'.format(path, section, key)) # log(DEBUG, 'looking for {}::[{}].{}'.format(path, section, key))
# TODO: Parse
with open(path, 'r') as f: with open(path, 'r') as f:
if not len(section): if not len(section):
rr = scan_section(f, key) return scan_section(f, key)
pat = '[' + section + ']' pat = '[' + section + ']'
for line in f: for line in f:
if line.rstrip() == pat: if line.rstrip() == pat:
return scan_section(f, key) return scan_section(f, key)
return None return None
except: except Exception:
log(DEBUG, path, 'not found') log(DEBUG, f'Not found: {path}')
# TODO: handle this special case cleaner somewhere up the stack # TODO: handle this special case cleaner somewhere up the stack
if section == 'build' and key == 'libname': if section == 'build' and key == 'libname':
return 'none' return 'none'
return None return None
@lru_cache(maxsize=None) @lru_cache(maxsize = None)
def get_value(self, project: str, section: str, key: str) -> str: def get_value(self, project: str, section: str, key: str) -> str | None:
if self.__top_name and project == self.__top_name: ret: str | None
proj_root = self.__topdir proj_dir = self.__proj_dir(project, pretty = False)
else: if proj_dir is None:
proj_root = self.__projs_root + '/' + project raise Exception(f"Can't get project directory for {project}")
if section == 'version': if section == 'version':
proj_version_dirs = [ proj_root ] proj_version_dirs = [proj_dir]
if proj_root != self.__topdir: if proj_dir != self.___topdir:
proj_version_dirs.append('/usr/share/doc/packages/' + project) proj_version_dirs.append('/usr/share/doc/packages/' + project)
for d in proj_version_dirs: for d in proj_version_dirs:
version_path = d + '/VERSION' version_path = d + '/VERSION'
@ -423,13 +582,24 @@ class App(Base):
log(DEBUG, f'Ignoring unreadable file "{version_path}"') log(DEBUG, f'Ignoring unreadable file "{version_path}"')
continue continue
raise Exception(f'No version file found for project "{project}"') raise Exception(f'No version file found for project "{project}"')
path = proj_root + '/make/project.conf' path = proj_dir + '/make/project.conf'
ret = self.read_value(path, section, key) ret = self.read_value(path, section, key)
log(DEBUG, 'Lookup %s -> %s / [%s%s] -> "%s"' % log(
(self.__top_name, project, section, '.' + key if key else '', ret)) DEBUG,
'Lookup %s -> %s / [%s%s] -> "%s"' %
(self.__top_name, project, section, '.' + key if key else '', ret),
)
return ret return ret
def get_values(self, projects: list[str], sections: list[str], keys: list[str]) -> list[str]: @lru_cache(maxsize = None)
def get_version(self, project) -> str:
ret = self.get_value(project, 'version', '')
if ret is None:
raise Exception(f"Can't get version of project {project}")
return ret
def get_values(self, projects: list[str], sections: list[str],
keys: list[str]) -> list[str]:
""" """
Collect a list of values from a list of given projects, sections and Collect a list of values from a list of given projects, sections and
keys, maintaining order keys, maintaining order
@ -441,39 +611,60 @@ class App(Base):
vals = self.get_value(p, section, key) vals = self.get_value(p, section, key)
if vals: if vals:
ret += [val.strip() for val in vals.split(',')] ret += [val.strip() for val in vals.split(',')]
return list(dict.fromkeys(ret)) # Remove duplicates, keep ordering return list(dict.fromkeys(ret)) # Remove duplicates, keep ordering
def get_project_refs(self, projects: list[str], sections: list[str], def get_project_refs(
keys: str|list[str], add_self: bool, scope: Scope, names_only=True) -> list[str]: self,
projects: list[str],
sections: list[str],
keys: str | list[str],
add_self: bool,
scope: Scope,
names_only = True,
) -> list[str]:
if isinstance(keys, str): if isinstance(keys, str):
keys = [ keys ] keys = [keys]
ret: list[str] = [] ret: list[str] = []
for section in sections: for section in sections:
for key in keys: for key in keys:
visited = set() visited: set[str] = set()
for name in projects: for name in projects:
rr: list[str] = [] rr: list[str] = []
self.__get_project_refs_cached(rr, visited, name, section, key, add_self, scope, names_only) self.__get_project_refs_cached(
rr, visited, name, section, key, add_self, scope, names_only
)
# TODO: this looks like a performance hogger # TODO: this looks like a performance hogger
for m in rr: for m in rr:
if not m in ret: if m not in ret:
ret.append(m) ret.append(m)
return ret return ret
def get_libname(self, projects) -> str: def get_libname(self, projects) -> str:
vals = self.get_project_refs(projects, ['build'], 'libname', vals = self.get_project_refs(
scope = Scope.One, add_self=False, names_only=True) projects,
['build'],
'libname',
scope = Scope.One,
add_self = False,
names_only = True,
)
if not vals: if not vals:
return ' '.join(projects) return ' '.join(projects)
if 'none' in vals: if 'none' in vals:
vals.remove('none') vals.remove('none')
return ' '.join(reversed(vals)) return ' '.join(reversed(vals))
def is_excluded_from_build(self, project: str) -> str|None: def is_excluded_from_build(self, project: str) -> str | None:
log(DEBUG, 'checking if project ' + project + ' is excluded from build') log(DEBUG, 'checking if project ' + project + ' is excluded from build')
exclude = self.get_project_refs([ project ], ['build'], 'exclude', exclude = self.get_project_refs(
scope = Scope.One, add_self=False, names_only=True) [project],
cascade = self.distro.os_cascade + [ 'all' ] ['build'],
'exclude',
scope = Scope.One,
add_self = False,
names_only = True,
)
cascade = self.distro.os_cascade + ['all']
for p1 in exclude: for p1 in exclude:
for p2 in cascade: for p2 in cascade:
if p1 == p2: if p1 == p2:

View file

@ -1,15 +1,16 @@
# -*- coding: utf-8 -*-
from __future__ import annotations from __future__ import annotations
from argparse import ArgumentParser from typing import TYPE_CHECKING
from ..App import App from ..CmdBase import CmdBase
from ..lib.Cmd import Cmd as Base
class Cmd(Base): # export if TYPE_CHECKING:
from ..App import App
from ..lib.Distro import Distro
def __init__(self, parent: App|Base, name: str, help: str) -> None: class Cmd(CmdBase): # export
def __init__(self, parent: App | CmdBase, name: str, help: str) -> None:
super().__init__(parent, name, help) super().__init__(parent, name, help)
async def _run(self, args): async def _run(self, args):

View file

@ -1,14 +1,12 @@
# -*- coding: utf-8 -*-
from argparse import ArgumentParser from argparse import ArgumentParser
from ..App import App from ..App import App
from .Cmd import Cmd as CmdBase from .Cmd import Cmd as CmdBase
class CmdPkg(CmdBase): # export class CmdPkg(CmdBase): # export
def __init__(self, parent: App) -> None: def __init__(self, parent: App) -> None:
super().__init__(parent, 'pkg', help="System package manager wrapper") super().__init__(parent, 'pkg', help = 'System package manager wrapper')
self.load_subcommands() self.load_subcommands()
def add_arguments(self, p: ArgumentParser) -> None: def add_arguments(self, p: ArgumentParser) -> None:

View file

@ -1,14 +1,14 @@
# -*- coding: utf-8 -*-
from argparse import ArgumentParser from argparse import ArgumentParser
from ..App import App from ..App import App
from .Cmd import Cmd as CmdBase from .Cmd import Cmd as CmdBase
class CmdPlatform(CmdBase): # export class CmdPlatform(CmdBase): # export
def __init__(self, parent: App) -> None: def __init__(self, parent: App) -> None:
super().__init__(parent, 'platform', help="Miscellaneous platform-related comamnds") super().__init__(
parent, 'platform', help = 'Miscellaneous platform-related comamnds'
)
self.load_subcommands() self.load_subcommands()
def add_arguments(self, p: ArgumentParser) -> None: def add_arguments(self, p: ArgumentParser) -> None:

View file

@ -1,14 +1,19 @@
# -*- coding: utf-8 -*-
from argparse import ArgumentParser from argparse import ArgumentParser
from ..App import App from ..App import App
from .Cmd import Cmd as CmdBase from .Cmd import Cmd as CmdBase
class CmdPosix(CmdBase): # export class CmdPosix(CmdBase): # export
def __init__(self, parent: App) -> None: def __init__(self, parent: App) -> None:
super().__init__(parent, 'posix', help='Perform various operations on a distro through its POSIX utility interface') super().__init__(
parent,
'posix',
help = (
'Perform various operations on a distro through its '
'POSIX utility interface'
),
)
self.load_subcommands() self.load_subcommands()
def add_arguments(self, p: ArgumentParser) -> None: def add_arguments(self, p: ArgumentParser) -> None:

View file

@ -1,15 +1,18 @@
# -*- coding: utf-8 -*-
import sys import sys
from argparse import ArgumentParser from argparse import ArgumentParser
from ..App import App from ..App import App
from .Cmd import Cmd as CmdBase from .Cmd import Cmd as CmdBase
class CmdProjects(CmdBase): # export class CmdProjects(CmdBase): # export
def __init__(self, parent: App) -> None: def __init__(self, parent: App) -> None:
super().__init__(parent, 'projects', help='Project metadata evaluation for building packages') super().__init__(
parent,
'projects',
help = 'Project metadata evaluation for building packages'
)
self.load_subcommands() self.load_subcommands()
def add_arguments(self, p: ArgumentParser) -> None: def add_arguments(self, p: ArgumentParser) -> None:

View file

@ -1,14 +1,12 @@
# -*- coding: utf-8 -*-
from argparse import ArgumentParser from argparse import ArgumentParser
from ..App import App from ..App import App
from .Cmd import Cmd as CmdBase from .Cmd import Cmd as CmdBase
class CmdSecrets(CmdBase): # export class CmdSecrets(CmdBase): # export
def __init__(self, parent: App) -> None: def __init__(self, parent: App) -> None:
super().__init__(parent, 'secrets', help="Manage package secrets") super().__init__(parent, 'secrets', help = 'Manage package secrets')
self.load_subcommands() self.load_subcommands()
def add_arguments(self, p: ArgumentParser) -> None: def add_arguments(self, p: ArgumentParser) -> None:

View file

@ -4,6 +4,6 @@ __all__ = detect_modules(
package_name = __name__, package_name = __name__,
package_path = __path__, package_path = __path__,
namespace = globals(), namespace = globals(),
prefix = "Cmd", prefix = 'Cmd',
skip = {"Cmd"}, skip = {'Cmd'},
) # pyright: ignore[reportUnsupportedDunderAll] ) # pyright: ignore[reportUnsupportedDunderAll]

View file

@ -1,16 +1,13 @@
# -*- coding: utf-8 -*-
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from ...lib.Distro import Distro
from ..CmdPkg import CmdPkg from ..CmdPkg import CmdPkg
from ..Cmd import Cmd as Base from ..Cmd import Cmd as Base
class Cmd(Base): # export class Cmd(Base): # export
def __init__(self, parent: CmdPkg, name: str, help: str) -> None: def __init__(self, parent: CmdPkg, name: str, help: str) -> None:
super().__init__(parent, name, help) super().__init__(parent, name, help)

View file

@ -1,18 +1,18 @@
# -*- coding: utf-8 -*- from argparse import ArgumentParser, Namespace
from argparse import Namespace, ArgumentParser
from .Cmd import Cmd
from ..CmdPkg import CmdPkg from ..CmdPkg import CmdPkg
from .Cmd import Cmd
class CmdDelete(Cmd): # export class CmdDelete(Cmd): # export
def __init__(self, parent: CmdPkg) -> None: def __init__(self, parent: CmdPkg) -> None:
super().__init__(parent, 'delete', help="Delete packages by name") super().__init__(parent, 'delete', help = 'Delete packages by name')
def add_arguments(self, parser: ArgumentParser) -> None: def add_arguments(self, parser: ArgumentParser) -> None:
super().add_arguments(parser) super().add_arguments(parser)
parser.add_argument("names", nargs="*", help="Names of packages to be deleted") parser.add_argument(
'names', nargs = '*', help = 'Names of packages to be deleted'
)
async def _run(self, args: Namespace) -> None: async def _run(self, args: Namespace) -> None:
return await self.distro.delete(args.names) return await self.distro.delete(args.names)

View file

@ -1,18 +1,21 @@
# -*- coding: utf-8 -*- from argparse import ArgumentParser, Namespace
from argparse import Namespace, ArgumentParser
from .Cmd import Cmd
from ..CmdPkg import CmdPkg from ..CmdPkg import CmdPkg
from .Cmd import Cmd
class CmdDup(Cmd): # export class CmdDup(Cmd): # export
def __init__(self, parent: CmdPkg) -> None: def __init__(self, parent: CmdPkg) -> None:
super().__init__(parent, 'dup', help="Upgrade distribution") super().__init__(parent, 'dup', help = 'Upgrade distribution')
def add_arguments(self, parser: ArgumentParser) -> None: def add_arguments(self, parser: ArgumentParser) -> None:
super().add_arguments(parser) super().add_arguments(parser)
parser.add_argument('--download-only', default=False, action='store_true', parser.add_argument(
help='Only download packages from the repos, don\'t install them, yet') '--download-only',
default = False,
action = 'store_true',
help = "Only download packages from the repos, don't install them, yet",
)
async def _run(self, args: Namespace) -> None: async def _run(self, args: Namespace) -> None:
return await self.distro.dup(download_only=args.download_only) return await self.distro.dup(download_only = args.download_only)

View file

@ -1,22 +1,35 @@
# -*- coding: utf-8 -*- from argparse import ArgumentParser, Namespace
from argparse import Namespace, ArgumentParser
from .Cmd import Cmd
from ..CmdPkg import CmdPkg from ..CmdPkg import CmdPkg
from .Cmd import Cmd
class CmdInstall(Cmd): # export class CmdInstall(Cmd): # export
def __init__(self, parent: CmdPkg) -> None: def __init__(self, parent: CmdPkg) -> None:
super().__init__(parent, 'install', help="Install the distribution's notion of available packages") super().__init__(
parent,
'install',
help = "Install the distribution's notion of available packages",
)
def add_arguments(self, parser: ArgumentParser) -> None: def add_arguments(self, parser: ArgumentParser) -> None:
super().add_arguments(parser) super().add_arguments(parser)
parser.add_argument("names", nargs="*", help="Packages to be installed") parser.add_argument('names', nargs = '*', help = 'Packages to be installed')
parser.add_argument('--only-update', default=False, action='store_true', help='Only update the listed packages, don\'t install them') parser.add_argument(
parser.add_argument('-F', '--fixed-strings', action='store_true', '--only-update',
help='Don\'t expand platform.expand_macros macros in <names>') default = False,
action = 'store_true',
help = "Only update the listed packages, don't install them",
)
parser.add_argument(
'-F',
'--fixed-strings',
action = 'store_true',
help = "Don't expand platform.expand_macros macros in <names>",
)
async def _run(self, args: Namespace) -> None: async def _run(self, args: Namespace) -> None:
names = names if args.fixed_strings else self.app.distro.expand_macros(args.names) names = (
return await self.distro.install(names, only_update=args.only_update) args.names if args.fixed_strings else self.distro.expand_macros(args.names)
)
return await self.distro.install(names, only_update = args.only_update)

View file

@ -1,18 +1,16 @@
# -*- coding: utf-8 -*- from argparse import ArgumentParser, Namespace
from argparse import Namespace, ArgumentParser
from .NamedPkgsCmd import NamedPkgsCmd as Base
from ..CmdPkg import CmdPkg from ..CmdPkg import CmdPkg
from .NamedPkgsCmd import NamedPkgsCmd as Base
class CmdLs(Base): # export class CmdLs(Base): # export
def __init__(self, parent: CmdPkg) -> None: def __init__(self, parent: CmdPkg) -> None:
super().__init__(parent, 'ls', help="List package contents") super().__init__(parent, 'ls', help = 'List package contents')
def add_arguments(self, parser: ArgumentParser) -> None: def add_arguments(self, parser: ArgumentParser) -> None:
super().add_arguments(parser) super().add_arguments(parser)
async def _run(self, args: Namespace) -> None: async def _run(self, args: Namespace) -> None:
for name in args.names: for name in args.names:
print('\n'.join(await self.parent.distro.pkg_files(name))) print('\n'.join(await self.distro.pkg_files(name)))

View file

@ -1,21 +1,19 @@
# -*- coding: utf-8 -*- from argparse import ArgumentParser, Namespace
from argparse import Namespace, ArgumentParser
from .NamedPkgsCmd import NamedPkgsCmd as Base
from ..CmdPkg import CmdPkg from ..CmdPkg import CmdPkg
from .NamedPkgsCmd import NamedPkgsCmd as Base
class CmdMeta(Base): # export class CmdMeta(Base): # export
def __init__(self, parent: CmdPkg) -> None: def __init__(self, parent: CmdPkg) -> None:
super().__init__(parent, 'meta', help="List package metadata") super().__init__(parent, 'meta', help = 'List package metadata')
def add_arguments(self, parser: ArgumentParser) -> None: def add_arguments(self, parser: ArgumentParser) -> None:
super().add_arguments(parser) super().add_arguments(parser)
async def _run(self, args: Namespace) -> None: async def _run(self, args: Namespace) -> None:
packages = await self.distro.select(args.names) names = await self.distro.select(args.names)
for package in packages: for name in names:
if len(args.names) > 1: if len(args.names) > 1:
print(f'-- {name}') print(f'-- {name}')
print(package) print(name)

View file

@ -1,17 +1,19 @@
# -*- coding: utf-8 -*- from argparse import ArgumentParser, Namespace
from argparse import Namespace, ArgumentParser
from .Cmd import Cmd
from ..CmdPkg import CmdPkg from ..CmdPkg import CmdPkg
from .Cmd import Cmd
class CmdRebootRequired(Cmd): # export class CmdRebootRequired(Cmd): # export
def __init__(self, parent: CmdPkg) -> None: def __init__(self, parent: CmdPkg) -> None:
super().__init__(parent, 'reboot-required', help="Check whether the machine needs rebooting") super().__init__(
parent,
'reboot-required',
help = 'Check whether the machine needs rebooting'
)
def add_arguments(self, parser: ArgumentParser) -> None: def add_arguments(self, parser: ArgumentParser) -> None:
super().add_arguments(parser) super().add_arguments(parser)
async def _run(self, args: Namespace) -> None: async def _run(self, args: Namespace) -> None:
return await self.distro.reboot_required() await self.distro.reboot_required()

View file

@ -1,14 +1,16 @@
# -*- coding: utf-8 -*- from argparse import ArgumentParser, Namespace
from argparse import Namespace, ArgumentParser
from .Cmd import Cmd
from ..CmdPkg import CmdPkg from ..CmdPkg import CmdPkg
from .Cmd import Cmd
class CmdRefresh(Cmd): # export class CmdRefresh(Cmd): # export
def __init__(self, parent: CmdPkg) -> None: def __init__(self, parent: CmdPkg) -> None:
super().__init__(parent, 'refresh', help="Refresh the distribution's notion of available packages") super().__init__(
parent,
'refresh',
help = "Refresh the distribution's notion of available packages",
)
def add_arguments(self, parser: ArgumentParser) -> None: def add_arguments(self, parser: ArgumentParser) -> None:
super().add_arguments(parser) super().add_arguments(parser)

View file

@ -1,23 +1,19 @@
# -*- coding: utf-8 -*- from argparse import ArgumentParser, Namespace
from argparse import Namespace, ArgumentParser
import re
from ...lib.Package import Package
from ...lib.PackageFilter import PackageFilterString from ...lib.PackageFilter import PackageFilterString
from ..CmdPkg import CmdPkg from ..CmdPkg import CmdPkg
from .Cmd import Cmd from .Cmd import Cmd
class CmdSelect(Cmd): # export class CmdSelect(Cmd): # export
def __init__(self, parent: CmdPkg) -> None: def __init__(self, parent: CmdPkg) -> None:
super().__init__(parent, 'select', help="Select packages by filter") super().__init__(parent, 'select', help = 'Select packages by filter')
def add_arguments(self, parser: ArgumentParser) -> None: def add_arguments(self, parser: ArgumentParser) -> None:
super().add_arguments(parser) super().add_arguments(parser)
parser.add_argument("filter", help="Package filter string") parser.add_argument('filter', help = 'Package filter string')
async def _run(self, args: Namespace) -> None: async def _run(self, args: Namespace) -> None:
filter = PackageFilterString(args.filter) if args.filter else None filter = PackageFilterString(args.filter) if args.filter else None
for p in await self.distro.select(filter=filter): for p in await self.distro.select(filter = filter):
print(p.name) print(p.name)

View file

@ -1,15 +1,13 @@
# -*- coding: utf-8 -*- from argparse import ArgumentParser
from argparse import Namespace, ArgumentParser
from .Cmd import Cmd as Base
from ..CmdPkg import CmdPkg as Parent from ..CmdPkg import CmdPkg as Parent
from .Cmd import Cmd as Base
class NamedPkgsCmd(Base): # export class NamedPkgsCmd(Base): # export
def __init__(self, parent: Parent, name: str, help: str) -> None: def __init__(self, parent: Parent, name: str, help: str) -> None:
super().__init__(parent, name, help) super().__init__(parent, name, help)
def add_arguments(self, parser: ArgumentParser) -> None: def add_arguments(self, parser: ArgumentParser) -> None:
super().add_arguments(parser) super().add_arguments(parser)
parser.add_argument('names', nargs='*', help='Package names') parser.add_argument('names', nargs = '*', help = 'Package names')

Some files were not shown because too many files have changed in this diff Show more