diff --git a/Makefile b/Makefile index 64c1ed8..579d8d6 100644 --- a/Makefile +++ b/Makefile @@ -35,12 +35,14 @@ help: .venv-build/bin/activate: requirements.build.txt @echo "Setting up development virtual env in .venv" + set -e; \ python -m venv .venv-build; \ . .venv-build/bin/activate; \ python -m pip install -r requirements.build.txt .venv-runtime/bin/activate: requirements.txt @echo "Setting up development virtual env in .venv" + set -e; \ python -m venv .venv-runtime; \ . .venv-runtime/bin/activate; \ python -m pip install -r requirements.txt @@ -49,27 +51,33 @@ clean: git clean --force -dX build: .venv-build/bin/activate + set -e; \ . .venv-build/bin/activate; \ python -m build standalone: .venv-build/bin/activate + set -e; \ . .venv-build/bin/activate; \ python -m PyInstaller --distpath . toltecmk.spec test: .venv-runtime/bin/activate + set -e; \ . .venv-runtime/bin/activate; \ python -m unittest; \ tests/foobar/test.sh format: .venv-build/bin/activate + set -e; \ . .venv-build/bin/activate; \ black --line-length 80 --check --diff toltec tests format-fix: .venv-build/bin/activate + set -e; \ . .venv-build/bin/activate; \ black --line-length 80 toltec tests lint: .venv-build/bin/activate + set -e; \ . .venv-build/bin/activate; \ echo "==> Typechecking files"; \ mypy --disallow-untyped-defs toltec; \ diff --git a/tests/fixtures/rmkit/package b/tests/fixtures/rmkit/package index 390971a..50f297b 100644 --- a/tests/fixtures/rmkit/package +++ b/tests/fixtures/rmkit/package @@ -33,7 +33,7 @@ prepare() { build() { pip3 install okp - make + make -j$(nporc) } bufshot() { diff --git a/tests/foobar/test.sh b/tests/foobar/test.sh index bd1c89d..ba426ae 100755 --- a/tests/foobar/test.sh +++ b/tests/foobar/test.sh @@ -27,7 +27,7 @@ python -m toltec \ --arch-name rmall \ $(dirname $0) -tree $tmpdir/dist +tree -pug $tmpdir/dist exists $tmpdir/dist/rmall/foo_0.0.0-1_rmall.ipk missing $tmpdir/dist/rm1/foo_0.0.0-1_rm1.ipk missing $tmpdir/dist/rm2/foo_0.0.0-1_rm2.ipk diff --git a/tests/test_toltec.py b/tests/test_toltec.py index d43bbb5..cfaaf0c 100644 --- a/tests/test_toltec.py +++ b/tests/test_toltec.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: MIT from os import path +import os import unittest from tempfile import TemporaryDirectory import subprocess diff --git a/toltec/__main__.py b/toltec/__main__.py index 16e5c5e..7eca09e 100644 --- a/toltec/__main__.py +++ b/toltec/__main__.py @@ -6,7 +6,6 @@ import os import sys from importlib.util import find_spec, spec_from_file_location, module_from_spec -from typing import Dict, List, Optional from toltec import parse_recipe from toltec.builder import Builder from toltec.recipe import Package @@ -97,7 +96,7 @@ def main() -> int: # pylint:disable=too-many-branches f"Hook module '{ident}' couldn’t be loaded" ) - build_matrix: Optional[Dict[str, Optional[List[Package]]]] = None + build_matrix: dict[str, list[Package] | None] | None = None if args.arch_name or args.package_name: build_matrix = {} diff --git a/toltec/bash.py b/toltec/bash.py index c6a6727..800ab2a 100644 --- a/toltec/bash.py +++ b/toltec/bash.py @@ -7,15 +7,15 @@ import subprocess import logging from collections import deque -from typing import Deque, Dict, Generator, List, Optional, Tuple, Union +from collections.abc import Generator from docker.client import DockerClient -AssociativeArray = Dict[str, str] -IndexedArray = List[Optional[str]] +AssociativeArray = dict[str, str] +IndexedArray = list[str | None] LogGenerator = Generator[str, None, None] -Any = Union[str, AssociativeArray, IndexedArray] -Variables = Dict[str, Optional[Any]] -Functions = Dict[str, str] +Any = str | AssociativeArray | IndexedArray +Variables = dict[str, Any | None] +Functions = dict[str, str] class ScriptError(Exception): @@ -36,6 +36,7 @@ class ScriptError(Exception): "BASH_COMMAND", "BASH_LINENO", "BASH_LOADABLES_PATH", + "BASH_MONOSECONDS", "BASH_SOURCE", "BASH_SUBSHELL", "BASH_VERSINFO", @@ -90,7 +91,7 @@ def _get_bash_stdout(src: str) -> str: :param src: bash script to run :returns: the stdout of the script """ - env: Dict[str, str] = { + env: dict[str, str] = { "PATH": os.environ["PATH"], } @@ -112,7 +113,7 @@ def _get_bash_stdout(src: str) -> str: return subshell.stdout.decode() -def get_declarations(src: str) -> Tuple[Variables, Functions]: +def get_declarations(src: str) -> tuple[Variables, Functions]: """ Extract all variables and functions defined by a Bash script. @@ -225,7 +226,7 @@ def _generate_string(string: str) -> str: def _parse_indexed(lexer: shlex.shlex) -> IndexedArray: """Parse an indexed Bash array.""" assert lexer.get_token() == "(" - result: List[Optional[str]] = [] + result: list[str | None] = [] while True: token = lexer.get_token() @@ -304,7 +305,7 @@ def _generate_assoc(array: AssociativeArray) -> str: ) -def _parse_var(lexer: shlex.shlex) -> Tuple[str, Optional[Any]]: +def _parse_var(lexer: shlex.shlex) -> tuple[str, Any | None]: """Parse a variable declaration.""" flags_token = lexer.get_token() assert flags_token is not None @@ -315,7 +316,7 @@ def _parse_var(lexer: shlex.shlex) -> Tuple[str, Optional[Any]]: var_flags = set() var_name = lexer.get_token() - var_value: Optional[Any] = None + var_value: Any | None = None lookahead = lexer.get_token() assert var_name is not None assert lookahead is not None @@ -339,7 +340,7 @@ def _parse_var(lexer: shlex.shlex) -> Tuple[str, Optional[Any]]: return var_name, var_value -def _parse_func(lexer: shlex.shlex) -> Tuple[int, int]: +def _parse_func(lexer: shlex.shlex) -> tuple[int, int]: """Find the starting and end bounds of a function declaration.""" assert lexer.get_token() == "{" brace_depth = 1 @@ -402,7 +403,7 @@ def run_script(variables: Variables, script: str) -> LogGenerator: def run_script_in_container( docker: DockerClient, image: str, - mounts: List, + mounts: list, variables: Variables, script: str, ) -> LogGenerator: @@ -469,7 +470,7 @@ def pipe_logs( :param max_lines_on_fail: number of context lines to print in non-debug mode """ - log_buffer: Deque[str] = deque() + log_buffer: deque[str] = deque() try: for line in logs: diff --git a/toltec/builder.py b/toltec/builder.py index aef1e50..b49f6ab 100644 --- a/toltec/builder.py +++ b/toltec/builder.py @@ -3,7 +3,7 @@ """Build recipes and create packages.""" import shutil -from typing import List, Mapping, Optional, Type +from collections.abc import Mapping from types import TracebackType import re import os @@ -40,11 +40,11 @@ def __init__(self, work_dir: str, dist_dir: str) -> None: :param work_dir: directory where packages are built :param dist_dir: directory where built packages are stored """ - self.work_dir = work_dir - self.dist_dir = dist_dir + self.work_dir: str = work_dir + self.dist_dir: str = dist_dir try: - self.docker = docker.from_env() + self.docker: docker.DockerClient = docker.from_env() except docker.errors.DockerException as err: raise BuildError( "Unable to connect to the Docker daemon. \ @@ -56,6 +56,7 @@ def __init__(self, work_dir: str, dist_dir: str) -> None: spec = find_spec(f"toltec.hooks.{hook}") if spec: module = module_from_spec(spec) + assert spec.loader is not None spec.loader.exec_module(module) # type: ignore module.register(self) # type: ignore else: @@ -68,9 +69,9 @@ def __enter__(self) -> "Builder": def __exit__( self, - exc_type: Optional[Type[BaseException]], - exc_value: Optional[BaseException], - traceback: Optional[TracebackType], + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, ) -> None: self.docker.close() @@ -135,7 +136,7 @@ def post_archive(self, package: Package, ar_path: str) -> None: def make( self, recipe_bundle: RecipeBundle, - build_matrix: Optional[Mapping[str, Optional[List[Package]]]] = None, + build_matrix: Mapping[str, list[Package] | None] | None = None, check_directory: bool = True, ) -> bool: """ @@ -175,7 +176,7 @@ def _make_arch( self, recipe: Recipe, build_dir: str, - packages: Optional[List[Package]] = None, + packages: list[Package] | None = None, ) -> bool: self.post_parse(recipe) @@ -226,7 +227,9 @@ def _fetch_sources( if self.URL_REGEX.match(source.url) is None: # Get source file from the recipe’s directory - shutil.copy2(os.path.join(recipe.path, source.url), local_path) + _ = shutil.copy2( + os.path.join(recipe.path, source.url), local_path + ) else: # Fetch source file from the network req = requests.get(source.url, timeout=(3.05, 300)) @@ -239,15 +242,15 @@ def _fetch_sources( with open(local_path, "wb") as local: for chunk in req.iter_content(chunk_size=1024): - local.write(chunk) + _ = local.write(chunk) # Verify checksum file_sha = util.file_sha256(local_path) if source.checksum not in ("SKIP", file_sha): raise BuildError( f"Invalid checksum for source file {source.url}:\n" - f" expected {source.checksum}\n" - f" actual {file_sha}" + + f" expected {source.checksum}\n" + + f" actual {file_sha}" ) # Automatically extract source archives @@ -258,19 +261,36 @@ def _fetch_sources( local_path, ) - @staticmethod - def _prepare(recipe: Recipe, src_dir: str) -> None: + def _prepare(self, recipe: Recipe, src_dir: str) -> None: """Prepare source files before building.""" if not recipe.prepare: logger.debug("Skipping prepare (nothing to do)") return logger.info("Preparing source files") - logs = bash.run_script( - script=recipe.prepare, + mount_src = "/src" + uid = os.getuid() + gid = os.getgid() + logs = bash.run_script_in_container( + self.docker, + image=self.IMAGE_PREFIX + "toolchain:v4.0", + mounts=[ + docker.types.Mount( + type="bind", + source=os.path.abspath(src_dir), + target=mount_src, + ), + ], variables={ - "srcdir": src_dir, + "srcdir": mount_src, }, + script="\n".join( + [ + f'cd "{mount_src}"', + recipe.prepare, + f"chown -R {uid}:{gid} {mount_src}", + ] + ), ) bash.pipe_logs(logger, logs, "prepare()") @@ -292,11 +312,12 @@ def _build(self, recipe: Recipe, src_dir: str) -> None: mount_src = "/src" repo_src = "/repo" uid = os.getuid() - pre_script: List[str] = [] + gid = os.getgid() + pre_script: list[str] = [] # Install required dependencies - build_deps = [] - host_deps = [] + build_deps: list[str] = [] + host_deps: list[str] = [] for dep in recipe.makedepends: if dep.kind == DependencyKind.BUILD: @@ -310,9 +331,10 @@ def _build(self, recipe: Recipe, src_dir: str) -> None: "export DEBIAN_FRONTEND=noninteractive", "apt-get update -qq", "apt-get install -qq --no-install-recommends" - ' -o Dpkg::Options::="--force-confdef"' - ' -o Dpkg::Options::="--force-confold"' - " -- " + " ".join(build_deps), + + ' -o Dpkg::Options::="--force-confdef"' + + ' -o Dpkg::Options::="--force-confold"' + + " -- " + + " ".join(build_deps), ) ) @@ -361,7 +383,8 @@ def _build(self, recipe: Recipe, src_dir: str) -> None: ( f"{opkg_exec} update --verbosity=0", f"{opkg_exec} install --verbosity=0 --no-install-recommends" - " -- " + " ".join(host_deps), + + " -- " + + " ".join(host_deps), ) ) @@ -391,22 +414,21 @@ def _build(self, recipe: Recipe, src_dir: str) -> None: *pre_script, f'cd "{mount_src}"', recipe.build, - f'chown -R {uid}:{uid} "{mount_src}"', + f"chown -R {uid}:{gid} {mount_src} {repo_src}", ) ), ) bash.pipe_logs(logger, logs, "build()") - @staticmethod - def _package(package: Package, src_dir: str, pkg_dir: str) -> None: + def _package(self, package: Package, src_dir: str, pkg_dir: str) -> None: """Make a package from a recipe’s build artifacts.""" logger.info("Packaging build artifacts for %s", package.name) logs = bash.run_script( - script=package.package, variables={ "srcdir": src_dir, "pkgdir": pkg_dir, }, + script=package.package, ) bash.pipe_logs(logger, logs, "package()") @@ -426,7 +448,7 @@ def _archive(package: Package, pkg_dir: str, ar_path: str) -> None: logger.info("Creating archive %s", package.filename()) # Convert install scripts to Debian format - scripts = {} + scripts: dict[str, str] = {} script_header = textwrap.dedent( """\ #!/usr/bin/env bash diff --git a/toltec/hooks/install_lib.py b/toltec/hooks/install_lib.py index 77d91a3..d11c05a 100644 --- a/toltec/hooks/install_lib.py +++ b/toltec/hooks/install_lib.py @@ -8,7 +8,7 @@ import inspect import logging -from typing import Set, Iterable +from collections.abc import Iterable from toltec.builder import Builder from toltec.recipe import Package from toltec.util import listener @@ -45,7 +45,7 @@ def post_package( "preupgrade", ): function = getattr(package, name) - methods: Set[str] = set() + methods: set[str] = set() for method in METHODS: # pylint: disable=consider-using-dict-items if method in function: methods.add(method) diff --git a/toltec/hooks/patch_rm2fb.py b/toltec/hooks/patch_rm2fb.py index a6edc3d..81c6e4c 100644 --- a/toltec/hooks/patch_rm2fb.py +++ b/toltec/hooks/patch_rm2fb.py @@ -16,7 +16,12 @@ from toltec.builder import Builder from toltec.recipe import Recipe from toltec.util import listener -from toltec.hooks.strip import walk_elfs, run_in_container, MOUNT_SRC +from toltec.hooks.strip import ( + walk_elfs, + restore_mtime_script, + run_in_container, + MOUNT_SRC, +) logger = logging.getLogger(__name__) @@ -68,15 +73,14 @@ def docker_file_path(file_path: str) -> str: ) for file_path in binaries: - original_mtime[file_path] = os.stat(file_path).st_mtime_ns + original_mtime[docker_file_path(file_path)] = os.stat( + file_path + ).st_mtime_ns script.append( "patchelf --add-needed librm2fb_client.so.1 " + " ".join(docker_file_path(file_path) for file_path in binaries) ) + script += restore_mtime_script(original_mtime) run_in_container(builder, src_dir, logger, script) - - # Restore original mtimes - for file_path, mtime in original_mtime.items(): - os.utime(file_path, ns=(mtime, mtime)) diff --git a/toltec/hooks/strip.py b/toltec/hooks/strip.py index 265d08e..dca1dd3 100644 --- a/toltec/hooks/strip.py +++ b/toltec/hooks/strip.py @@ -9,7 +9,7 @@ import os import logging import shlex -from typing import Callable, List +from typing import Callable import docker from elftools.elf.elffile import ELFFile, ELFError from toltec import bash @@ -20,7 +20,7 @@ logger = logging.getLogger(__name__) MOUNT_SRC = "/src" -TOOLCHAIN = "toolchain:v4.0" +TOOLCHAIN = "python:v4.0" def walk_elfs(src_dir: str, for_each: Callable) -> None: @@ -41,7 +41,7 @@ def walk_elfs(src_dir: str, for_each: Callable) -> None: def run_in_container( - builder: Builder, src_dir: str, _logger: logging.Logger, script: List[str] + builder: Builder, src_dir: str, _logger: logging.Logger, script: list[str] ) -> None: """Run a script in a container and log output""" logs = bash.run_script_in_container( @@ -60,6 +60,19 @@ def run_in_container( bash.pipe_logs(_logger, logs) +def restore_mtime_script(original_mtime: dict[str, int]) -> list[str]: + """Restore original mtimes for files after they have been modified""" + script: list[str] = [] + for file_path, mtime in original_mtime.items(): + script.append( + 'echo "import os; os.utime(' + + f'\\"{file_path}\\", ns=({mtime}, {mtime})' + + ')" | python3 -u' + ) + + return script + + def register(builder: Builder) -> None: """Register the hook""" @@ -72,9 +85,9 @@ def post_build( # pylint: disable=too-many-locals,too-many-branches return # Search for binary objects that can be stripped - strip_arm: List[str] = [] - strip_aarch64: List[str] = [] - strip_x86: List[str] = [] + strip_arm: list[str] = [] + strip_aarch64: list[str] = [] + strip_x86: list[str] = [] def filter_elfs(info: ELFFile, file_path: str) -> None: symtab = info.get_section_by_name(".symtab") @@ -96,19 +109,21 @@ def filter_elfs(info: ELFFile, file_path: str) -> None: # Save original mtimes to restore them afterwards # This will prevent any Makefile rules to be triggered again # in packaging scripts that use `make install` - original_mtime = {} - - for file_path in strip_arm + strip_x86: - original_mtime[file_path] = os.stat(file_path).st_mtime_ns - - # Run strip on found binaries - script = [] + original_mtime: dict[str, int] = {} def docker_file_path(file_path: str) -> str: return shlex.quote( os.path.join(MOUNT_SRC, os.path.relpath(file_path, src_dir)) ) + for file_path in strip_arm + strip_x86: + original_mtime[docker_file_path(file_path)] = os.stat( + file_path + ).st_mtime_ns + + # Run strip on found binaries + script: list[str] = [] + # Strip debugging symbols and unneeded sections if strip_x86: script.append( @@ -162,8 +177,5 @@ def docker_file_path(file_path: str) -> str: os.path.relpath(file_path, src_dir), ) + script += restore_mtime_script(original_mtime) run_in_container(builder, src_dir, logger, script) - - # Restore original mtimes - for file_path, mtime in original_mtime.items(): - os.utime(file_path, ns=(mtime, mtime)) diff --git a/toltec/ipk.py b/toltec/ipk.py index a911c59..67d8938 100644 --- a/toltec/ipk.py +++ b/toltec/ipk.py @@ -3,7 +3,7 @@ """Read and write ipk packages.""" from gzip import GzipFile -from typing import Dict, IO, Optional, Type, Union +from typing import IO from types import TracebackType from io import BytesIO import tarfile @@ -35,7 +35,7 @@ def _targz_open(fileobj: IO[bytes], epoch: int) -> tarfile.TarFile: def _clean_info( - root: Optional[str], epoch: int, info: tarfile.TarInfo + root: str | None, epoch: int, info: tarfile.TarInfo ) -> tarfile.TarInfo: """ Remove variable data from an archive entry. @@ -81,7 +81,7 @@ def _add_file( def write_control( - file: IO[bytes], epoch: int, metadata: str, scripts: Dict[str, str] + file: IO[bytes], epoch: int, metadata: str, scripts: dict[str, str] ) -> None: """ Create the control sub-archive of an ipk package. @@ -109,7 +109,7 @@ def write_control( def write_data( file: IO[bytes], epoch: int, - pkg_dir: Optional[str] = None, + pkg_dir: str | None = None, ) -> None: """ Create the data sub-archive of an ipk package. @@ -130,8 +130,8 @@ def write( file: IO[bytes], epoch: int, metadata: str, - scripts: Dict[str, str], - pkg_dir: Optional[str] = None, + scripts: dict[str, str], + pkg_dir: str | None = None, ) -> None: """ Create an ipk package. @@ -164,14 +164,14 @@ def write( class Reader: """Read from ipk packages.""" - def __init__(self, file: Union[str, IO[bytes]]): + def __init__(self, file: str | IO[bytes]): """ Create a package reader. :param file: path to the package file to read, or opened file object for a package file (in the second case, the package file object will not by closed on exit) """ - self._file: Optional[IO[bytes]] = None + self._file: IO[bytes] | None = None if isinstance(file, str): self._file = open(file, "rb") # pylint:disable=consider-using-with @@ -180,12 +180,12 @@ def __init__(self, file: Union[str, IO[bytes]]): self._file = file self._close = False - self._root_archive: Optional[tarfile.TarFile] = None - self._data_file: Optional[IO[bytes]] = None + self._root_archive: tarfile.TarFile | None = None + self._data_file: IO[bytes] | None = None - self.data: Optional[tarfile.TarFile] = None - self.metadata: Optional[str] = None - self.scripts: Dict[str, str] = {} + self.data: tarfile.TarFile | None = None + self.metadata: str | None = None + self.scripts: dict[str, str] = {} def __enter__(self) -> "Reader": """Load package data to memory.""" @@ -216,9 +216,9 @@ def __enter__(self) -> "Reader": def __exit__( self, - exc_type: Optional[Type[BaseException]], - exc_inst: Optional[BaseException], - traceback: Optional[TracebackType], + exc_type: type[BaseException] | None, + exc_inst: BaseException | None, + traceback: TracebackType | None, ) -> None: """Free resources containing package data.""" if self.data is not None: diff --git a/toltec/recipe.py b/toltec/recipe.py index e2f1367..361feae 100644 --- a/toltec/recipe.py +++ b/toltec/recipe.py @@ -10,7 +10,7 @@ from dataclasses import dataclass from datetime import datetime -from typing import Dict, List, NamedTuple, Set +from typing import NamedTuple import os import textwrap from .version import Version, Dependency @@ -37,7 +37,7 @@ class RecipeWarning(Warning): # Set of variations of the same recipes that target different architectures -RecipeBundle = Dict[str, "Recipe"] +RecipeBundle = dict[str, "Recipe"] class Source(NamedTuple): @@ -64,10 +64,10 @@ class Recipe: # pylint: disable=too-many-instance-attributes timestamp: datetime # Set of source items to be downloaded - sources: Set[Source] + sources: set[Source] # Set of packages that are needed to build this recipe - makedepends: Set[Dependency] + makedepends: set[Dependency] # Full name and email address of this recipe’s maintainer maintainer: str @@ -79,7 +79,7 @@ class Recipe: # pylint: disable=too-many-instance-attributes arch: str # Set of flags to be used by the build system - flags: List[str] + flags: list[str] # Bash script for preparing (patching, moving) source files before build prepare: str @@ -88,7 +88,7 @@ class Recipe: # pylint: disable=too-many-instance-attributes build: str # Set of packages to generate from the build artifacts - packages: Dict[str, "Package"] + packages: dict[str, "Package"] @dataclass @@ -117,22 +117,22 @@ class Package: # pylint: disable=too-many-instance-attributes license: str # Set of packages that must be installed for this package to work - installdepends: Set[Dependency] + installdepends: set[Dependency] # Set of packages that this package recommends installing - recommends: Set[Dependency] + recommends: set[Dependency] # Set of packages that provide additional features for this package - optdepends: Set[Dependency] + optdepends: set[Dependency] # Set of incompatible packages - conflicts: Set[Dependency] + conflicts: set[Dependency] # Set of packages replaced by this package - replaces: Set[Dependency] + replaces: set[Dependency] # Set of packages that this package provides - provides: Set[Dependency] + provides: set[Dependency] # Bash script for packaging build artifacts package: str diff --git a/toltec/recipe_parsers/bash.py b/toltec/recipe_parsers/bash.py index d6793ad..0547f0e 100644 --- a/toltec/recipe_parsers/bash.py +++ b/toltec/recipe_parsers/bash.py @@ -5,7 +5,8 @@ import copy import warnings from itertools import product -from typing import Any, Dict, Generator, Iterable, Optional, Tuple +from typing import Any +from collections.abc import Generator, Iterable import os import dateutil.parser from ..version import ( @@ -50,7 +51,7 @@ def _instantiate_arch( path: str, variables: bash.Variables, functions: bash.Functions, -) -> Generator[Tuple[str, bash.Variables, bash.Functions], None, None]: +) -> Generator[tuple[str, bash.Variables, bash.Functions], None, None]: """ Instantiate a recipe definition for each supported architecture. @@ -130,7 +131,7 @@ def _parse_recipe( # pylint: disable=too-many-locals, disable=too-many-statemen :raises RecipeError: if the recipe contains an error :returns: loaded recipe """ - attrs: Dict[str, Any] = {} + attrs: dict[str, Any] = {} attrs["path"] = path raw_vars: bash.Variables = {} @@ -285,7 +286,7 @@ def _parse_package( # pylint: disable=too-many-locals, disable=too-many-stateme :param functions: functions declared in the package :raises RecipeError: if the package contains an error """ - attrs: Dict[str, Any] = {} + attrs: dict[str, Any] = {} attrs["parent"] = parent # Parse fields @@ -397,7 +398,7 @@ def _pop_field_string( path: str, variables: bash.Variables, name: str, - default: Optional[str] = None, + default: str | None = None, ) -> str: if name not in variables: if default is None: @@ -419,7 +420,7 @@ def _pop_field_indexed( path: str, variables: bash.Variables, name: str, - default: Optional[bash.IndexedArray] = None, + default: bash.IndexedArray | None = None, ) -> bash.IndexedArray: if name not in variables: if default is None: diff --git a/toltec/util.py b/toltec/util.py index a6aa515..98a5474 100644 --- a/toltec/util.py +++ b/toltec/util.py @@ -15,13 +15,8 @@ Any, Callable, cast, - Dict, IO, - List, - Optional, Protocol, - Type, - Union, ) import warnings import zipfile @@ -65,11 +60,11 @@ def setup_logging(args: argparse.Namespace) -> None: logging.basicConfig(format=LOGGING_FORMAT, level=args.verbose) def formatwarning( - message: Union[str, Warning], - category: Type[Warning], + message: str | Warning, + category: type[Warning], filename: str, lineno: int, - line: Optional[str] = None, + line: str | None = None, ) -> str: del filename, lineno, line return f"[{category.__name__}] {message}" @@ -94,7 +89,7 @@ def file_sha256(path: str) -> str: return sha256.hexdigest() -def split_all_parts(path: str) -> List[str]: +def split_all_parts(path: str) -> list[str]: """Split a file path into all its directory components.""" parts = [] prefix = path @@ -108,7 +103,7 @@ def split_all_parts(path: str) -> List[str]: return parts -def split_all_exts(path: str) -> List[str]: +def split_all_exts(path: str) -> list[str]: """Get the list of extensions in a file path.""" exts = [] remaining = path @@ -131,7 +126,7 @@ def all_equal(seq: Iterable) -> bool: return first and not second -def remove_prefix(filenames: List[str]) -> Dict[str, str]: +def remove_prefix(filenames: list[str]) -> dict[str, str]: """Find and remove the longest directory prefix shared by all files.""" split_filenames = [split_all_parts(filename) for filename in filenames] @@ -204,9 +199,9 @@ def auto_extract(archive_path: str, dest_path: str) -> bool: def _auto_extract( # pylint:disable=too-many-arguments,disable=too-many-locals,disable=too-many-positional-arguments - members: List[str], + members: list[str], getinfo: Callable[[str], Any], - extract: Callable[[Any], Optional[IO[bytes]]], + extract: Callable[[Any], IO[bytes] | None], isdir: Callable[[Any], bool], issym: Callable[[Any], bool], getmode: Callable[[Any], int], @@ -253,8 +248,8 @@ def _auto_extract( # pylint:disable=too-many-arguments,disable=too-many-locals, def query_user( question: str, default: str, - options: Optional[List[str]] = None, - aliases: Optional[Dict[str, str]] = None, + options: list[str] | None = None, + aliases: dict[str, str] | None = None, ) -> str: """ Ask the user to make a choice. @@ -323,7 +318,7 @@ def check_directory(path: str, message: str) -> bool: return True -def list_tree(root: str) -> List[str]: +def list_tree(root: str) -> list[str]: """ Get a sorted list of all files and folders under a given root folder. @@ -362,7 +357,7 @@ def hook(func: HookTrigger) -> Hook: :param func: empty function to declare as a hook :returns: usable hook """ - listeners: List[HookListener] = [] + listeners: list[HookListener] = [] def register(new_listener: HookListener) -> None: listeners.append(new_listener) diff --git a/toltec/version.py b/toltec/version.py index 8624413..c54a4ef 100644 --- a/toltec/version.py +++ b/toltec/version.py @@ -13,7 +13,7 @@ import re from functools import total_ordering from enum import Enum -from typing import Optional, Callable +from typing import Callable # Characters permitted in the upstream part of a version number _UPSTREAM_CHARS = "A-Za-z0-9.+~-" @@ -142,8 +142,7 @@ def __init__(self, epoch: int, upstream: str, revision: str): if epoch < 0: raise InvalidVersionError( - f"Invalid epoch '{epoch}', only non-negative values " - "are allowed" + f"Invalid epoch '{epoch}', only non-negative values are allowed" ) if not upstream: @@ -164,7 +163,7 @@ def __init__(self, epoch: int, upstream: str, revision: str): f"are {_REVISION_CHARS}" ) - self._original: Optional[str] = None + self._original: str | None = None @staticmethod def parse(version: str) -> "Version": @@ -278,14 +277,14 @@ def __init__( kind: DependencyKind, package: str, version_comparator: VersionComparator = VersionComparator.EQUAL, - version: Optional[Version] = None, + version: Version | None = None, ): self.kind = kind self.package = package self.version_comparator = version_comparator self.version = version - self._original: Optional[str] = None + self._original: str | None = None @staticmethod def parse(dependency: str) -> "Dependency":