diff --git a/src/pkgmgr/__init__.py b/src/pkgmgr/__init__.py index 427934d..9b06509 100644 --- a/src/pkgmgr/__init__.py +++ b/src/pkgmgr/__init__.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - """ Top-level pkgmgr package. diff --git a/src/pkgmgr/actions/archive/workflow.py b/src/pkgmgr/actions/archive/workflow.py index 0c1b72b..8616523 100644 --- a/src/pkgmgr/actions/archive/workflow.py +++ b/src/pkgmgr/actions/archive/workflow.py @@ -2,6 +2,7 @@ from __future__ import annotations +import contextlib from dataclasses import dataclass from pathlib import Path diff --git a/src/pkgmgr/actions/branch/close_branch.py b/src/pkgmgr/actions/branch/close_branch.py index d9b8b39..3ed8197 100644 --- a/src/pkgmgr/actions/branch/close_branch.py +++ b/src/pkgmgr/actions/branch/close_branch.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import Optional - from pkgmgr.core.git.commands import ( GitDeleteRemoteBranchError, checkout, @@ -17,7 +15,7 @@ from pkgmgr.core.git.queries import get_current_branch, resolve_base_branch def close_branch( - name: Optional[str], + name: str | None, base_branch: str = "main", fallback_base: str = "master", cwd: str = ".", diff --git a/src/pkgmgr/actions/branch/drop_branch.py b/src/pkgmgr/actions/branch/drop_branch.py index 9010233..e75fa00 100644 --- a/src/pkgmgr/actions/branch/drop_branch.py +++ b/src/pkgmgr/actions/branch/drop_branch.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import Optional - from pkgmgr.core.git.commands import ( GitDeleteRemoteBranchError, delete_local_branch, @@ -12,7 +10,7 @@ from pkgmgr.core.git.queries import get_current_branch, resolve_base_branch def drop_branch( - name: Optional[str], + name: str | None, base_branch: str = "main", fallback_base: str = "master", cwd: str = ".", diff --git a/src/pkgmgr/actions/branch/open_branch.py b/src/pkgmgr/actions/branch/open_branch.py index 1a13fa1..5758ab4 100644 --- a/src/pkgmgr/actions/branch/open_branch.py +++ b/src/pkgmgr/actions/branch/open_branch.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import Optional - from pkgmgr.core.git.commands import ( checkout, create_branch, @@ -13,7 +11,7 @@ from pkgmgr.core.git.queries import resolve_base_branch def open_branch( - name: Optional[str], + name: str | None, base_branch: str = "main", fallback_base: str = "master", cwd: str = ".", diff --git a/src/pkgmgr/actions/changelog/__init__.py b/src/pkgmgr/actions/changelog/__init__.py index 88cb2a5..5d0f868 100644 --- a/src/pkgmgr/actions/changelog/__init__.py +++ b/src/pkgmgr/actions/changelog/__init__.py @@ -1,13 +1,9 @@ -#!/usr/bin/env python3 - """ Helpers to generate changelog information from Git history. """ from __future__ import annotations -from typing import Optional - from pkgmgr.core.git.queries import ( GitChangelogQueryError, get_changelog, @@ -16,8 +12,8 @@ from pkgmgr.core.git.queries import ( def generate_changelog( cwd: str, - from_ref: Optional[str] = None, - to_ref: Optional[str] = None, + from_ref: str | None = None, + to_ref: str | None = None, include_merges: bool = False, ) -> str: """ diff --git a/src/pkgmgr/actions/code_scanning/__init__.py b/src/pkgmgr/actions/code_scanning/__init__.py index c2cdc0c..ed24c1c 100644 --- a/src/pkgmgr/actions/code_scanning/__init__.py +++ b/src/pkgmgr/actions/code_scanning/__init__.py @@ -27,14 +27,14 @@ class CodeScanningResult: repo: str output_dir: str alert_count: int - files: List[str] = field(default_factory=list) + files: list[str] = field(default_factory=list) -def _gh(args: List[str]) -> subprocess.CompletedProcess[str]: - return subprocess.run(["gh", *args], capture_output=True, text=True) +def _gh(args: list[str]) -> subprocess.CompletedProcess[str]: + return subprocess.run(["gh", *args], capture_output=True, text=True, check=False) -def _resolve_repo(repo: Optional[str]) -> str: +def _resolve_repo(repo: str | None) -> str: if repo: return repo proc = _gh(["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"]) @@ -48,7 +48,7 @@ def _resolve_repo(repo: Optional[str]) -> str: return name -def _fetch_json(endpoint: str, params: Optional[List[str]] = None) -> Any: +def _fetch_json(endpoint: str, params: list[str] | None = None) -> Any: args = ["api", endpoint, "--paginate"] for param in params or []: args += ["-f", param] @@ -74,7 +74,7 @@ def _alert_row(alert: dict) -> str: return f"- [{severity}] {rule_id} — {path}:{line} ({state})\n {message}" -def _build_summary(repo: str, generated_at: str, alerts: List[dict]) -> str: +def _build_summary(repo: str, generated_at: str, alerts: list[dict]) -> str: by_severity: Counter = Counter() by_state: Counter = Counter() by_rule: Counter = Counter() @@ -112,9 +112,9 @@ def _build_summary(repo: str, generated_at: str, alerts: List[dict]) -> str: def download_code_scanning( - repo: Optional[str] = None, - output_dir: Optional[str] = None, - state: Optional[str] = None, + repo: str | None = None, + output_dir: str | None = None, + state: str | None = None, ) -> CodeScanningResult: if not shutil.which("gh"): raise CodeScanningError("the GitHub CLI 'gh' is not installed or not on PATH") diff --git a/src/pkgmgr/actions/config/add.py b/src/pkgmgr/actions/config/add.py index f4962ae..b4d216b 100644 --- a/src/pkgmgr/actions/config/add.py +++ b/src/pkgmgr/actions/config/add.py @@ -30,7 +30,7 @@ def interactive_add(config, USER_CONFIG_PATH: str): confirm = input("Add this entry to user config? (y/N): ").strip().lower() if confirm == "y": if os.path.exists(USER_CONFIG_PATH): - with open(USER_CONFIG_PATH, "r") as f: + with open(USER_CONFIG_PATH) as f: user_config = yaml.safe_load(f) or {} else: user_config = {"repositories": []} diff --git a/src/pkgmgr/actions/config/init.py b/src/pkgmgr/actions/config/init.py index 2c6a158..e19a1e8 100644 --- a/src/pkgmgr/actions/config/init.py +++ b/src/pkgmgr/actions/config/init.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - """ Initialize user configuration by scanning the repositories base directory. @@ -22,7 +20,7 @@ For each discovered repository, the function: from __future__ import annotations import os -from typing import Any, Dict +from typing import Any from pkgmgr.core.command.alias import generate_alias from pkgmgr.core.config.save import save_user_config @@ -30,8 +28,8 @@ from pkgmgr.core.git.queries import get_latest_commit def config_init( - user_config: Dict[str, Any], - defaults_config: Dict[str, Any], + user_config: dict[str, Any], + defaults_config: dict[str, Any], bin_dir: str, user_config_path: str, ) -> None: @@ -128,7 +126,7 @@ def config_init( "[WARN] Could not read commit (not a git repo or no commits)." ) - entry: Dict[str, Any] = { + entry: dict[str, Any] = { "provider": provider, "account": account, "repository": repo_name, diff --git a/src/pkgmgr/actions/install/__init__.py b/src/pkgmgr/actions/install/__init__.py index dac0267..e5e9194 100644 --- a/src/pkgmgr/actions/install/__init__.py +++ b/src/pkgmgr/actions/install/__init__.py @@ -1,6 +1,4 @@ # src/pkgmgr/actions/install/__init__.py -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- """ High-level entry point for repository installation. @@ -16,7 +14,7 @@ Responsibilities: from __future__ import annotations import os -from typing import Any, Dict, List, Optional, Tuple +from typing import Any from pkgmgr.actions.install.context import RepoContext from pkgmgr.actions.install.installers.makefile import ( @@ -37,7 +35,7 @@ from pkgmgr.core.repository.dir import get_repo_dir from pkgmgr.core.repository.identifier import get_repo_identifier from pkgmgr.core.repository.verify import verify_repository -Repository = Dict[str, Any] +Repository = dict[str, Any] INSTALLERS = [ ArchPkgbuildInstaller(), @@ -52,12 +50,12 @@ INSTALLERS = [ def _ensure_repo_dir( repo: Repository, repositories_base_dir: str, - all_repos: List[Repository], + all_repos: list[Repository], preview: bool, no_verification: bool, clone_mode: str, identifier: str, -) -> Optional[str]: +) -> str | None: """ Compute and, if necessary, clone the repository directory. @@ -127,7 +125,7 @@ def _create_context( repo_dir: str, repositories_base_dir: str, bin_dir: str, - all_repos: List[Repository], + all_repos: list[Repository], no_verification: bool, preview: bool, quiet: bool, @@ -155,10 +153,10 @@ def _create_context( def install_repos( - selected_repos: List[Repository], + selected_repos: list[Repository], repositories_base_dir: str, bin_dir: str, - all_repos: List[Repository], + all_repos: list[Repository], no_verification: bool, preview: bool, quiet: bool, @@ -179,7 +177,7 @@ def install_repos( overall command never exits non-zero because of per-repository failures. """ pipeline = InstallationPipeline(INSTALLERS) - failures: List[Tuple[str, str]] = [] + failures: list[tuple[str, str]] = [] for repo in selected_repos: identifier = get_repo_identifier(repo, all_repos) diff --git a/src/pkgmgr/actions/install/capabilities.py b/src/pkgmgr/actions/install/capabilities.py index 1770af7..5ac6168 100644 --- a/src/pkgmgr/actions/install/capabilities.py +++ b/src/pkgmgr/actions/install/capabilities.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - """ Capability detection for pkgmgr. @@ -35,7 +33,7 @@ import glob import os from abc import ABC, abstractmethod from collections.abc import Iterable -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING if TYPE_CHECKING: from pkgmgr.actions.install.context import RepoContext @@ -46,12 +44,12 @@ if TYPE_CHECKING: # --------------------------------------------------------------------------- -def _read_text_if_exists(path: str) -> Optional[str]: +def _read_text_if_exists(path: str) -> str | None: """Read a file as UTF-8 text, returning None if it does not exist or fails.""" if not os.path.exists(path): return None try: - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: return f.read() except OSError: return None @@ -75,12 +73,12 @@ def _scan_files_for_patterns(files: Iterable[str], patterns: Iterable[str]) -> b return False -def _first_spec_file(repo_dir: str) -> Optional[str]: +def _first_spec_file(repo_dir: str) -> str | None: """Return the first *.spec file in repo_dir, if any.""" matches = glob.glob(os.path.join(repo_dir, "*.spec")) if not matches: return None - return sorted(matches)[0] + return min(matches) # --------------------------------------------------------------------------- @@ -216,7 +214,7 @@ class MakeInstallCapability(CapabilityMatcher): if not os.path.exists(makefile): return False try: - with open(makefile, "r", encoding="utf-8") as f: + with open(makefile, encoding="utf-8") as f: for line in f: if line.strip().startswith("install:"): return True @@ -360,7 +358,7 @@ def detect_capabilities( def resolve_effective_capabilities( ctx: RepoContext, - layers: Optional[Iterable[str]] = None, + layers: Iterable[str] | None = None, ) -> dict[str, set[str]]: """ Resolve *effective* capabilities for each layer using a bottom-up strategy. @@ -381,10 +379,7 @@ def resolve_effective_capabilities( This means *any* higher layer can overshadow a lower layer, not just a specific one like Nix. The resolver is completely generic. """ - if layers is None: - layers_list = list(LAYER_ORDER) - else: - layers_list = list(layers) + layers_list = list(LAYER_ORDER) if layers is None else list(layers) raw_caps = detect_capabilities(ctx, layers_list) effective: dict[str, set[str]] = {layer: set() for layer in layers_list} diff --git a/src/pkgmgr/actions/install/context.py b/src/pkgmgr/actions/install/context.py index c69777c..c397f31 100644 --- a/src/pkgmgr/actions/install/context.py +++ b/src/pkgmgr/actions/install/context.py @@ -1,6 +1,4 @@ # src/pkgmgr/actions/install/context.py -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- """ Shared context object for repository installation steps. @@ -10,19 +8,19 @@ they do not depend on global state or long parameter lists. """ from dataclasses import dataclass -from typing import Any, Dict, List +from typing import Any @dataclass class RepoContext: """Container for all repository-related data used during installation.""" - repo: Dict[str, Any] + repo: dict[str, Any] identifier: str repo_dir: str repositories_base_dir: str bin_dir: str - all_repos: List[Dict[str, Any]] + all_repos: list[dict[str, Any]] no_verification: bool preview: bool diff --git a/src/pkgmgr/actions/install/installers/__init__.py b/src/pkgmgr/actions/install/installers/__init__.py index 9694cbf..8038e58 100644 --- a/src/pkgmgr/actions/install/installers/__init__.py +++ b/src/pkgmgr/actions/install/installers/__init__.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - """ Installer package for pkgmgr. diff --git a/src/pkgmgr/actions/install/installers/base.py b/src/pkgmgr/actions/install/installers/base.py index 28f1468..303f4d3 100644 --- a/src/pkgmgr/actions/install/installers/base.py +++ b/src/pkgmgr/actions/install/installers/base.py @@ -1,11 +1,9 @@ -#!/usr/bin/env python3 - """ Base interface for all installer components in the pkgmgr installation pipeline. """ +from __future__ import annotations from abc import ABC, abstractmethod -from typing import Optional, Set from pkgmgr.actions.install.capabilities import CAPABILITY_MATCHERS from pkgmgr.actions.install.context import RepoContext @@ -23,9 +21,9 @@ class BaseInstaller(ABC): # Examples: "nix", "python", "makefile". # This is used by capability matchers to decide which patterns to # search for in the repository. - layer: Optional[str] = None + layer: str | None = None - def discover_capabilities(self, ctx: RepoContext) -> Set[str]: + def discover_capabilities(self, ctx: RepoContext) -> set[str]: """ Determine which logical capabilities this installer will provide for this specific repository instance. @@ -35,7 +33,7 @@ class BaseInstaller(ABC): Makefile, etc.) and decide, via string matching, whether a given capability is actually provided by this layer. """ - caps: Set[str] = set() + caps: set[str] = set() if not self.layer: return caps diff --git a/src/pkgmgr/actions/install/installers/makefile.py b/src/pkgmgr/actions/install/installers/makefile.py index 5018f4f..9d173c6 100644 --- a/src/pkgmgr/actions/install/installers/makefile.py +++ b/src/pkgmgr/actions/install/installers/makefile.py @@ -26,16 +26,14 @@ class MakefileInstaller(BaseInstaller): def _has_install_target(self, makefile_path: str) -> bool: try: - with open(makefile_path, "r", encoding="utf-8", errors="ignore") as f: + with open(makefile_path, encoding="utf-8", errors="ignore") as f: content = f.read() except OSError: return False if re.search(r"^install\s*:", content, flags=re.MULTILINE): return True - if re.search(r"^install-[a-zA-Z0-9_-]*\s*:", content, flags=re.MULTILINE): - return True - return False + return bool(re.search(r"^install-[a-zA-Z0-9_-]*\s*:", content, flags=re.MULTILINE)) def run(self, ctx: RepoContext) -> None: makefile_path = os.path.join(ctx.repo_dir, self.MAKEFILE_NAME) diff --git a/src/pkgmgr/actions/install/installers/nix/conflicts.py b/src/pkgmgr/actions/install/installers/nix/conflicts.py index 65014ee..d722f92 100644 --- a/src/pkgmgr/actions/install/installers/nix/conflicts.py +++ b/src/pkgmgr/actions/install/installers/nix/conflicts.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING from .profile import NixProfileInspector from .retry import GitHubRateLimitRetry @@ -49,7 +49,7 @@ class NixConflictResolver: store_prefixes = self._parser.existing_store_prefixes(combined) # 2) Resolve them to concrete remove tokens - tokens: List[str] = self._profile.find_remove_tokens_for_store_prefixes( + tokens: list[str] = self._profile.find_remove_tokens_for_store_prefixes( ctx, self._runner, store_prefixes, diff --git a/src/pkgmgr/actions/install/installers/nix/installer.py b/src/pkgmgr/actions/install/installers/nix/installer.py index 79c207e..aef6c27 100644 --- a/src/pkgmgr/actions/install/installers/nix/installer.py +++ b/src/pkgmgr/actions/install/installers/nix/installer.py @@ -2,7 +2,7 @@ from __future__ import annotations import os import shutil -from typing import TYPE_CHECKING, List, Tuple +from typing import TYPE_CHECKING from pkgmgr.actions.install.installers.base import BaseInstaller @@ -42,7 +42,7 @@ class NixFlakeInstaller(BaseInstaller): return os.path.exists(os.path.join(ctx.repo_dir, self.FLAKE_FILE)) - def _profile_outputs(self, ctx: RepoContext) -> List[Tuple[str, bool]]: + def _profile_outputs(self, ctx: RepoContext) -> list[tuple[str, bool]]: # (output_name, allow_failure) if ctx.identifier in {"pkgmgr", "package-manager"}: return [("pkgmgr", False), ("default", True)] diff --git a/src/pkgmgr/actions/install/installers/nix/profile/inspector.py b/src/pkgmgr/actions/install/installers/nix/profile/inspector.py index c14163a..9d25b80 100644 --- a/src/pkgmgr/actions/install/installers/nix/profile/inspector.py +++ b/src/pkgmgr/actions/install/installers/nix/profile/inspector.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, List +from typing import TYPE_CHECKING, Any from .matcher import ( entry_matches_output, @@ -43,11 +43,11 @@ class NixProfileInspector: ctx: RepoContext, runner: CommandRunner, output: str, - ) -> List[int]: + ) -> list[int]: data = self.list_json(ctx, runner) entries = normalize_elements(data) - hits: List[int] = [] + hits: list[int] = [] for e in entries: if e.index is None: continue @@ -61,7 +61,7 @@ class NixProfileInspector: ctx: RepoContext, runner: CommandRunner, store_path: str, - ) -> List[int]: + ) -> list[int]: needle = (store_path or "").strip() if not needle: return [] @@ -69,7 +69,7 @@ class NixProfileInspector: data = self.list_json(ctx, runner) entries = normalize_elements(data) - hits: List[int] = [] + hits: list[int] = [] for e in entries: if e.index is None: continue @@ -87,7 +87,7 @@ class NixProfileInspector: ctx: RepoContext, runner: CommandRunner, output: str, - ) -> List[str]: + ) -> list[str]: """ Returns profile remove tokens to remove entries matching a given output. @@ -101,7 +101,7 @@ class NixProfileInspector: data = self.list_json(ctx, runner) entries = normalize_elements(data) - tokens: List[str] = [ + tokens: list[str] = [ out ] # critical: matches nix's own suggestion for conflicts @@ -119,7 +119,7 @@ class NixProfileInspector: # stable unique preserving order seen: set[str] = set() - uniq: List[str] = [] + uniq: list[str] = [] for t in tokens: if t and t not in seen: uniq.append(t) @@ -130,8 +130,8 @@ class NixProfileInspector: self, ctx: RepoContext, runner: CommandRunner, - prefixes: List[str], - ) -> List[str]: + prefixes: list[str], + ) -> list[str]: """ Returns remove tokens for entries whose store path matches any prefix. """ @@ -143,7 +143,7 @@ class NixProfileInspector: data = self.list_json(ctx, runner) entries = normalize_elements(data) - tokens: List[str] = [] + tokens: list[str] = [] for e in entries: if not e.store_paths: continue @@ -156,7 +156,7 @@ class NixProfileInspector: tokens.append(n) seen: set[str] = set() - uniq: List[str] = [] + uniq: list[str] = [] for t in tokens: if t and t not in seen: uniq.append(t) diff --git a/src/pkgmgr/actions/install/installers/nix/profile/matcher.py b/src/pkgmgr/actions/install/installers/nix/profile/matcher.py index 6d747c0..72b91f1 100644 --- a/src/pkgmgr/actions/install/installers/nix/profile/matcher.py +++ b/src/pkgmgr/actions/install/installers/nix/profile/matcher.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import List - from .models import NixProfileEntry @@ -51,9 +49,9 @@ def entry_matches_store_path(entry: NixProfileEntry, store_path: str) -> bool: return any((p or "") == needle for p in entry.store_paths) -def stable_unique_ints(values: List[int]) -> List[int]: +def stable_unique_ints(values: list[int]) -> list[int]: seen: set[int] = set() - uniq: List[int] = [] + uniq: list[int] = [] for v in values: if v in seen: continue diff --git a/src/pkgmgr/actions/install/installers/nix/profile/models.py b/src/pkgmgr/actions/install/installers/nix/profile/models.py index 1a23d62..72051a5 100644 --- a/src/pkgmgr/actions/install/installers/nix/profile/models.py +++ b/src/pkgmgr/actions/install/installers/nix/profile/models.py @@ -1,7 +1,6 @@ from __future__ import annotations from dataclasses import dataclass -from typing import List, Optional @dataclass(frozen=True) @@ -11,7 +10,7 @@ class NixProfileEntry: """ key: str - index: Optional[int] + index: int | None name: str attr_path: str - store_paths: List[str] + store_paths: list[str] diff --git a/src/pkgmgr/actions/install/installers/nix/profile/normalizer.py b/src/pkgmgr/actions/install/installers/nix/profile/normalizer.py index 29674ad..19ac873 100644 --- a/src/pkgmgr/actions/install/installers/nix/profile/normalizer.py +++ b/src/pkgmgr/actions/install/installers/nix/profile/normalizer.py @@ -2,12 +2,12 @@ from __future__ import annotations import re from collections.abc import Iterable -from typing import Any, Dict, List, Optional +from typing import Any from .models import NixProfileEntry -def coerce_index(key: str, entry: Dict[str, Any]) -> Optional[int]: +def coerce_index(key: str, entry: dict[str, Any]) -> int | None: """ Nix JSON schema varies: - elements keys might be "0", "1", ... @@ -73,7 +73,7 @@ def iter_store_paths(entry: Dict[str, Any]) -> Iterable[str]: outs = entry.get("outputs") if isinstance(outs, dict): - for _, ov in outs.items(): + for ov in outs.values(): if isinstance(ov, dict): p = ov.get("storePath") if isinstance(p, str): @@ -88,7 +88,7 @@ def normalize_store_path(store_path: str) -> str: return (store_path or "").strip() -def normalize_elements(data: Dict[str, Any]) -> List[NixProfileEntry]: +def normalize_elements(data: dict[str, Any]) -> list[NixProfileEntry]: """ Converts nix profile list JSON into a list of normalized entries. @@ -100,7 +100,7 @@ def normalize_elements(data: Dict[str, Any]) -> List[NixProfileEntry]: if not isinstance(elements, dict): return [] - normalized: List[NixProfileEntry] = [] + normalized: list[NixProfileEntry] = [] for k, entry in elements.items(): if not isinstance(entry, dict): @@ -110,7 +110,7 @@ def normalize_elements(data: Dict[str, Any]) -> List[NixProfileEntry]: name = str(entry.get("name", "") or "") attr = str(entry.get("attrPath", "") or "") - store_paths: List[str] = [] + store_paths: list[str] = [] for p in iter_store_paths(entry): sp = normalize_store_path(p) if sp: diff --git a/src/pkgmgr/actions/install/installers/nix/profile/parser.py b/src/pkgmgr/actions/install/installers/nix/profile/parser.py index c7e5a89..106be83 100644 --- a/src/pkgmgr/actions/install/installers/nix/profile/parser.py +++ b/src/pkgmgr/actions/install/installers/nix/profile/parser.py @@ -1,10 +1,10 @@ from __future__ import annotations import json -from typing import Any, Dict +from typing import Any -def parse_profile_list_json(raw: str) -> Dict[str, Any]: +def parse_profile_list_json(raw: str) -> dict[str, Any]: """ Parse JSON output from `nix profile list --json`. diff --git a/src/pkgmgr/actions/install/installers/nix/profile_list.py b/src/pkgmgr/actions/install/installers/nix/profile_list.py index 302af7a..4aea026 100644 --- a/src/pkgmgr/actions/install/installers/nix/profile_list.py +++ b/src/pkgmgr/actions/install/installers/nix/profile_list.py @@ -1,7 +1,7 @@ from __future__ import annotations import re -from typing import TYPE_CHECKING, List, Tuple +from typing import TYPE_CHECKING from .runner import CommandRunner @@ -19,12 +19,12 @@ class NixProfileListReader: m = re.match(r"^(/nix/store/[0-9a-z]{32}-[^/ \t]+)", raw) return m.group(1) if m else raw - def entries(self, ctx: RepoContext) -> List[Tuple[int, str]]: + def entries(self, ctx: RepoContext) -> list[tuple[int, str]]: res = self._runner.run(ctx, "nix profile list", allow_failure=True) if res.returncode != 0: return [] - entries: List[Tuple[int, str]] = [] + entries: list[tuple[int, str]] = [] pat = re.compile( r"^\s*(\d+)\s+.*?(/nix/store/[0-9a-z]{32}-[^/ \t]+)", re.MULTILINE, @@ -49,20 +49,20 @@ class NixProfileListReader: return uniq def indices_matching_store_prefixes( - self, ctx: RepoContext, prefixes: List[str] - ) -> List[int]: + self, ctx: RepoContext, prefixes: list[str] + ) -> list[int]: prefixes = [self._store_prefix(p) for p in prefixes if p] prefixes = [p for p in prefixes if p] if not prefixes: return [] - hits: List[int] = [] + hits: list[int] = [] for idx, sp in self.entries(ctx): if any(sp == p for p in prefixes): hits.append(idx) seen: set[int] = set() - uniq: List[int] = [] + uniq: list[int] = [] for i in hits: if i not in seen: seen.add(i) diff --git a/src/pkgmgr/actions/install/installers/nix/runner.py b/src/pkgmgr/actions/install/installers/nix/runner.py index 3638ff8..14af1c1 100644 --- a/src/pkgmgr/actions/install/installers/nix/runner.py +++ b/src/pkgmgr/actions/install/installers/nix/runner.py @@ -31,8 +31,7 @@ class CommandRunner: shell=True, cwd=repo_dir, check=False, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + capture_output=True, text=True, ) except Exception as e: diff --git a/src/pkgmgr/actions/install/installers/nix/textparse.py b/src/pkgmgr/actions/install/installers/nix/textparse.py index ac5dad7..3e0886b 100644 --- a/src/pkgmgr/actions/install/installers/nix/textparse.py +++ b/src/pkgmgr/actions/install/installers/nix/textparse.py @@ -1,7 +1,6 @@ from __future__ import annotations import re -from typing import List class NixConflictTextParser: @@ -11,13 +10,13 @@ class NixConflictTextParser: m = re.match(r"^(/nix/store/[0-9a-z]{32}-[^/ \t]+)", raw) return m.group(1) if m else raw - def remove_tokens(self, text: str) -> List[str]: + def remove_tokens(self, text: str) -> list[str]: pat = re.compile( r"^\s*nix profile remove\s+([^\s'\"`]+|'[^']+'|\"[^\"]+\")\s*$", re.MULTILINE, ) - tokens: List[str] = [] + tokens: list[str] = [] for m in pat.finditer(text or ""): t = (m.group(1) or "").strip() if (t.startswith("'") and t.endswith("'")) or ( @@ -28,7 +27,7 @@ class NixConflictTextParser: tokens.append(t) seen: set[str] = set() - uniq: List[str] = [] + uniq: list[str] = [] for t in tokens: if t not in seen: seen.add(t) @@ -36,9 +35,9 @@ class NixConflictTextParser: return uniq - def existing_store_prefixes(self, text: str) -> List[str]: + def existing_store_prefixes(self, text: str) -> list[str]: lines = (text or "").splitlines() - prefixes: List[str] = [] + prefixes: list[str] = [] in_existing = False in_new = False @@ -69,7 +68,7 @@ class NixConflictTextParser: norm = [self._store_prefix(p) for p in prefixes if p] seen: set[str] = set() - uniq: List[str] = [] + uniq: list[str] = [] for p in norm: if p and p not in seen: seen.add(p) diff --git a/src/pkgmgr/actions/install/installers/os_packages/debian_control.py b/src/pkgmgr/actions/install/installers/os_packages/debian_control.py index ea1eeaf..cc4f62e 100644 --- a/src/pkgmgr/actions/install/installers/os_packages/debian_control.py +++ b/src/pkgmgr/actions/install/installers/os_packages/debian_control.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - """ Installer for Debian/Ubuntu packages defined via debian/control. @@ -12,11 +10,11 @@ This installer: It is intended for Debian-based systems where dpkg-buildpackage and apt/dpkg tooling are available. """ +from __future__ import annotations import glob import os import shutil -from typing import List, Optional from pkgmgr.actions.install.context import RepoContext from pkgmgr.actions.install.installers.base import BaseInstaller @@ -55,7 +53,7 @@ class DebianControlInstaller(BaseInstaller): return os.path.exists(self._control_path(ctx)) - def _find_built_debs(self, repo_dir: str) -> List[str]: + def _find_built_debs(self, repo_dir: str) -> list[str]: """ Find .deb files built by dpkg-buildpackage. @@ -66,7 +64,7 @@ class DebianControlInstaller(BaseInstaller): pattern = os.path.join(parent, "*.deb") return sorted(glob.glob(pattern)) - def _privileged_prefix(self) -> Optional[str]: + def _privileged_prefix(self) -> str | None: """ Determine how to run privileged commands: diff --git a/src/pkgmgr/actions/install/installers/os_packages/rpm_spec.py b/src/pkgmgr/actions/install/installers/os_packages/rpm_spec.py index 00248c7..62c5f17 100644 --- a/src/pkgmgr/actions/install/installers/os_packages/rpm_spec.py +++ b/src/pkgmgr/actions/install/installers/os_packages/rpm_spec.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - """ Installer for RPM-based packages defined in *.spec files. @@ -13,12 +11,12 @@ This installer: It targets RPM-based systems (Fedora / RHEL / CentOS / Rocky / Alma, etc.). """ +from __future__ import annotations import glob import os import shutil import tarfile -from typing import List, Optional, Tuple from pkgmgr.actions.install.context import RepoContext from pkgmgr.actions.install.installers.base import BaseInstaller @@ -52,7 +50,7 @@ class RpmSpecInstaller(BaseInstaller): return has_dnf or has_yum or has_yum_builddep - def _spec_path(self, ctx: RepoContext) -> Optional[str]: + def _spec_path(self, ctx: RepoContext) -> str | None: """Return the first *.spec file in the repository root, if any.""" pattern = os.path.join(ctx.repo_dir, "*.spec") matches = sorted(glob.glob(pattern)) @@ -91,7 +89,7 @@ class RpmSpecInstaller(BaseInstaller): for sub in ("BUILD", "BUILDROOT", "RPMS", "SOURCES", "SPECS", "SRPMS"): os.makedirs(os.path.join(topdir, sub), exist_ok=True) - def _parse_name_version(self, spec_path: str) -> Optional[Tuple[str, str]]: + def _parse_name_version(self, spec_path: str) -> tuple[str, str] | None: """ Parse Name and Version from the given .spec file. @@ -100,7 +98,7 @@ class RpmSpecInstaller(BaseInstaller): name = None version = None - with open(spec_path, "r", encoding="utf-8") as f: + with open(spec_path, encoding="utf-8") as f: for raw_line in f: line = raw_line.strip() # Ignore comments @@ -182,7 +180,7 @@ class RpmSpecInstaller(BaseInstaller): return self._spec_path(ctx) is not None - def _find_built_rpms(self) -> List[str]: + def _find_built_rpms(self) -> list[str]: """ Find RPMs built by rpmbuild. @@ -212,7 +210,7 @@ class RpmSpecInstaller(BaseInstaller): run_command(cmd, cwd=ctx.repo_dir, preview=ctx.preview) - def _install_built_rpms(self, ctx: RepoContext, rpms: List[str]) -> None: + def _install_built_rpms(self, ctx: RepoContext, rpms: list[str]) -> None: """ Install or upgrade the built RPMs. diff --git a/src/pkgmgr/actions/install/layers.py b/src/pkgmgr/actions/install/layers.py index f36d081..fbd967a 100644 --- a/src/pkgmgr/actions/install/layers.py +++ b/src/pkgmgr/actions/install/layers.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - """ CLI layer model for the pkgmgr installation pipeline. @@ -18,7 +16,6 @@ from __future__ import annotations import os from enum import Enum -from typing import Optional class CliLayer(str, Enum): @@ -37,7 +34,7 @@ CLI_LAYERS: list[CliLayer] = [ ] -def layer_priority(layer: Optional[CliLayer]) -> int: +def layer_priority(layer: CliLayer | None) -> int: """ Return a numeric priority index for a given layer. @@ -69,13 +66,11 @@ def classify_command_layer(command: str, repo_dir: str) -> CliLayer: home = os.path.expanduser("~") # OS package managers - if command_abs.startswith("/usr/") or command_abs.startswith("/bin/"): + if command_abs.startswith(("/usr/", "/bin/")): return CliLayer.OS_PACKAGES # Nix store / profile - if command_abs.startswith("/nix/store/") or command_abs.startswith( - os.path.join(home, ".nix-profile") - ): + if command_abs.startswith(("/nix/store/", os.path.join(home, ".nix-profile"))): return CliLayer.NIX # User-local bin diff --git a/src/pkgmgr/actions/install/pipeline.py b/src/pkgmgr/actions/install/pipeline.py index f32f37f..0d5e3e9 100644 --- a/src/pkgmgr/actions/install/pipeline.py +++ b/src/pkgmgr/actions/install/pipeline.py @@ -1,6 +1,4 @@ # src/pkgmgr/actions/install/pipeline.py -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- """ Installation pipeline orchestration for repositories. @@ -10,7 +8,6 @@ from __future__ import annotations from collections.abc import Sequence from dataclasses import dataclass -from typing import Optional, Set from pkgmgr.actions.install.context import RepoContext from pkgmgr.actions.install.installers.base import BaseInstaller @@ -25,8 +22,8 @@ from pkgmgr.core.command.resolve import resolve_command_for_repo @dataclass class CommandState: - command: Optional[str] - layer: Optional[CliLayer] + command: str | None + layer: CliLayer | None class CommandResolver: @@ -84,7 +81,7 @@ class InstallationPipeline: else: repo.pop("command", None) - provided_capabilities: Set[str] = set() + provided_capabilities: set[str] = set() for installer in self._installers: layer_name = getattr(installer, "layer", None) diff --git a/src/pkgmgr/actions/mirror/context.py b/src/pkgmgr/actions/mirror/context.py index 22591ee..93d3c51 100644 --- a/src/pkgmgr/actions/mirror/context.py +++ b/src/pkgmgr/actions/mirror/context.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import List - from pkgmgr.core.repository.dir import get_repo_dir from pkgmgr.core.repository.identifier import get_repo_identifier @@ -12,7 +10,7 @@ from .types import MirrorMap, RepoMirrorContext, Repository def build_context( repo: Repository, repositories_base_dir: str, - all_repos: List[Repository], + all_repos: list[Repository], ) -> RepoMirrorContext: """ Build a RepoMirrorContext for a single repository. diff --git a/src/pkgmgr/actions/mirror/diff_cmd.py b/src/pkgmgr/actions/mirror/diff_cmd.py index 3b467b8..a266bcf 100644 --- a/src/pkgmgr/actions/mirror/diff_cmd.py +++ b/src/pkgmgr/actions/mirror/diff_cmd.py @@ -1,16 +1,14 @@ from __future__ import annotations -from typing import List - from .context import build_context from .printing import print_header from .types import Repository def diff_mirrors( - selected_repos: List[Repository], + selected_repos: list[Repository], repositories_base_dir: str, - all_repos: List[Repository], + all_repos: list[Repository], ) -> None: """ Show differences between config mirrors and MIRRORS file. diff --git a/src/pkgmgr/actions/mirror/git_remote.py b/src/pkgmgr/actions/mirror/git_remote.py index c0daec2..9334c66 100644 --- a/src/pkgmgr/actions/mirror/git_remote.py +++ b/src/pkgmgr/actions/mirror/git_remote.py @@ -1,7 +1,6 @@ from __future__ import annotations import os -from typing import Optional, Set from pkgmgr.core.git.commands import ( GitAddRemoteError, @@ -38,13 +37,10 @@ def _is_git_remote_url(url: str) -> bool: if u.startswith("ssh://"): return True - if (u.startswith("https://") or u.startswith("http://")) and u.endswith(".git"): - return True - - return False + return bool((u.startswith(("https://", "http://"))) and u.endswith(".git")) -def build_default_ssh_url(repo: Repository) -> Optional[str]: +def build_default_ssh_url(repo: Repository) -> str | None: provider = repo.get("provider") account = repo.get("account") name = repo.get("repository") @@ -66,7 +62,7 @@ def _git_mirrors_only(m: MirrorMap) -> MirrorMap: def determine_primary_remote_url( repo: Repository, ctx: RepoMirrorContext, -) -> Optional[str]: +) -> str | None: """ Priority order (GIT URLS ONLY): 1. origin from resolved mirrors (if it is a git URL) @@ -80,7 +76,7 @@ def determine_primary_remote_url( return origin for mirrors in (ctx.file_mirrors, ctx.config_mirrors): - for _, url in mirrors.items(): + for url in mirrors.values(): if url and _is_git_remote_url(url): return url @@ -116,7 +112,7 @@ def _ensure_additional_push_urls( Non-git URLs (like PyPI) are ignored and will never land in git config. """ git_only = _git_mirrors_only(mirrors) - desired: Set[str] = {u for u in git_only.values() if u and u != primary} + desired: set[str] = {u for u in git_only.values() if u and u != primary} if not desired: return diff --git a/src/pkgmgr/actions/mirror/io.py b/src/pkgmgr/actions/mirror/io.py index c5c9098..18588d4 100644 --- a/src/pkgmgr/actions/mirror/io.py +++ b/src/pkgmgr/actions/mirror/io.py @@ -42,7 +42,7 @@ def read_mirrors_file(repo_dir: str, filename: str = "MIRRORS") -> MirrorMap: return mirrors try: - with open(path, "r", encoding="utf-8") as fh: + with open(path, encoding="utf-8") as fh: for line in fh: stripped = line.strip() if not stripped or stripped.startswith("#"): diff --git a/src/pkgmgr/actions/mirror/list_cmd.py b/src/pkgmgr/actions/mirror/list_cmd.py index a3b094a..ffa3763 100644 --- a/src/pkgmgr/actions/mirror/list_cmd.py +++ b/src/pkgmgr/actions/mirror/list_cmd.py @@ -1,16 +1,14 @@ from __future__ import annotations -from typing import List - from .context import build_context from .printing import print_header, print_named_mirrors from .types import Repository def list_mirrors( - selected_repos: List[Repository], + selected_repos: list[Repository], repositories_base_dir: str, - all_repos: List[Repository], + all_repos: list[Repository], source: str = "all", ) -> None: """ diff --git a/src/pkgmgr/actions/mirror/merge_cmd.py b/src/pkgmgr/actions/mirror/merge_cmd.py index 7f9cca8..ac17548 100644 --- a/src/pkgmgr/actions/mirror/merge_cmd.py +++ b/src/pkgmgr/actions/mirror/merge_cmd.py @@ -1,7 +1,6 @@ from __future__ import annotations import os -from typing import Dict, List, Optional, Tuple import yaml @@ -16,7 +15,7 @@ from .types import MirrorMap, Repository # ----------------------------------------------------------------------------- -def _repo_key(repo: Repository) -> Tuple[str, str, str]: +def _repo_key(repo: Repository) -> tuple[str, str, str]: """ Normalised key for identifying a repository in config files. """ @@ -27,7 +26,7 @@ def _repo_key(repo: Repository) -> Tuple[str, str, str]: ) -def _load_user_config(path: str) -> Dict[str, object]: +def _load_user_config(path: str) -> dict[str, object]: """ Load a user config YAML file as dict. Non-dicts yield {}. @@ -49,13 +48,13 @@ def _load_user_config(path: str) -> Dict[str, object]: def merge_mirrors( - selected_repos: List[Repository], + selected_repos: list[Repository], repositories_base_dir: str, - all_repos: List[Repository], + all_repos: list[Repository], source: str, target: str, preview: bool = False, - user_config_path: Optional[str] = None, + user_config_path: str | None = None, ) -> None: """ Merge mirrors between config and MIRRORS file. @@ -73,8 +72,8 @@ def merge_mirrors( """ # Load user config once if we intend to write to it. - user_cfg: Optional[Dict[str, object]] = None - user_cfg_path_expanded: Optional[str] = None + user_cfg: dict[str, object] | None = None + user_cfg_path_expanded: str | None = None if target == "config" and user_config_path and not preview: user_cfg_path_expanded = os.path.expanduser(user_config_path) @@ -129,7 +128,7 @@ def merge_mirrors( repos = user_cfg.get("repositories") target_key = _repo_key(repo) - existing_repo: Optional[Repository] = None + existing_repo: Repository | None = None # Find existing repo entry for entry in repos: diff --git a/src/pkgmgr/actions/mirror/remote_provision.py b/src/pkgmgr/actions/mirror/remote_provision.py index a7faf37..970ae59 100644 --- a/src/pkgmgr/actions/mirror/remote_provision.py +++ b/src/pkgmgr/actions/mirror/remote_provision.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import List - from pkgmgr.core.remote_provisioning import ProviderHint, RepoSpec, ensure_remote_repo from pkgmgr.core.remote_provisioning.ensure import EnsureOptions @@ -64,7 +62,7 @@ def ensure_remote_repository_for_url( def ensure_remote_repository( repo: Repository, repositories_base_dir: str, - all_repos: List[Repository], + all_repos: list[Repository], preview: bool, ) -> None: """ diff --git a/src/pkgmgr/actions/mirror/setup_cmd.py b/src/pkgmgr/actions/mirror/setup_cmd.py index fe22014..78194c2 100644 --- a/src/pkgmgr/actions/mirror/setup_cmd.py +++ b/src/pkgmgr/actions/mirror/setup_cmd.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import List - from pkgmgr.core.git.queries import probe_remote_reachable_detail from pkgmgr.core.remote_provisioning import ProviderHint, RepoSpec, set_repo_visibility from pkgmgr.core.remote_provisioning.visibility import VisibilityOptions @@ -23,9 +21,7 @@ def _is_git_remote_url(url: str) -> bool: return True if u.startswith("ssh://"): return True - if (u.startswith("https://") or u.startswith("http://")) and u.endswith(".git"): - return True - return False + return bool((u.startswith(("https://", "http://"))) and u.endswith(".git")) def _provider_hint_from_host(host: str) -> str | None: @@ -89,7 +85,7 @@ def _print_probe_result(name: str | None, url: str, *, cwd: str) -> None: def _setup_local_mirrors_for_repo( repo: Repository, repositories_base_dir: str, - all_repos: List[Repository], + all_repos: list[Repository], preview: bool, ) -> None: ctx = build_context(repo, repositories_base_dir, all_repos) @@ -106,7 +102,7 @@ def _setup_local_mirrors_for_repo( def _setup_remote_mirrors_for_repo( repo: Repository, repositories_base_dir: str, - all_repos: List[Repository], + all_repos: list[Repository], preview: bool, ensure_remote: bool, ensure_visibility: str | None, @@ -195,9 +191,9 @@ def _setup_remote_mirrors_for_repo( def setup_mirrors( - selected_repos: List[Repository], + selected_repos: list[Repository], repositories_base_dir: str, - all_repos: List[Repository], + all_repos: list[Repository], preview: bool = False, local: bool = True, remote: bool = True, diff --git a/src/pkgmgr/actions/mirror/types.py b/src/pkgmgr/actions/mirror/types.py index a95bf80..aae93d5 100644 --- a/src/pkgmgr/actions/mirror/types.py +++ b/src/pkgmgr/actions/mirror/types.py @@ -1,10 +1,10 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any, Dict +from typing import Any -Repository = Dict[str, Any] -MirrorMap = Dict[str, str] +Repository = dict[str, Any] +MirrorMap = dict[str, str] @dataclass(frozen=True) diff --git a/src/pkgmgr/actions/mirror/url_utils.py b/src/pkgmgr/actions/mirror/url_utils.py index 8515435..0935caa 100644 --- a/src/pkgmgr/actions/mirror/url_utils.py +++ b/src/pkgmgr/actions/mirror/url_utils.py @@ -1,11 +1,10 @@ # src/pkgmgr/actions/mirror/url_utils.py from __future__ import annotations -from typing import Optional, Tuple from urllib.parse import urlparse -def hostport_from_git_url(url: str) -> Tuple[str, Optional[str]]: +def hostport_from_git_url(url: str) -> tuple[str, str | None]: url = (url or "").strip() if not url: return "", None @@ -58,7 +57,7 @@ def _strip_dot_git(name: str) -> str: return n -def parse_repo_from_git_url(url: str) -> Tuple[str, Optional[str], Optional[str]]: +def parse_repo_from_git_url(url: str) -> tuple[str, str | None, str | None]: """ Parse (host, owner, repo_name) from common Git remote URLs. diff --git a/src/pkgmgr/actions/mirror/visibility_cmd.py b/src/pkgmgr/actions/mirror/visibility_cmd.py index 6d16e62..ef13001 100644 --- a/src/pkgmgr/actions/mirror/visibility_cmd.py +++ b/src/pkgmgr/actions/mirror/visibility_cmd.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import List - from pkgmgr.core.remote_provisioning import ProviderHint, RepoSpec, set_repo_visibility from pkgmgr.core.remote_provisioning.visibility import VisibilityOptions @@ -20,9 +18,7 @@ def _is_git_remote_url(url: str) -> bool: return True if u.startswith("ssh://"): return True - if (u.startswith("https://") or u.startswith("http://")) and u.endswith(".git"): - return True - return False + return bool((u.startswith(("https://", "http://"))) and u.endswith(".git")) def _provider_hint_from_host(host: str) -> str | None: @@ -66,9 +62,9 @@ def _apply_visibility_for_url( def set_mirror_visibility( - selected_repos: List[Repository], + selected_repos: list[Repository], repositories_base_dir: str, - all_repos: List[Repository], + all_repos: list[Repository], *, visibility: str, preview: bool = False, diff --git a/src/pkgmgr/actions/release/files/__init__.py b/src/pkgmgr/actions/release/files/__init__.py index 3c56045..e09f468 100644 --- a/src/pkgmgr/actions/release/files/__init__.py +++ b/src/pkgmgr/actions/release/files/__init__.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - """ Backwards-compatible facade for the release file update helpers. diff --git a/src/pkgmgr/actions/release/files/changelog_lint.py b/src/pkgmgr/actions/release/files/changelog_lint.py index 5fa5335..eea7ba1 100644 --- a/src/pkgmgr/actions/release/files/changelog_lint.py +++ b/src/pkgmgr/actions/release/files/changelog_lint.py @@ -1,5 +1,6 @@ from __future__ import annotations +import contextlib import os import re import shutil @@ -70,6 +71,7 @@ def _markdownlint(reference_path: str, document: str) -> list[str]: cwd=directory, capture_output=True, text=True, + check=False, ) if proc.returncode == 0: return [] diff --git a/src/pkgmgr/actions/release/files/changelog_md.py b/src/pkgmgr/actions/release/files/changelog_md.py index af50f15..9b3d0f9 100644 --- a/src/pkgmgr/actions/release/files/changelog_md.py +++ b/src/pkgmgr/actions/release/files/changelog_md.py @@ -4,7 +4,6 @@ import os import re import sys from datetime import date -from typing import Optional from .changelog_lint import ( ChangelogLintError, @@ -55,7 +54,7 @@ def _insert_after_h1(existing: str, entry: str) -> str: def update_changelog( changelog_path: str, new_version: str, - message: Optional[str] = None, + message: str | None = None, preview: bool = False, ) -> str: """Insert a new release entry into CHANGELOG.md. @@ -87,7 +86,7 @@ def update_changelog( elif preview or not sys.stdin.isatty(): body, entry = _entry_for(message or f"Release {new_version}") else: - attempt: Optional[str] = None + attempt: str | None = None while True: print( "\n[INFO] Provide the changelog entry — a leading '#' becomes " diff --git a/src/pkgmgr/actions/release/files/debian.py b/src/pkgmgr/actions/release/files/debian.py index b0d6c03..5288669 100644 --- a/src/pkgmgr/actions/release/files/debian.py +++ b/src/pkgmgr/actions/release/files/debian.py @@ -2,12 +2,11 @@ from __future__ import annotations import os from datetime import datetime -from typing import Optional, Tuple from pkgmgr.core.git.queries import get_config_value -def _get_debian_author() -> Tuple[str, str]: +def _get_debian_author() -> tuple[str, str]: name = os.environ.get("DEBFULLNAME") email = os.environ.get("DEBEMAIL") @@ -33,7 +32,7 @@ def update_debian_changelog( debian_changelog_path: str, package_name: str, new_version: str, - message: Optional[str] = None, + message: str | None = None, preview: bool = False, ) -> None: if not os.path.exists(debian_changelog_path): diff --git a/src/pkgmgr/actions/release/files/editor.py b/src/pkgmgr/actions/release/files/editor.py index fd54018..a990548 100644 --- a/src/pkgmgr/actions/release/files/editor.py +++ b/src/pkgmgr/actions/release/files/editor.py @@ -1,12 +1,12 @@ from __future__ import annotations +import contextlib import os import subprocess import tempfile -from typing import Optional -def _open_editor_for_changelog(initial_message: Optional[str] = None) -> str: +def _open_editor_for_changelog(initial_message: str | None = None) -> str: editor = os.environ.get("EDITOR", "nano") with tempfile.NamedTemporaryFile( diff --git a/src/pkgmgr/actions/release/files/pyproject.py b/src/pkgmgr/actions/release/files/pyproject.py index afefb39..1547513 100644 --- a/src/pkgmgr/actions/release/files/pyproject.py +++ b/src/pkgmgr/actions/release/files/pyproject.py @@ -12,7 +12,7 @@ def update_pyproject_version( return try: - with open(pyproject_path, "r", encoding="utf-8") as f: + with open(pyproject_path, encoding="utf-8") as f: content = f.read() except OSError as exc: print(f"[WARN] Could not read pyproject.toml: {exc}") diff --git a/src/pkgmgr/actions/release/files/rpm_changelog.py b/src/pkgmgr/actions/release/files/rpm_changelog.py index 586a204..fbada7f 100644 --- a/src/pkgmgr/actions/release/files/rpm_changelog.py +++ b/src/pkgmgr/actions/release/files/rpm_changelog.py @@ -2,7 +2,6 @@ from __future__ import annotations import os from datetime import datetime -from typing import Optional from .debian import _get_debian_author @@ -11,7 +10,7 @@ def update_spec_changelog( spec_path: str, package_name: str, new_version: str, - message: Optional[str] = None, + message: str | None = None, preview: bool = False, ) -> None: if not os.path.exists(spec_path): diff --git a/src/pkgmgr/actions/release/package_name.py b/src/pkgmgr/actions/release/package_name.py index 0306634..80cab36 100644 --- a/src/pkgmgr/actions/release/package_name.py +++ b/src/pkgmgr/actions/release/package_name.py @@ -24,7 +24,6 @@ from __future__ import annotations import os import re -from typing import Optional from pkgmgr.core.repository.paths import RepoPaths @@ -33,17 +32,17 @@ _PKGBUILD_NAME_RE = re.compile(r"^pkgname=([^\s#]+)\s*$", re.MULTILINE) _RPM_NAME_RE = re.compile(r"^Name:\s*(\S+)\s*$", re.MULTILINE) -def _read(path: Optional[str]) -> str: +def _read(path: str | None) -> str: if not path or not os.path.isfile(path): return "" try: - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: return f.read() except OSError: return "" -def _extract(pattern: re.Pattern[str], text: str) -> Optional[str]: +def _extract(pattern: re.Pattern[str], text: str) -> str | None: if not text: return None match = pattern.search(text) diff --git a/src/pkgmgr/actions/release/versioning.py b/src/pkgmgr/actions/release/versioning.py index 481e2f3..2d9c5cf 100644 --- a/src/pkgmgr/actions/release/versioning.py +++ b/src/pkgmgr/actions/release/versioning.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - """ Version discovery and bumping helpers for the release workflow. """ diff --git a/src/pkgmgr/actions/release/workflow.py b/src/pkgmgr/actions/release/workflow.py index 5888fcd..b4d6725 100644 --- a/src/pkgmgr/actions/release/workflow.py +++ b/src/pkgmgr/actions/release/workflow.py @@ -2,7 +2,6 @@ from __future__ import annotations import os import sys -from typing import Optional from pkgmgr.actions.branch import close_branch from pkgmgr.core.git import GitRunError, run @@ -34,7 +33,7 @@ def _release_impl( pyproject_path: str = "pyproject.toml", changelog_path: str = "CHANGELOG.md", release_type: str = "patch", - message: Optional[str] = None, + message: str | None = None, preview: bool = False, close: bool = False, force: bool = False, @@ -87,7 +86,7 @@ def _release_impl( else: print("[INFO] No RPM spec file found. Skipping spec version update.") - effective_message: Optional[str] = message + effective_message: str | None = message if isinstance(changelog_message, str) and changelog_message.strip(): effective_message = changelog_message.strip() @@ -195,7 +194,7 @@ def release( pyproject_path: str = "pyproject.toml", changelog_path: str = "CHANGELOG.md", release_type: str = "patch", - message: Optional[str] = None, + message: str | None = None, preview: bool = False, force: bool = False, close: bool = False, diff --git a/src/pkgmgr/actions/repository/_parallel.py b/src/pkgmgr/actions/repository/_parallel.py index d931f3d..c7add17 100644 --- a/src/pkgmgr/actions/repository/_parallel.py +++ b/src/pkgmgr/actions/repository/_parallel.py @@ -3,29 +3,29 @@ from __future__ import annotations import os import sys from concurrent.futures import ThreadPoolExecutor, as_completed -from typing import Any, Callable, Dict, List, Tuple +from typing import Any, Callable from pkgmgr.core.repository.dir import get_repo_dir from pkgmgr.core.repository.identifier import get_repo_identifier -Repository = Dict[str, Any] -RepoRef = Tuple[str, str] -OpResult = Tuple[bool, str] +Repository = dict[str, Any] +RepoRef = tuple[str, str] +OpResult = tuple[bool, str] RepoOp = Callable[[str], OpResult] def resolve_repos( - selected_repos: List[Repository], + selected_repos: list[Repository], repositories_base_dir: str, - all_repos: List[Repository], -) -> List[RepoRef]: + all_repos: list[Repository], +) -> list[RepoRef]: """ Resolve ``(identifier, repo_dir)`` pairs for ``selected_repos``. Repositories whose directory does not exist on disk are reported and skipped, matching the prior behavior of pull/push handlers. """ - resolved: List[RepoRef] = [] + resolved: list[RepoRef] = [] for repo in selected_repos: ident = get_repo_identifier(repo, all_repos) rd = get_repo_dir(repositories_base_dir, repo) @@ -37,7 +37,7 @@ def resolve_repos( def run_on_repos( - repos: List[RepoRef], + repos: list[RepoRef], op: RepoOp, *, jobs: int, @@ -55,7 +55,7 @@ def run_on_repos( return effective_jobs = max(1, min(jobs, len(repos))) - failed: List[Tuple[str, str]] = [] + failed: list[tuple[str, str]] = [] if effective_jobs == 1: for ident, rd in repos: diff --git a/src/pkgmgr/actions/repository/clone.py b/src/pkgmgr/actions/repository/clone.py index 6fe7c88..d4e11d6 100644 --- a/src/pkgmgr/actions/repository/clone.py +++ b/src/pkgmgr/actions/repository/clone.py @@ -1,7 +1,7 @@ from __future__ import annotations import os -from typing import Any, Dict, List, Optional +from typing import Any from pkgmgr.core.git.commands import GitCloneError from pkgmgr.core.git.commands import clone as git_clone @@ -9,10 +9,10 @@ from pkgmgr.core.repository.dir import get_repo_dir from pkgmgr.core.repository.identifier import get_repo_identifier from pkgmgr.core.repository.verify import verify_repository -Repository = Dict[str, Any] +Repository = dict[str, Any] -def _build_clone_url(repo: Repository, clone_mode: str) -> Optional[str]: +def _build_clone_url(repo: Repository, clone_mode: str) -> str | None: provider = repo.get("provider") account = repo.get("account") name = repo.get("repository") @@ -34,9 +34,9 @@ def _build_clone_url(repo: Repository, clone_mode: str) -> Optional[str]: def clone_repos( - selected_repos: List[Repository], + selected_repos: list[Repository], repositories_base_dir: str, - all_repos: List[Repository], + all_repos: list[Repository], preview: bool, no_verification: bool, clone_mode: str, diff --git a/src/pkgmgr/actions/repository/create/__init__.py b/src/pkgmgr/actions/repository/create/__init__.py index 0ca6a49..3a980d0 100644 --- a/src/pkgmgr/actions/repository/create/__init__.py +++ b/src/pkgmgr/actions/repository/create/__init__.py @@ -1,10 +1,10 @@ from __future__ import annotations -from typing import Any, Dict +from typing import Any from .service import CreateRepoService -RepositoryConfig = Dict[str, Any] +RepositoryConfig = dict[str, Any] __all__ = [ "CreateRepoService", diff --git a/src/pkgmgr/actions/repository/create/config_writer.py b/src/pkgmgr/actions/repository/create/config_writer.py index 3711484..cf73f63 100644 --- a/src/pkgmgr/actions/repository/create/config_writer.py +++ b/src/pkgmgr/actions/repository/create/config_writer.py @@ -1,21 +1,21 @@ from __future__ import annotations import os -from typing import Any, Dict, Set +from typing import Any import yaml from pkgmgr.core.command.alias import generate_alias from pkgmgr.core.config.save import save_user_config -Repository = Dict[str, Any] +Repository = dict[str, Any] class ConfigRepoWriter: def __init__( self, *, - config_merged: Dict[str, Any], + config_merged: dict[str, Any], user_config_path: str, bin_dir: str, ): @@ -43,7 +43,7 @@ class ConfigRepoWriter: ): return repo - existing_aliases: Set[str] = { + existing_aliases: set[str] = { str(r.get("alias")) for r in repositories if r.get("alias") } @@ -70,7 +70,7 @@ class ConfigRepoWriter: return repo if os.path.exists(self.user_config_path): - with open(self.user_config_path, "r", encoding="utf-8") as f: + with open(self.user_config_path, encoding="utf-8") as f: user_cfg = yaml.safe_load(f) or {} else: user_cfg = {} diff --git a/src/pkgmgr/actions/repository/create/mirrors.py b/src/pkgmgr/actions/repository/create/mirrors.py index 97917bb..da64a6c 100644 --- a/src/pkgmgr/actions/repository/create/mirrors.py +++ b/src/pkgmgr/actions/repository/create/mirrors.py @@ -1,11 +1,11 @@ from __future__ import annotations -from typing import Any, Dict +from typing import Any from pkgmgr.actions.mirror.io import write_mirrors_file from pkgmgr.actions.mirror.setup_cmd import setup_mirrors -Repository = Dict[str, Any] +Repository = dict[str, Any] class MirrorBootstrapper: diff --git a/src/pkgmgr/actions/repository/create/model.py b/src/pkgmgr/actions/repository/create/model.py index 02911aa..258fcdf 100644 --- a/src/pkgmgr/actions/repository/create/model.py +++ b/src/pkgmgr/actions/repository/create/model.py @@ -1,12 +1,11 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Optional @dataclass(frozen=True) class RepoParts: host: str - port: Optional[str] + port: str | None owner: str name: str diff --git a/src/pkgmgr/actions/repository/create/parser.py b/src/pkgmgr/actions/repository/create/parser.py index e347e2b..7ab3180 100644 --- a/src/pkgmgr/actions/repository/create/parser.py +++ b/src/pkgmgr/actions/repository/create/parser.py @@ -1,7 +1,6 @@ from __future__ import annotations import re -from typing import Tuple from urllib.parse import urlparse from .model import RepoParts @@ -50,7 +49,7 @@ def _parse_git_url(url: str) -> RepoParts: return RepoParts(host=host, port=port, owner=owner, name=name) -def _split_host_port(host: str) -> Tuple[str, str | None]: +def _split_host_port(host: str) -> tuple[str, str | None]: if ":" in host: h, p = host.split(":", 1) return h, p or None diff --git a/src/pkgmgr/actions/repository/create/planner.py b/src/pkgmgr/actions/repository/create/planner.py index ca43cde..2bcc944 100644 --- a/src/pkgmgr/actions/repository/create/planner.py +++ b/src/pkgmgr/actions/repository/create/planner.py @@ -1,7 +1,7 @@ from __future__ import annotations import os -from typing import Any, Dict +from typing import Any from .model import RepoParts @@ -38,7 +38,7 @@ class CreateRepoPlanner: *, author_name: str, author_email: str, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: return { "provider": self.parts.host, "port": self.parts.port, diff --git a/src/pkgmgr/actions/repository/create/service.py b/src/pkgmgr/actions/repository/create/service.py index 4a8e754..062d221 100644 --- a/src/pkgmgr/actions/repository/create/service.py +++ b/src/pkgmgr/actions/repository/create/service.py @@ -1,7 +1,7 @@ from __future__ import annotations import os -from typing import Any, Dict +from typing import Any from pkgmgr.core.git.queries import get_config_value @@ -17,7 +17,7 @@ class CreateRepoService: def __init__( self, *, - config_merged: Dict[str, Any], + config_merged: dict[str, Any], user_config_path: str, bin_dir: str, ): diff --git a/src/pkgmgr/actions/repository/create/templates.py b/src/pkgmgr/actions/repository/create/templates.py index ac849f1..e7e409b 100644 --- a/src/pkgmgr/actions/repository/create/templates.py +++ b/src/pkgmgr/actions/repository/create/templates.py @@ -25,7 +25,7 @@ class TemplateRenderer: self, *, repo_dir: str, - context: Dict[str, Any], + context: dict[str, Any], preview: bool, ) -> None: if preview: diff --git a/src/pkgmgr/actions/repository/list.py b/src/pkgmgr/actions/repository/list.py index 7b42ff9..ff72ed5 100644 --- a/src/pkgmgr/actions/repository/list.py +++ b/src/pkgmgr/actions/repository/list.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - """ Pretty-print repository list with status, categories, tags and path. @@ -15,9 +13,9 @@ from __future__ import annotations import os import re from textwrap import wrap -from typing import Any, Dict, List, Optional +from typing import Any -Repository = Dict[str, Any] +Repository = dict[str, Any] RESET = "\033[0m" BOLD = "\033[1m" @@ -29,7 +27,7 @@ MAGENTA = "\033[35m" GREY = "\033[90m" -def _compile_maybe_regex(pattern: str) -> Optional[re.Pattern[str]]: +def _compile_maybe_regex(pattern: str) -> re.Pattern[str] | None: """ If pattern is of the form /.../, return a compiled regex (case-insensitive). Otherwise return None. @@ -88,7 +86,7 @@ def _compute_status( """ Compute a human-readable status string, e.g. 'present,alias,ignored'. """ - parts: List[str] = [] + parts: list[str] = [] exists = os.path.isdir(repo_dir) if exists: @@ -128,7 +126,7 @@ def _color_status(status_padded: str) -> str: pad_spaces = len(status_padded) - len(core) plain_parts = core.split(",") if core else [] - colored_parts: List[str] = [] + colored_parts: list[str] = [] for raw_part in plain_parts: name = raw_part.strip() @@ -156,12 +154,12 @@ def _color_status(status_padded: str) -> str: def list_repositories( - repositories: List[Repository], + repositories: list[Repository], repositories_base_dir: str, binaries_dir: str, search_filter: str = "", status_filter: str = "", - extra_tags: Optional[List[str]] = None, + extra_tags: list[str] | None = None, show_description: bool = False, ) -> None: """ @@ -188,7 +186,7 @@ def list_repositories( extra_tags = [] search_regex = _compile_maybe_regex(search_filter) - rows: List[Dict[str, Any]] = [] + rows: list[dict[str, Any]] = [] # ------------------------------------------------------------------ # Build rows @@ -208,17 +206,7 @@ def list_repositories( continue if search_filter: - haystack = " ".join( - [ - identifier, - alias, - provider, - account, - description, - homepage, - repo_dir, - ] - ) + haystack = f"{identifier} {alias} {provider} {account} {description} {homepage} {repo_dir}" if search_regex: if not search_regex.search(haystack): continue @@ -226,13 +214,13 @@ def list_repositories( if search_filter.lower() not in haystack.lower(): continue - categories: List[str] = [] + categories: list[str] = [] categories.extend(map(str, repo.get("category_files", []))) if repo.get("category"): categories.append(str(repo["category"])) - yaml_tags: List[str] = list(map(str, repo.get("tags", []))) - display_tags: List[str] = sorted(set(yaml_tags + list(map(str, extra_tags)))) + yaml_tags: list[str] = list(map(str, repo.get("tags", []))) + display_tags: list[str] = sorted(set(yaml_tags + list(map(str, extra_tags)))) rows.append( { diff --git a/src/pkgmgr/actions/repository/pull.py b/src/pkgmgr/actions/repository/pull.py index 3983088..d66db30 100644 --- a/src/pkgmgr/actions/repository/pull.py +++ b/src/pkgmgr/actions/repository/pull.py @@ -2,7 +2,7 @@ from __future__ import annotations import os from concurrent.futures import ThreadPoolExecutor -from typing import Any, Dict, List, Tuple +from typing import Any from pkgmgr.actions.repository._parallel import RepoRef, run_on_repos from pkgmgr.core.git.commands import GitPullArgsError, pull_args @@ -10,10 +10,10 @@ from pkgmgr.core.repository.dir import get_repo_dir from pkgmgr.core.repository.identifier import get_repo_identifier from pkgmgr.core.repository.verify import verify_repository -Repository = Dict[str, Any] +Repository = dict[str, Any] -def _pull_one(repo_dir: str, extra_args: List[str], preview: bool) -> Tuple[bool, str]: +def _pull_one(repo_dir: str, extra_args: list[str], preview: bool) -> tuple[bool, str]: try: pull_args(extra_args, cwd=repo_dir, preview=preview) return (True, "") @@ -25,7 +25,7 @@ def _verify_one( repo: Repository, repo_dir: str, no_verification: bool, -) -> Tuple[bool, bool, List[str]]: +) -> tuple[bool, bool, list[str]]: """Returns (has_verified_info, verified_ok, errors).""" verified_ok, errors, _commit, _key = verify_repository( repo, @@ -37,10 +37,10 @@ def _verify_one( def _verify_all( - candidates: List[Tuple[Repository, str, str]], + candidates: list[tuple[Repository, str, str]], no_verification: bool, jobs: int, -) -> List[Tuple[str, str, bool, bool, List[str]]]: +) -> list[tuple[str, str, bool, bool, list[str]]]: """ Verify all candidates (parallel if ``jobs > 1``), preserving input order. @@ -63,10 +63,10 @@ def _verify_all( def pull_with_verification( - selected_repos: List[Repository], + selected_repos: list[Repository], repositories_base_dir: str, - all_repos: List[Repository], - extra_args: List[str], + all_repos: list[Repository], + extra_args: list[str], no_verification: bool, preview: bool, jobs: int = 1, @@ -80,7 +80,7 @@ def pull_with_verification( - Approved repos are then pulled in parallel when ``jobs > 1``. - On any pull failure, prints a summary and exits with status 1. """ - candidates: List[Tuple[Repository, str, str]] = [] + candidates: list[tuple[Repository, str, str]] = [] for repo in selected_repos: ident = get_repo_identifier(repo, all_repos) rd = get_repo_dir(repositories_base_dir, repo) @@ -94,7 +94,7 @@ def pull_with_verification( verify_results = _verify_all(candidates, no_verification, jobs) - approved: List[RepoRef] = [] + approved: list[RepoRef] = [] for ident, rd, has_verified_info, verified_ok, errors in verify_results: if ( not preview diff --git a/src/pkgmgr/actions/repository/push.py b/src/pkgmgr/actions/repository/push.py index f80dbc0..db645ce 100644 --- a/src/pkgmgr/actions/repository/push.py +++ b/src/pkgmgr/actions/repository/push.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Dict, List, Tuple +from typing import Any from pkgmgr.actions.repository._parallel import ( resolve_repos, @@ -8,10 +8,10 @@ from pkgmgr.actions.repository._parallel import ( ) from pkgmgr.core.git.commands import GitPushArgsError, push_args -Repository = Dict[str, Any] +Repository = dict[str, Any] -def _push_one(repo_dir: str, extra_args: List[str], preview: bool) -> Tuple[bool, str]: +def _push_one(repo_dir: str, extra_args: list[str], preview: bool) -> tuple[bool, str]: try: push_args(extra_args, cwd=repo_dir, preview=preview) return (True, "") @@ -20,10 +20,10 @@ def _push_one(repo_dir: str, extra_args: List[str], preview: bool) -> Tuple[bool def push_in_parallel( - selected_repos: List[Repository], + selected_repos: list[Repository], repositories_base_dir: str, - all_repos: List[Repository], - extra_args: List[str], + all_repos: list[Repository], + extra_args: list[str], preview: bool, jobs: int = 1, ) -> None: diff --git a/src/pkgmgr/actions/update/__init__.py b/src/pkgmgr/actions/update/__init__.py index e91f6d8..ace9e0e 100644 --- a/src/pkgmgr/actions/update/__init__.py +++ b/src/pkgmgr/actions/update/__init__.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - from __future__ import annotations from pkgmgr.actions.update.manager import UpdateManager diff --git a/src/pkgmgr/actions/update/manager.py b/src/pkgmgr/actions/update/manager.py index 5bc504e..cda49dc 100644 --- a/src/pkgmgr/actions/update/manager.py +++ b/src/pkgmgr/actions/update/manager.py @@ -1,9 +1,7 @@ -#!/usr/bin/env python3 - from __future__ import annotations from collections.abc import Iterable -from typing import Any, List, Tuple +from typing import Any from pkgmgr.actions.update.system_updater import SystemUpdater @@ -37,7 +35,7 @@ class UpdateManager: from pkgmgr.actions.repository.pull import pull_with_verification from pkgmgr.core.repository.identifier import get_repo_identifier - failures: List[Tuple[str, str]] = [] + failures: list[tuple[str, str]] = [] for repo in list(selected_repos): identifier = get_repo_identifier(repo, all_repos) diff --git a/src/pkgmgr/actions/update/os_release.py b/src/pkgmgr/actions/update/os_release.py index 641caff..7a8b7e6 100644 --- a/src/pkgmgr/actions/update/os_release.py +++ b/src/pkgmgr/actions/update/os_release.py @@ -1,21 +1,18 @@ -#!/usr/bin/env python3 - from __future__ import annotations import os from dataclasses import dataclass -from typing import Dict -def read_os_release(path: str = "/etc/os-release") -> Dict[str, str]: +def read_os_release(path: str = "/etc/os-release") -> dict[str, str]: """ Parse /etc/os-release into a dict. Returns empty dict if missing. """ if not os.path.exists(path): return {} - result: Dict[str, str] = {} - with open(path, "r", encoding="utf-8") as f: + result: dict[str, str] = {} + with open(path, encoding="utf-8") as f: for line in f: line = line.strip() if not line or line.startswith("#") or "=" not in line: diff --git a/src/pkgmgr/actions/update/system_updater.py b/src/pkgmgr/actions/update/system_updater.py index a2a5363..45a6bf3 100644 --- a/src/pkgmgr/actions/update/system_updater.py +++ b/src/pkgmgr/actions/update/system_updater.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - from __future__ import annotations import platform diff --git a/src/pkgmgr/cli/commands/changelog.py b/src/pkgmgr/cli/commands/changelog.py index 8f97356..acc3b84 100644 --- a/src/pkgmgr/cli/commands/changelog.py +++ b/src/pkgmgr/cli/commands/changelog.py @@ -64,7 +64,7 @@ def _find_previous_and_current_tag( def handle_changelog( args, ctx: CLIContext, - selected: List[Repository], + selected: list[Repository], ) -> None: """ Handle the 'changelog' command. diff --git a/src/pkgmgr/cli/commands/config.py b/src/pkgmgr/cli/commands/config.py index 462416e..289a5e0 100644 --- a/src/pkgmgr/cli/commands/config.py +++ b/src/pkgmgr/cli/commands/config.py @@ -1,6 +1,4 @@ # src/pkgmgr/cli/commands/config.py -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- from __future__ import annotations @@ -8,7 +6,7 @@ import os import shutil import sys from pathlib import Path -from typing import Any, Dict, Optional +from typing import Any import yaml @@ -21,7 +19,7 @@ from pkgmgr.core.config.save import save_user_config from pkgmgr.core.repository.resolve import resolve_repos -def _load_user_config(user_config_path: str) -> Dict[str, Any]: +def _load_user_config(user_config_path: str) -> dict[str, Any]: """ Load the user config from ~/.config/pkgmgr/config.yaml (or whatever ctx.user_config_path is), creating the directory if needed. @@ -32,12 +30,12 @@ def _load_user_config(user_config_path: str) -> Dict[str, Any]: os.makedirs(cfg_dir, exist_ok=True) if os.path.exists(user_config_path_expanded): - with open(user_config_path_expanded, "r", encoding="utf-8") as f: + with open(user_config_path_expanded, encoding="utf-8") as f: return yaml.safe_load(f) or {"repositories": []} return {"repositories": []} -def _find_defaults_source_dir() -> Optional[str]: +def _find_defaults_source_dir() -> str | None: """ Find the directory inside the installed pkgmgr package that contains the default config files. @@ -75,7 +73,7 @@ def _update_default_configs(user_config_path: str) -> None: for name in os.listdir(source_dir): lower = name.lower() - if not (lower.endswith(".yml") or lower.endswith(".yaml")): + if not (lower.endswith((".yml", ".yaml"))): continue if name == "config.yaml": continue diff --git a/src/pkgmgr/cli/commands/make.py b/src/pkgmgr/cli/commands/make.py index cf7c399..b8e163c 100644 --- a/src/pkgmgr/cli/commands/make.py +++ b/src/pkgmgr/cli/commands/make.py @@ -1,18 +1,18 @@ from __future__ import annotations import sys -from typing import Any, Dict, List +from typing import Any from pkgmgr.actions.proxy import exec_proxy_command from pkgmgr.cli.context import CLIContext -Repository = Dict[str, Any] +Repository = dict[str, Any] def handle_make( args, ctx: CLIContext, - selected: List[Repository], + selected: list[Repository], ) -> None: """ Handle the 'make' command by delegating to exec_proxy_command. diff --git a/src/pkgmgr/cli/commands/mirror.py b/src/pkgmgr/cli/commands/mirror.py index e341e17..31ec5fe 100644 --- a/src/pkgmgr/cli/commands/mirror.py +++ b/src/pkgmgr/cli/commands/mirror.py @@ -2,7 +2,7 @@ from __future__ import annotations import sys -from typing import Any, Dict, List +from typing import Any from pkgmgr.actions.mirror import ( diff_mirrors, @@ -13,13 +13,13 @@ from pkgmgr.actions.mirror import ( ) from pkgmgr.cli.context import CLIContext -Repository = Dict[str, Any] +Repository = dict[str, Any] def handle_mirror_command( ctx: CLIContext, args: Any, - selected: List[Repository], + selected: list[Repository], ) -> None: """ Entry point for 'pkgmgr mirror' subcommands. diff --git a/src/pkgmgr/cli/commands/publish.py b/src/pkgmgr/cli/commands/publish.py index d3f0203..5848f6b 100644 --- a/src/pkgmgr/cli/commands/publish.py +++ b/src/pkgmgr/cli/commands/publish.py @@ -1,17 +1,17 @@ from __future__ import annotations import os -from typing import Any, Dict, List +from typing import Any from pkgmgr.actions.publish import publish from pkgmgr.cli.context import CLIContext from pkgmgr.core.repository.dir import get_repo_dir from pkgmgr.core.repository.identifier import get_repo_identifier -Repository = Dict[str, Any] +Repository = dict[str, Any] -def handle_publish(args, ctx: CLIContext, selected: List[Repository]) -> None: +def handle_publish(args, ctx: CLIContext, selected: list[Repository]) -> None: if not selected: print("[pkgmgr] No repositories selected for publish.") return diff --git a/src/pkgmgr/cli/commands/release.py b/src/pkgmgr/cli/commands/release.py index 29d915e..4b3daab 100644 --- a/src/pkgmgr/cli/commands/release.py +++ b/src/pkgmgr/cli/commands/release.py @@ -1,10 +1,8 @@ -#!/usr/bin/env python3 - from __future__ import annotations import os import sys -from typing import Any, Dict, List +from typing import Any from pkgmgr.actions.publish import publish as run_publish from pkgmgr.actions.release import release as run_release @@ -12,13 +10,13 @@ from pkgmgr.cli.context import CLIContext from pkgmgr.core.repository.dir import get_repo_dir from pkgmgr.core.repository.identifier import get_repo_identifier -Repository = Dict[str, Any] +Repository = dict[str, Any] def handle_release( args, ctx: CLIContext, - selected: List[Repository], + selected: list[Repository], ) -> None: if not selected: print("[pkgmgr] No repositories selected for release.") diff --git a/src/pkgmgr/cli/commands/repos.py b/src/pkgmgr/cli/commands/repos.py index ea248de..e522d05 100644 --- a/src/pkgmgr/cli/commands/repos.py +++ b/src/pkgmgr/cli/commands/repos.py @@ -1,9 +1,7 @@ -#!/usr/bin/env python3 - from __future__ import annotations import sys -from typing import Any, Dict, List +from typing import Any from pkgmgr.actions.install import install_repos from pkgmgr.actions.repository.create import create_repo @@ -15,7 +13,7 @@ from pkgmgr.cli.context import CLIContext from pkgmgr.core.command.run import run_command from pkgmgr.core.repository.dir import get_repo_dir -Repository = Dict[str, Any] +Repository = dict[str, Any] def _resolve_repository_directory(repository: Repository, ctx: CLIContext) -> str: @@ -45,7 +43,7 @@ def _resolve_repository_directory(repository: Repository, ctx: CLIContext) -> st def handle_repos_command( args, ctx: CLIContext, - selected: List[Repository], + selected: list[Repository], ) -> None: """ Handle core repository commands (install/update/deinstall/delete/status/list/path/shell/create). diff --git a/src/pkgmgr/cli/commands/tools.py b/src/pkgmgr/cli/commands/tools.py index c81620a..ee55925 100644 --- a/src/pkgmgr/cli/commands/tools.py +++ b/src/pkgmgr/cli/commands/tools.py @@ -1,19 +1,19 @@ from __future__ import annotations -from typing import Any, Dict, List +from typing import Any from pkgmgr.cli.context import CLIContext from pkgmgr.cli.tools import open_vscode_workspace from pkgmgr.cli.tools.paths import resolve_repository_path from pkgmgr.core.command.run import run_command -Repository = Dict[str, Any] +Repository = dict[str, Any] def handle_tools_command( args, ctx: CLIContext, - selected: List[Repository], + selected: list[Repository], ) -> None: # ------------------------------------------------------------------ # nautilus "explore" command diff --git a/src/pkgmgr/cli/commands/version.py b/src/pkgmgr/cli/commands/version.py index d86bc8d..f8778dc 100644 --- a/src/pkgmgr/cli/commands/version.py +++ b/src/pkgmgr/cli/commands/version.py @@ -23,7 +23,7 @@ from pkgmgr.core.version.source import ( read_spec_version, ) -Repository = Dict[str, Any] +Repository = dict[str, Any] def _print_pkgmgr_self_version() -> None: @@ -74,7 +74,7 @@ def _print_pkgmgr_self_version() -> None: def handle_version( args, ctx: CLIContext, - selected: List[Repository], + selected: list[Repository], ) -> None: """ Handle the 'version' command. diff --git a/src/pkgmgr/cli/context.py b/src/pkgmgr/cli/context.py index ae64aab..ea56074 100644 --- a/src/pkgmgr/cli/context.py +++ b/src/pkgmgr/cli/context.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any, Dict, List +from typing import Any @dataclass @@ -13,8 +13,8 @@ class CLIContext: keeps the CLI layer thin and structured. """ - config_merged: Dict[str, Any] + config_merged: dict[str, Any] repositories_base_dir: str - all_repositories: List[Dict[str, Any]] + all_repositories: list[dict[str, Any]] binaries_dir: str user_config_path: str diff --git a/src/pkgmgr/cli/dispatch.py b/src/pkgmgr/cli/dispatch.py index db7f305..fd93f51 100644 --- a/src/pkgmgr/cli/dispatch.py +++ b/src/pkgmgr/cli/dispatch.py @@ -2,7 +2,7 @@ from __future__ import annotations import os import sys -from typing import Any, Dict, List +from typing import Any from pkgmgr.cli.commands import ( handle_archive, @@ -34,7 +34,7 @@ def _has_explicit_selection(args) -> bool: ) -def _select_repo_for_current_directory(ctx: CLIContext) -> List[Dict[str, Any]]: +def _select_repo_for_current_directory(ctx: CLIContext) -> list[dict[str, Any]]: cwd = os.path.abspath(os.getcwd()) matches = [] diff --git a/src/pkgmgr/cli/parser/branch_cmd.py b/src/pkgmgr/cli/parser/branch_cmd.py index d31545f..44709ac 100644 --- a/src/pkgmgr/cli/parser/branch_cmd.py +++ b/src/pkgmgr/cli/parser/branch_cmd.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - from __future__ import annotations import argparse diff --git a/src/pkgmgr/cli/parser/changelog_cmd.py b/src/pkgmgr/cli/parser/changelog_cmd.py index 8896f22..35c760b 100644 --- a/src/pkgmgr/cli/parser/changelog_cmd.py +++ b/src/pkgmgr/cli/parser/changelog_cmd.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - from __future__ import annotations import argparse diff --git a/src/pkgmgr/cli/parser/common.py b/src/pkgmgr/cli/parser/common.py index 2255ed1..5f11a7a 100644 --- a/src/pkgmgr/cli/parser/common.py +++ b/src/pkgmgr/cli/parser/common.py @@ -2,7 +2,6 @@ from __future__ import annotations import argparse -from typing import Optional, Tuple class SortedSubParsersAction(argparse._SubParsersAction): @@ -19,8 +18,8 @@ class SortedSubParsersAction(argparse._SubParsersAction): def _has_action( parser: argparse.ArgumentParser, *, - positional: Optional[str] = None, - options: Tuple[str, ...] = (), + positional: str | None = None, + options: tuple[str, ...] = (), ) -> bool: """ Check whether the parser already has an action. diff --git a/src/pkgmgr/cli/parser/config_cmd.py b/src/pkgmgr/cli/parser/config_cmd.py index 7d48c74..4b3f102 100644 --- a/src/pkgmgr/cli/parser/config_cmd.py +++ b/src/pkgmgr/cli/parser/config_cmd.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - from __future__ import annotations import argparse diff --git a/src/pkgmgr/cli/parser/install_update.py b/src/pkgmgr/cli/parser/install_update.py index b2d7897..f0696cd 100644 --- a/src/pkgmgr/cli/parser/install_update.py +++ b/src/pkgmgr/cli/parser/install_update.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - import argparse from pkgmgr.cli.parser.common import ( diff --git a/src/pkgmgr/cli/parser/list_cmd.py b/src/pkgmgr/cli/parser/list_cmd.py index 7eaead5..b6332b5 100644 --- a/src/pkgmgr/cli/parser/list_cmd.py +++ b/src/pkgmgr/cli/parser/list_cmd.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - from __future__ import annotations import argparse diff --git a/src/pkgmgr/cli/parser/make_cmd.py b/src/pkgmgr/cli/parser/make_cmd.py index 661b4b9..9fc9d82 100644 --- a/src/pkgmgr/cli/parser/make_cmd.py +++ b/src/pkgmgr/cli/parser/make_cmd.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - from __future__ import annotations import argparse diff --git a/src/pkgmgr/cli/parser/mirror_cmd.py b/src/pkgmgr/cli/parser/mirror_cmd.py index 2287afa..d4a8bf6 100644 --- a/src/pkgmgr/cli/parser/mirror_cmd.py +++ b/src/pkgmgr/cli/parser/mirror_cmd.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - from __future__ import annotations import argparse diff --git a/src/pkgmgr/cli/parser/navigation_cmd.py b/src/pkgmgr/cli/parser/navigation_cmd.py index 4bca222..cd1ca79 100644 --- a/src/pkgmgr/cli/parser/navigation_cmd.py +++ b/src/pkgmgr/cli/parser/navigation_cmd.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - from __future__ import annotations import argparse diff --git a/src/pkgmgr/cli/parser/release_cmd.py b/src/pkgmgr/cli/parser/release_cmd.py index 5f84078..a806b67 100644 --- a/src/pkgmgr/cli/parser/release_cmd.py +++ b/src/pkgmgr/cli/parser/release_cmd.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - from __future__ import annotations import argparse diff --git a/src/pkgmgr/cli/parser/version_cmd.py b/src/pkgmgr/cli/parser/version_cmd.py index 267c12a..e9eb743 100644 --- a/src/pkgmgr/cli/parser/version_cmd.py +++ b/src/pkgmgr/cli/parser/version_cmd.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - from __future__ import annotations import argparse diff --git a/src/pkgmgr/cli/proxy.py b/src/pkgmgr/cli/proxy.py index e219a72..2df33bf 100644 --- a/src/pkgmgr/cli/proxy.py +++ b/src/pkgmgr/cli/proxy.py @@ -1,11 +1,9 @@ -#!/usr/bin/env python3 - from __future__ import annotations import argparse import os import sys -from typing import Any, Dict, List +from typing import Any from pkgmgr.actions.proxy import exec_proxy_command from pkgmgr.actions.repository.clone import clone_repos @@ -15,7 +13,7 @@ from pkgmgr.cli.context import CLIContext from pkgmgr.core.repository.dir import get_repo_dir from pkgmgr.core.repository.selected import get_selected_repos -PROXY_COMMANDS: Dict[str, List[str]] = { +PROXY_COMMANDS: dict[str, list[str]] = { "git": [ "pull", "push", diff --git a/src/pkgmgr/cli/tools/paths.py b/src/pkgmgr/cli/tools/paths.py index 174e371..198a4ea 100644 --- a/src/pkgmgr/cli/tools/paths.py +++ b/src/pkgmgr/cli/tools/paths.py @@ -1,11 +1,11 @@ from __future__ import annotations -from typing import Any, Dict +from typing import Any from pkgmgr.cli.context import CLIContext from pkgmgr.core.repository.dir import get_repo_dir -Repository = Dict[str, Any] +Repository = dict[str, Any] def resolve_repository_path(repository: Repository, ctx: CLIContext) -> str: diff --git a/src/pkgmgr/cli/tools/vscode.py b/src/pkgmgr/cli/tools/vscode.py index 2f0065d..01cffca 100644 --- a/src/pkgmgr/cli/tools/vscode.py +++ b/src/pkgmgr/cli/tools/vscode.py @@ -3,14 +3,14 @@ from __future__ import annotations import json import os import shutil -from typing import Any, Dict, List +from typing import Any from pkgmgr.cli.context import CLIContext from pkgmgr.cli.tools.paths import resolve_repository_path from pkgmgr.core.command.run import run_command from pkgmgr.core.repository.identifier import get_repo_identifier -Repository = Dict[str, Any] +Repository = dict[str, Any] def _ensure_vscode_cli_available() -> None: @@ -26,7 +26,7 @@ def _ensure_vscode_cli_available() -> None: ) -def _ensure_identifiers_are_filename_safe(identifiers: List[str]) -> None: +def _ensure_identifiers_are_filename_safe(identifiers: list[str]) -> None: """ Ensure identifiers can be used in a filename. @@ -52,14 +52,14 @@ def _resolve_workspaces_dir(ctx: CLIContext) -> str: return os.path.expanduser(directories_cfg.get("workspaces", "~/Workspaces")) -def _build_workspace_filename(identifiers: List[str]) -> str: +def _build_workspace_filename(identifiers: list[str]) -> str: sorted_identifiers = sorted(identifiers) return "_".join(sorted_identifiers) + ".code-workspace" def _build_workspace_data( - selected: List[Repository], ctx: CLIContext -) -> Dict[str, Any]: + selected: list[Repository], ctx: CLIContext +) -> dict[str, Any]: folders = [{"path": resolve_repository_path(repo, ctx)} for repo in selected] return { "folders": folders, @@ -67,7 +67,7 @@ def _build_workspace_data( } -def open_vscode_workspace(ctx: CLIContext, selected: List[Repository]) -> None: +def open_vscode_workspace(ctx: CLIContext, selected: list[Repository]) -> None: """ Create (if missing) and open a VS Code workspace for the selected repositories. diff --git a/src/pkgmgr/core/command/ink.py b/src/pkgmgr/core/command/ink.py index 9d48178..3cc1b4b 100644 --- a/src/pkgmgr/core/command/ink.py +++ b/src/pkgmgr/core/command/ink.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - import os from pkgmgr.core.repository.dir import get_repo_dir diff --git a/src/pkgmgr/core/command/resolve.py b/src/pkgmgr/core/command/resolve.py index 7bfb583..f837324 100644 --- a/src/pkgmgr/core/command/resolve.py +++ b/src/pkgmgr/core/command/resolve.py @@ -1,15 +1,17 @@ +from __future__ import annotations + import os import shutil -from typing import Any, Dict, List, Optional +from typing import Any -Repository = Dict[str, Any] +Repository = dict[str, Any] def _is_executable(path: str) -> bool: return os.path.exists(path) and os.access(path, os.X_OK) -def _find_python_package_root(repo_dir: str) -> Optional[str]: +def _find_python_package_root(repo_dir: str) -> str | None: """ Detect a Python src-layout package: @@ -29,19 +31,19 @@ def _find_python_package_root(repo_dir: str) -> Optional[str]: return None -def _nix_binary_candidates(home: str, names: List[str]) -> List[str]: +def _nix_binary_candidates(home: str, names: list[str]) -> list[str]: """ Build possible Nix profile binary paths for a list of candidate names. """ return [os.path.join(home, ".nix-profile", "bin", name) for name in names if name] -def _path_binary_candidates(names: List[str]) -> List[str]: +def _path_binary_candidates(names: list[str]) -> list[str]: """ Resolve candidate names via PATH using shutil.which. Returns only existing, executable paths. """ - binaries: List[str] = [] + binaries: list[str] = [] for name in names: if not name: continue @@ -55,7 +57,7 @@ def resolve_command_for_repo( repo: Repository, repo_identifier: str, repo_dir: str, -) -> Optional[str]: +) -> str | None: """ Resolve the executable command for a repository. @@ -110,7 +112,7 @@ def resolve_command_for_repo( else: python_package_name = None - candidate_names: List[str] = [] + candidate_names: list[str] = [] seen: set[str] = set() for name in ( @@ -130,8 +132,8 @@ def resolve_command_for_repo( path_binaries = _path_binary_candidates(candidate_names) # b) Classify system (/usr/...) vs non-system - system_binary: Optional[str] = None - non_system_binary: Optional[str] = None + system_binary: str | None = None + non_system_binary: str | None = None for bin_path in path_binaries: if bin_path.startswith("/usr"): diff --git a/src/pkgmgr/core/command/run.py b/src/pkgmgr/core/command/run.py index 9c7ec53..9786714 100644 --- a/src/pkgmgr/core/command/run.py +++ b/src/pkgmgr/core/command/run.py @@ -1,16 +1,17 @@ from __future__ import annotations +import contextlib import selectors import subprocess import sys -from typing import List, Optional, Union +from typing import Union -CommandType = Union[str, List[str]] +CommandType = Union[str, list[str]] def run_command( cmd: CommandType, - cwd: Optional[str] = None, + cwd: str | None = None, preview: bool = False, allow_failure: bool = False, ) -> subprocess.CompletedProcess: @@ -48,8 +49,8 @@ def run_command( sel.register(process.stdout, selectors.EVENT_READ, data="stdout") sel.register(process.stderr, selectors.EVENT_READ, data="stderr") - stdout_lines: List[str] = [] - stderr_lines: List[str] = [] + stdout_lines: list[str] = [] + stderr_lines: list[str] = [] try: while sel.get_map(): diff --git a/src/pkgmgr/core/config/load.py b/src/pkgmgr/core/config/load.py index dca19fc..62d4c04 100644 --- a/src/pkgmgr/core/config/load.py +++ b/src/pkgmgr/core/config/load.py @@ -1,6 +1,4 @@ # src/pkgmgr/core/config/load.py -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- """ Load and merge pkgmgr configuration. @@ -38,11 +36,11 @@ from __future__ import annotations import os from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple +from typing import Any import yaml -Repo = Dict[str, Any] +Repo = dict[str, Any] # --------------------------------------------------------------------------- @@ -50,7 +48,7 @@ Repo = Dict[str, Any] # --------------------------------------------------------------------------- -def _deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]: +def _deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]: """ Recursively merge two dictionaries. @@ -64,7 +62,7 @@ def _deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any return base -def _repo_key(repo: Repo) -> Tuple[str, str, str]: +def _repo_key(repo: Repo) -> tuple[str, str, str]: """ Normalised key for identifying a repository across config files. """ @@ -76,10 +74,10 @@ def _repo_key(repo: Repo) -> Tuple[str, str, str]: def _merge_repo_lists( - base_list: List[Repo], - new_list: List[Repo], - category_name: Optional[str] = None, -) -> List[Repo]: + base_list: list[Repo], + new_list: list[Repo], + category_name: str | None = None, +) -> list[Repo]: """ Merge two repository lists, matching by (provider, account, repository). @@ -87,7 +85,7 @@ def _merge_repo_lists( - If it exists, its fields are deep-merged (override wins). - If category_name is set, it is appended to repo["category_files"]. """ - index: Dict[Tuple[str, str, str], Repo] = {_repo_key(r): r for r in base_list} + index: dict[tuple[str, str, str], Repo] = {_repo_key(r): r for r in base_list} for src in new_list: key = _repo_key(src) @@ -120,7 +118,7 @@ def _merge_repo_lists( return base_list -def _load_yaml_file(path: Path) -> Dict[str, Any]: +def _load_yaml_file(path: Path) -> dict[str, Any]: """ Load a single YAML file as dict. Non-dicts yield {}. """ @@ -135,8 +133,8 @@ def _load_yaml_file(path: Path) -> Dict[str, Any]: def _load_layer_dir( config_dir: Path, - skip_filename: Optional[str] = None, -) -> Dict[str, Any]: + skip_filename: str | None = None, +) -> dict[str, Any]: """ Load all *.yml/*.yaml from a directory as layered defaults. @@ -148,7 +146,7 @@ def _load_layer_dir( "repositories": [...], } """ - defaults: Dict[str, Any] = {"directories": {}, "repositories": []} + defaults: dict[str, Any] = {"directories": {}, "repositories": []} if not config_dir.is_dir(): return defaults @@ -186,7 +184,7 @@ def _load_layer_dir( return defaults -def _load_defaults_from_package_or_project() -> Dict[str, Any]: +def _load_defaults_from_package_or_project() -> dict[str, Any]: """ Fallback: load default configs from possible install or dev layouts. @@ -224,7 +222,7 @@ def _load_defaults_from_package_or_project() -> Dict[str, Any]: # --------------------------------------------------------------------------- -def load_config(user_config_path: str) -> Dict[str, Any]: +def load_config(user_config_path: str) -> dict[str, Any]: """ Load and merge configuration for pkgmgr. @@ -258,14 +256,14 @@ def load_config(user_config_path: str) -> Dict[str, Any]: defaults.setdefault("repositories", []) # 4) User config - user_cfg: Dict[str, Any] = {} + user_cfg: dict[str, Any] = {} if user_cfg_path.is_file(): user_cfg = _load_yaml_file(user_cfg_path) user_cfg.setdefault("directories", {}) user_cfg.setdefault("repositories", []) # 5) Merge - merged: Dict[str, Any] = {} + merged: dict[str, Any] = {} merged["directories"] = {} _deep_merge(merged["directories"], defaults["directories"]) diff --git a/src/pkgmgr/core/credentials/providers/env.py b/src/pkgmgr/core/credentials/providers/env.py index 7f2a8b8..9e0e852 100644 --- a/src/pkgmgr/core/credentials/providers/env.py +++ b/src/pkgmgr/core/credentials/providers/env.py @@ -3,7 +3,6 @@ from __future__ import annotations import os from dataclasses import dataclass -from typing import Optional from ..store_keys import env_var_candidates from ..types import TokenRequest, TokenResult @@ -15,7 +14,7 @@ class EnvTokenProvider: source_name: str = "env" - def get(self, request: TokenRequest) -> Optional[TokenResult]: + def get(self, request: TokenRequest) -> TokenResult | None: for key in env_var_candidates( request.provider_kind, request.host, request.owner ): diff --git a/src/pkgmgr/core/credentials/providers/gh.py b/src/pkgmgr/core/credentials/providers/gh.py index 7ad2973..7de4b27 100644 --- a/src/pkgmgr/core/credentials/providers/gh.py +++ b/src/pkgmgr/core/credentials/providers/gh.py @@ -3,7 +3,6 @@ from __future__ import annotations import shutil import subprocess from dataclasses import dataclass -from typing import Optional from ..types import TokenRequest, TokenResult @@ -18,7 +17,7 @@ class GhTokenProvider: source_name: str = "gh" - def get(self, request: TokenRequest) -> Optional[TokenResult]: + def get(self, request: TokenRequest) -> TokenResult | None: # Only meaningful for GitHub-like providers kind = (request.provider_kind or "").strip().lower() if kind not in ("github", "github.com"): diff --git a/src/pkgmgr/core/credentials/providers/prompt.py b/src/pkgmgr/core/credentials/providers/prompt.py index 4d03d6f..1268829 100644 --- a/src/pkgmgr/core/credentials/providers/prompt.py +++ b/src/pkgmgr/core/credentials/providers/prompt.py @@ -4,12 +4,11 @@ from __future__ import annotations import sys from dataclasses import dataclass from getpass import getpass -from typing import Optional from ..types import TokenRequest, TokenResult -def _token_help_url(provider_kind: str, host: str) -> Optional[str]: +def _token_help_url(provider_kind: str, host: str) -> str | None: """ Return a provider-specific URL where a user can create/get an API token. @@ -51,7 +50,7 @@ class PromptTokenProvider: source_name: str = "prompt" - def get(self, request: TokenRequest) -> Optional[TokenResult]: + def get(self, request: TokenRequest) -> TokenResult | None: if not sys.stdin.isatty(): return None diff --git a/src/pkgmgr/core/credentials/store_keys.py b/src/pkgmgr/core/credentials/store_keys.py index e0510c2..2c68354 100644 --- a/src/pkgmgr/core/credentials/store_keys.py +++ b/src/pkgmgr/core/credentials/store_keys.py @@ -2,7 +2,6 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Optional @dataclass(frozen=True) @@ -14,7 +13,7 @@ class KeyringKey: def build_keyring_key( - provider_kind: str, host: str, owner: Optional[str] + provider_kind: str, host: str, owner: str | None ) -> KeyringKey: """Build a stable keyring key. @@ -30,7 +29,7 @@ def build_keyring_key( def env_var_candidates( - provider_kind: str, host: str, owner: Optional[str] + provider_kind: str, host: str, owner: str | None ) -> list[str]: """Return a list of environment variable names to try. diff --git a/src/pkgmgr/core/git/commands/add.py b/src/pkgmgr/core/git/commands/add.py index 41e9be7..1860d26 100644 --- a/src/pkgmgr/core/git/commands/add.py +++ b/src/pkgmgr/core/git/commands/add.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections.abc import Iterable, Sequence -from typing import List, Union +from typing import Union from ..errors import GitCommandError, GitRunError from ..run import run @@ -14,7 +14,7 @@ class GitAddError(GitCommandError): PathLike = Union[str, Sequence[str], Iterable[str]] -def _normalize_paths(paths: PathLike) -> List[str]: +def _normalize_paths(paths: PathLike) -> list[str]: if isinstance(paths, str): return [paths] return [p for p in paths] diff --git a/src/pkgmgr/core/git/commands/clone.py b/src/pkgmgr/core/git/commands/clone.py index 434ada3..64d9c70 100644 --- a/src/pkgmgr/core/git/commands/clone.py +++ b/src/pkgmgr/core/git/commands/clone.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import List - from ..errors import GitCommandError, GitRunError from ..run import run @@ -11,7 +9,7 @@ class GitCloneError(GitCommandError): def clone( - args: List[str], + args: list[str], *, cwd: str = ".", preview: bool = False, diff --git a/src/pkgmgr/core/git/commands/pull_args.py b/src/pkgmgr/core/git/commands/pull_args.py index 374ab75..cb51947 100644 --- a/src/pkgmgr/core/git/commands/pull_args.py +++ b/src/pkgmgr/core/git/commands/pull_args.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import List - from ..errors import GitCommandError, GitRunError from ..run import run @@ -11,7 +9,7 @@ class GitPullArgsError(GitCommandError): def pull_args( - args: List[str] | None = None, + args: list[str] | None = None, *, cwd: str = ".", preview: bool = False, diff --git a/src/pkgmgr/core/git/commands/push_args.py b/src/pkgmgr/core/git/commands/push_args.py index c455482..fb36daa 100644 --- a/src/pkgmgr/core/git/commands/push_args.py +++ b/src/pkgmgr/core/git/commands/push_args.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import List - from ..errors import GitCommandError, GitRunError from ..run import run @@ -11,7 +9,7 @@ class GitPushArgsError(GitCommandError): def push_args( - args: List[str] | None = None, + args: list[str] | None = None, *, cwd: str = ".", preview: bool = False, diff --git a/src/pkgmgr/core/git/queries/get_changelog.py b/src/pkgmgr/core/git/queries/get_changelog.py index 750f96b..88ab5e0 100644 --- a/src/pkgmgr/core/git/queries/get_changelog.py +++ b/src/pkgmgr/core/git/queries/get_changelog.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import Optional - from ..errors import GitQueryError, GitRunError from ..run import run @@ -13,8 +11,8 @@ class GitChangelogQueryError(GitQueryError): def get_changelog( *, cwd: str, - from_ref: Optional[str] = None, - to_ref: Optional[str] = None, + from_ref: str | None = None, + to_ref: str | None = None, include_merges: bool = False, ) -> str: """ diff --git a/src/pkgmgr/core/git/queries/get_config_value.py b/src/pkgmgr/core/git/queries/get_config_value.py index 3869c3b..118aaee 100644 --- a/src/pkgmgr/core/git/queries/get_config_value.py +++ b/src/pkgmgr/core/git/queries/get_config_value.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import Optional - from ..errors import GitRunError from ..run import run @@ -17,7 +15,7 @@ def _is_missing_key_error(exc: GitRunError) -> bool: return "exit code: 1" in msg -def get_config_value(key: str, *, cwd: str = ".") -> Optional[str]: +def get_config_value(key: str, *, cwd: str = ".") -> str | None: """ Return a value from `git config --get `, or None if not set. diff --git a/src/pkgmgr/core/git/queries/get_current_branch.py b/src/pkgmgr/core/git/queries/get_current_branch.py index b63b090..3d1f38c 100644 --- a/src/pkgmgr/core/git/queries/get_current_branch.py +++ b/src/pkgmgr/core/git/queries/get_current_branch.py @@ -1,12 +1,10 @@ from __future__ import annotations -from typing import Optional - from ..errors import GitRunError from ..run import run -def get_current_branch(cwd: str = ".") -> Optional[str]: +def get_current_branch(cwd: str = ".") -> str | None: """ Return the current branch name, or None if it cannot be determined. diff --git a/src/pkgmgr/core/git/queries/get_head_commit.py b/src/pkgmgr/core/git/queries/get_head_commit.py index 84d4117..11d98ee 100644 --- a/src/pkgmgr/core/git/queries/get_head_commit.py +++ b/src/pkgmgr/core/git/queries/get_head_commit.py @@ -1,12 +1,10 @@ from __future__ import annotations -from typing import Optional - from ..errors import GitRunError from ..run import run -def get_head_commit(cwd: str = ".") -> Optional[str]: +def get_head_commit(cwd: str = ".") -> str | None: """ Return the current HEAD commit hash, or None if it cannot be determined. """ diff --git a/src/pkgmgr/core/git/queries/get_latest_commit.py b/src/pkgmgr/core/git/queries/get_latest_commit.py index a0a82c6..9cbef8f 100644 --- a/src/pkgmgr/core/git/queries/get_latest_commit.py +++ b/src/pkgmgr/core/git/queries/get_latest_commit.py @@ -1,12 +1,10 @@ from __future__ import annotations -from typing import Optional - from ..errors import GitRunError from ..run import run -def get_latest_commit(cwd: str = ".") -> Optional[str]: +def get_latest_commit(cwd: str = ".") -> str | None: """ Return the latest commit hash for the repository in `cwd`. diff --git a/src/pkgmgr/core/git/queries/get_latest_signing_key.py b/src/pkgmgr/core/git/queries/get_latest_signing_key.py index a99b7f0..c4bb681 100644 --- a/src/pkgmgr/core/git/queries/get_latest_signing_key.py +++ b/src/pkgmgr/core/git/queries/get_latest_signing_key.py @@ -43,8 +43,7 @@ def get_latest_signing_key(*, cwd: str = ".") -> str: cmd, cwd=cwd, check=False, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + capture_output=True, text=True, ) except OSError as exc: diff --git a/src/pkgmgr/core/git/queries/get_remote_push_urls.py b/src/pkgmgr/core/git/queries/get_remote_push_urls.py index 629ac47..e92e4e8 100644 --- a/src/pkgmgr/core/git/queries/get_remote_push_urls.py +++ b/src/pkgmgr/core/git/queries/get_remote_push_urls.py @@ -1,11 +1,9 @@ from __future__ import annotations -from typing import Set - from ..run import run -def get_remote_push_urls(remote: str, cwd: str = ".") -> Set[str]: +def get_remote_push_urls(remote: str, cwd: str = ".") -> set[str]: """ Return all push URLs configured for a remote. diff --git a/src/pkgmgr/core/git/queries/get_repo_root.py b/src/pkgmgr/core/git/queries/get_repo_root.py index 55ae311..d85bc74 100644 --- a/src/pkgmgr/core/git/queries/get_repo_root.py +++ b/src/pkgmgr/core/git/queries/get_repo_root.py @@ -1,12 +1,10 @@ from __future__ import annotations -from typing import Optional - from ..errors import GitRunError from ..run import run -def get_repo_root(*, cwd: str = ".") -> Optional[str]: +def get_repo_root(*, cwd: str = ".") -> str | None: """ Return the git repository root directory (top-level), or None if not available. diff --git a/src/pkgmgr/core/git/queries/get_tags.py b/src/pkgmgr/core/git/queries/get_tags.py index 4ebc19c..0a2c345 100644 --- a/src/pkgmgr/core/git/queries/get_tags.py +++ b/src/pkgmgr/core/git/queries/get_tags.py @@ -1,12 +1,10 @@ from __future__ import annotations -from typing import List - from ..errors import GitRunError from ..run import run -def get_tags(cwd: str = ".") -> List[str]: +def get_tags(cwd: str = ".") -> list[str]: """ Return a list of all tags in the repository in `cwd`. diff --git a/src/pkgmgr/core/git/queries/get_tags_at_ref.py b/src/pkgmgr/core/git/queries/get_tags_at_ref.py index 8ce74c7..916e57d 100644 --- a/src/pkgmgr/core/git/queries/get_tags_at_ref.py +++ b/src/pkgmgr/core/git/queries/get_tags_at_ref.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import List - from ..errors import GitQueryError, GitRunError from ..run import run @@ -10,7 +8,7 @@ class GitTagsAtRefQueryError(GitQueryError): """Raised when querying tags for a ref fails.""" -def get_tags_at_ref(ref: str, *, cwd: str = ".") -> List[str]: +def get_tags_at_ref(ref: str, *, cwd: str = ".") -> list[str]: """ Return all git tags pointing at a given ref. diff --git a/src/pkgmgr/core/git/queries/get_upstream_ref.py b/src/pkgmgr/core/git/queries/get_upstream_ref.py index ef6dd1d..81c3710 100644 --- a/src/pkgmgr/core/git/queries/get_upstream_ref.py +++ b/src/pkgmgr/core/git/queries/get_upstream_ref.py @@ -1,12 +1,10 @@ from __future__ import annotations -from typing import Optional - from ..errors import GitRunError from ..run import run -def get_upstream_ref(*, cwd: str = ".") -> Optional[str]: +def get_upstream_ref(*, cwd: str = ".") -> str | None: """ Return the configured upstream ref for the current branch, or None if none. diff --git a/src/pkgmgr/core/git/queries/list_remotes.py b/src/pkgmgr/core/git/queries/list_remotes.py index c982124..83ba7ec 100644 --- a/src/pkgmgr/core/git/queries/list_remotes.py +++ b/src/pkgmgr/core/git/queries/list_remotes.py @@ -1,11 +1,9 @@ from __future__ import annotations -from typing import List - from ..run import run -def list_remotes(cwd: str = ".") -> List[str]: +def list_remotes(cwd: str = ".") -> list[str]: """ Return a list of configured git remotes (e.g. ['origin', 'upstream']). diff --git a/src/pkgmgr/core/git/queries/list_tags.py b/src/pkgmgr/core/git/queries/list_tags.py index 8d57c9d..2cf34e4 100644 --- a/src/pkgmgr/core/git/queries/list_tags.py +++ b/src/pkgmgr/core/git/queries/list_tags.py @@ -1,11 +1,9 @@ from __future__ import annotations -from typing import List - from ..run import run -def list_tags(pattern: str = "*", *, cwd: str = ".") -> List[str]: +def list_tags(pattern: str = "*", *, cwd: str = ".") -> list[str]: """ List tags matching a pattern. diff --git a/src/pkgmgr/core/git/queries/probe_remote_reachable.py b/src/pkgmgr/core/git/queries/probe_remote_reachable.py index 350a0d2..56a6509 100644 --- a/src/pkgmgr/core/git/queries/probe_remote_reachable.py +++ b/src/pkgmgr/core/git/queries/probe_remote_reachable.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import Tuple - from ..errors import GitRunError from ..run import run @@ -91,7 +89,7 @@ def _format_reason(exc: GitRunError, *, url: str) -> str: return reason.strip() -def probe_remote_reachable_detail(url: str, cwd: str = ".") -> Tuple[bool, str]: +def probe_remote_reachable_detail(url: str, cwd: str = ".") -> tuple[bool, str]: """ Probe whether a remote URL is reachable. diff --git a/src/pkgmgr/core/git/run.py b/src/pkgmgr/core/git/run.py index 9c93682..a0d5d2c 100644 --- a/src/pkgmgr/core/git/run.py +++ b/src/pkgmgr/core/git/run.py @@ -1,7 +1,6 @@ from __future__ import annotations import subprocess -from typing import List from .errors import GitNotRepositoryError, GitRunError @@ -12,7 +11,7 @@ def _is_not_repo_error(stderr: str) -> bool: def run( - args: List[str], + args: list[str], *, cwd: str = ".", preview: bool = False, @@ -36,8 +35,7 @@ def run( cmd, cwd=cwd, check=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + capture_output=True, text=True, ) except subprocess.CalledProcessError as exc: diff --git a/src/pkgmgr/core/remote_provisioning/ensure.py b/src/pkgmgr/core/remote_provisioning/ensure.py index f1335b6..6b03355 100644 --- a/src/pkgmgr/core/remote_provisioning/ensure.py +++ b/src/pkgmgr/core/remote_provisioning/ensure.py @@ -2,7 +2,6 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Optional from pkgmgr.core.credentials.resolver import ResolutionOptions, TokenResolver @@ -45,10 +44,10 @@ def _raise_mapped_http_error(exc: HttpError, host: str) -> None: def ensure_remote_repo( spec: RepoSpec, - provider_hint: Optional[ProviderHint] = None, - options: Optional[EnsureOptions] = None, - registry: Optional[ProviderRegistry] = None, - token_resolver: Optional[TokenResolver] = None, + provider_hint: ProviderHint | None = None, + options: EnsureOptions | None = None, + registry: ProviderRegistry | None = None, + token_resolver: TokenResolver | None = None, ) -> EnsureResult: """Ensure that the remote repository exists (create if missing). diff --git a/src/pkgmgr/core/remote_provisioning/http/client.py b/src/pkgmgr/core/remote_provisioning/http/client.py index 6e34472..42bfe0e 100644 --- a/src/pkgmgr/core/remote_provisioning/http/client.py +++ b/src/pkgmgr/core/remote_provisioning/http/client.py @@ -6,7 +6,7 @@ import ssl import urllib.error import urllib.request from dataclasses import dataclass -from typing import Any, Dict, Optional +from typing import Any from .errors import HttpError @@ -15,7 +15,7 @@ from .errors import HttpError class HttpResponse: status: int text: str - json: Optional[Dict[str, Any]] = None + json: dict[str, Any] | None = None class HttpClient: @@ -28,11 +28,11 @@ class HttpClient: self, method: str, url: str, - headers: Optional[Dict[str, str]] = None, - payload: Optional[Dict[str, Any]] = None, + headers: dict[str, str] | None = None, + payload: dict[str, Any] | None = None, ) -> HttpResponse: - data: Optional[bytes] = None - final_headers: Dict[str, str] = dict(headers or {}) + data: bytes | None = None + final_headers: dict[str, str] = dict(headers or {}) if payload is not None: data = json.dumps(payload).encode("utf-8") diff --git a/src/pkgmgr/core/remote_provisioning/providers/base.py b/src/pkgmgr/core/remote_provisioning/providers/base.py index 96da7ed..5f479d3 100644 --- a/src/pkgmgr/core/remote_provisioning/providers/base.py +++ b/src/pkgmgr/core/remote_provisioning/providers/base.py @@ -49,6 +49,6 @@ class RemoteProvider(ABC): @staticmethod def _api_base(host: str) -> str: # Default to https. If you need http for local dev, store host as "http://..." - if host.startswith("http://") or host.startswith("https://"): + if host.startswith(("http://", "https://")): return host.rstrip("/") return f"https://{host}".rstrip("/") diff --git a/src/pkgmgr/core/remote_provisioning/providers/gitea.py b/src/pkgmgr/core/remote_provisioning/providers/gitea.py index 1c89b39..4835c68 100644 --- a/src/pkgmgr/core/remote_provisioning/providers/gitea.py +++ b/src/pkgmgr/core/remote_provisioning/providers/gitea.py @@ -1,7 +1,7 @@ # src/pkgmgr/core/remote_provisioning/providers/gitea.py from __future__ import annotations -from typing import Any, Dict +from typing import Any from ..http.client import HttpClient from ..http.errors import HttpError @@ -25,11 +25,9 @@ class GiteaProvider(RemoteProvider): - If you add more providers later, tighten this heuristic or use provider hints. """ h = host.lower() - if h in ("github.com", "api.github.com") or h.endswith(".github.com"): - return False - return True + return not (h in ("github.com", "api.github.com") or h.endswith(".github.com")) - def _headers(self, token: str) -> Dict[str, str]: + def _headers(self, token: str) -> dict[str, str]: """ Gitea commonly supports: Authorization: token @@ -70,7 +68,7 @@ class GiteaProvider(RemoteProvider): def set_repo_private(self, token: str, spec: RepoSpec, *, private: bool) -> None: base = self._api_base(spec.host) url = f"{base}/api/v1/repos/{spec.owner}/{spec.name}" - payload: Dict[str, Any] = {"private": bool(private)} + payload: dict[str, Any] = {"private": bool(private)} resp = self._http.request_json( "PATCH", @@ -88,7 +86,7 @@ class GiteaProvider(RemoteProvider): def create_repo(self, token: str, spec: RepoSpec) -> EnsureResult: base = self._api_base(spec.host) - payload: Dict[str, Any] = { + payload: dict[str, Any] = { "name": spec.name, "private": bool(spec.private), } diff --git a/src/pkgmgr/core/remote_provisioning/providers/github.py b/src/pkgmgr/core/remote_provisioning/providers/github.py index 832b39d..7374477 100644 --- a/src/pkgmgr/core/remote_provisioning/providers/github.py +++ b/src/pkgmgr/core/remote_provisioning/providers/github.py @@ -1,7 +1,7 @@ # src/pkgmgr/core/remote_provisioning/providers/github.py from __future__ import annotations -from typing import Any, Dict +from typing import Any from ..http.client import HttpClient from ..http.errors import HttpError @@ -32,11 +32,11 @@ class GitHubProvider(RemoteProvider): return "https://api.github.com" # Enterprise instance: - if host.startswith("http://") or host.startswith("https://"): + if host.startswith(("http://", "https://")): return host.rstrip("/") + "/api/v3" return f"https://{host}/api/v3" - def _headers(self, token: str) -> Dict[str, str]: + def _headers(self, token: str) -> dict[str, str]: return { "Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json", @@ -72,7 +72,7 @@ class GitHubProvider(RemoteProvider): def set_repo_private(self, token: str, spec: RepoSpec, *, private: bool) -> None: api = self._api_base(spec.host) url = f"{api}/repos/{spec.owner}/{spec.name}" - payload: Dict[str, Any] = {"private": bool(private)} + payload: dict[str, Any] = {"private": bool(private)} resp = self._http.request_json( "PATCH", @@ -90,7 +90,7 @@ class GitHubProvider(RemoteProvider): def create_repo(self, token: str, spec: RepoSpec) -> EnsureResult: api = self._api_base(spec.host) - payload: Dict[str, Any] = { + payload: dict[str, Any] = { "name": spec.name, "private": bool(spec.private), } diff --git a/src/pkgmgr/core/remote_provisioning/registry.py b/src/pkgmgr/core/remote_provisioning/registry.py index 04de497..1ffc01b 100644 --- a/src/pkgmgr/core/remote_provisioning/registry.py +++ b/src/pkgmgr/core/remote_provisioning/registry.py @@ -2,7 +2,6 @@ from __future__ import annotations from dataclasses import dataclass -from typing import List, Optional from .providers.base import RemoteProvider from .providers.gitea import GiteaProvider diff --git a/src/pkgmgr/core/remote_provisioning/types.py b/src/pkgmgr/core/remote_provisioning/types.py index 46b5afd..77befc3 100644 --- a/src/pkgmgr/core/remote_provisioning/types.py +++ b/src/pkgmgr/core/remote_provisioning/types.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Literal, Optional +from typing import Literal EnsureStatus = Literal[ "exists", @@ -18,7 +18,7 @@ EnsureStatus = Literal[ class ProviderHint: """Optional hint to force a provider kind.""" - kind: Optional[str] = None # e.g. "gitea" or "github" + kind: str | None = None # e.g. "gitea" or "github" @dataclass(frozen=True) @@ -30,14 +30,14 @@ class RepoSpec: name: str private: bool = True description: str = "" - default_branch: Optional[str] = None + default_branch: str | None = None @dataclass(frozen=True) class EnsureResult: status: EnsureStatus message: str - url: Optional[str] = None + url: str | None = None class RemoteProvisioningError(RuntimeError): diff --git a/src/pkgmgr/core/remote_provisioning/visibility.py b/src/pkgmgr/core/remote_provisioning/visibility.py index 3dc60b1..37c7b21 100644 --- a/src/pkgmgr/core/remote_provisioning/visibility.py +++ b/src/pkgmgr/core/remote_provisioning/visibility.py @@ -2,7 +2,6 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Optional from pkgmgr.core.credentials.resolver import ResolutionOptions, TokenResolver @@ -47,10 +46,10 @@ def set_repo_visibility( spec: RepoSpec, *, private: bool, - provider_hint: Optional[ProviderHint] = None, - options: Optional[VisibilityOptions] = None, - registry: Optional[ProviderRegistry] = None, - token_resolver: Optional[TokenResolver] = None, + provider_hint: ProviderHint | None = None, + options: VisibilityOptions | None = None, + registry: ProviderRegistry | None = None, + token_resolver: TokenResolver | None = None, ) -> EnsureResult: """ Set repository visibility (public/private) WITHOUT creating repositories. diff --git a/src/pkgmgr/core/repository/dir.py b/src/pkgmgr/core/repository/dir.py index 3fc84fe..35aa1bf 100644 --- a/src/pkgmgr/core/repository/dir.py +++ b/src/pkgmgr/core/repository/dir.py @@ -1,9 +1,9 @@ import os import sys -from typing import Any, Dict +from typing import Any -def get_repo_dir(repositories_base_dir: str, repo: Dict[str, Any]) -> str: +def get_repo_dir(repositories_base_dir: str, repo: dict[str, Any]) -> str: """ Build the local repository directory path from: repositories_base_dir/provider/account/repository diff --git a/src/pkgmgr/core/repository/paths.py b/src/pkgmgr/core/repository/paths.py index b7f1a79..64967fe 100644 --- a/src/pkgmgr/core/repository/paths.py +++ b/src/pkgmgr/core/repository/paths.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - """ Central repository path resolver. @@ -20,7 +18,6 @@ from __future__ import annotations import os from collections.abc import Iterable from dataclasses import dataclass -from typing import Optional @dataclass(frozen=True) @@ -31,23 +28,23 @@ class RepoPaths: flake_nix: str # Human changelog (typically Markdown) - changelog_md: Optional[str] + changelog_md: str | None # Packaging-related files - arch_pkgbuild: Optional[str] - debian_changelog: Optional[str] - debian_control: Optional[str] - rpm_spec: Optional[str] + arch_pkgbuild: str | None + debian_changelog: str | None + debian_control: str | None + rpm_spec: str | None -def _first_existing(candidates: Iterable[str]) -> Optional[str]: +def _first_existing(candidates: Iterable[str]) -> str | None: for p in candidates: if p and os.path.isfile(p): return p return None -def _find_first_spec_in_dir(dir_path: str) -> Optional[str]: +def _find_first_spec_in_dir(dir_path: str) -> str | None: if not os.path.isdir(dir_path): return None try: diff --git a/src/pkgmgr/core/repository/resolve.py b/src/pkgmgr/core/repository/resolve.py index 3689e28..ad2a9e9 100644 --- a/src/pkgmgr/core/repository/resolve.py +++ b/src/pkgmgr/core/repository/resolve.py @@ -13,12 +13,12 @@ def resolve_repos(identifiers: [], all_repos: []): full_id = ( f"{repo.get('provider')}/{repo.get('account')}/{repo.get('repository')}" ) - if ident == full_id or ident == repo.get("alias"): - matches.append(repo) - elif ident == repo.get("repository"): + if ident == full_id or ident == repo.get("alias") or ( + ident == repo.get("repository") # Only match if repository name is unique among all_repos. - if sum(1 for r in all_repos if r.get("repository") == ident) == 1: - matches.append(repo) + and sum(1 for r in all_repos if r.get("repository") == ident) == 1 + ): + matches.append(repo) if not matches: print(f"Identifier '{ident}' did not match any repository in config.") else: diff --git a/src/pkgmgr/core/repository/selected.py b/src/pkgmgr/core/repository/selected.py index 71c317d..704d294 100644 --- a/src/pkgmgr/core/repository/selected.py +++ b/src/pkgmgr/core/repository/selected.py @@ -1,16 +1,14 @@ -#!/usr/bin/env python3 - from __future__ import annotations import os import re from collections.abc import Sequence -from typing import Any, Dict, List +from typing import Any from pkgmgr.core.repository.ignored import filter_ignored from pkgmgr.core.repository.resolve import resolve_repos -Repository = Dict[str, Any] +Repository = dict[str, Any] def _compile_maybe_regex(pattern: str): @@ -42,10 +40,7 @@ def _match_any(values: Sequence[str], pattern: str) -> bool: """ Return True if any of the values matches the pattern. """ - for v in values: - if _match_pattern(v, pattern): - return True - return False + return any(_match_pattern(v, pattern) for v in values) def _build_identifier_string(repo: Repository) -> str: @@ -72,15 +67,15 @@ def _build_identifier_string(repo: Repository) -> str: def _apply_filters( - repos: List[Repository], + repos: list[Repository], string_pattern: str, - category_patterns: List[str], - tag_patterns: List[str], -) -> List[Repository]: + category_patterns: list[str], + tag_patterns: list[str], +) -> list[Repository]: if not string_pattern and not category_patterns and not tag_patterns: return repos - filtered: List[Repository] = [] + filtered: list[Repository] = [] for repo in repos: # String filter @@ -91,7 +86,7 @@ def _apply_filters( # Category filter: only real categories, NOT tags if category_patterns: - cats: List[str] = [] + cats: list[str] = [] cats.extend(map(str, repo.get("category_files", []))) if "category" in repo: cats.append(str(repo["category"])) @@ -109,7 +104,7 @@ def _apply_filters( # Tag filter: YAML tags only if tag_patterns: - tags: List[str] = list(map(str, repo.get("tags", []))) + tags: list[str] = list(map(str, repo.get("tags", []))) if not tags: continue @@ -126,7 +121,7 @@ def _apply_filters( return filtered -def _maybe_filter_ignored(args, repos: List[Repository]) -> List[Repository]: +def _maybe_filter_ignored(args, repos: list[Repository]) -> list[Repository]: """ Apply ignore filtering unless the caller explicitly opted to include ignored repositories (via args.include_ignored). @@ -141,7 +136,7 @@ def _maybe_filter_ignored(args, repos: List[Repository]) -> List[Repository]: return filter_ignored(repos) -def get_selected_repos(args, all_repositories: List[Repository]) -> List[Repository]: +def get_selected_repos(args, all_repositories: list[Repository]) -> list[Repository]: """ Compute the list of repositories selected by CLI arguments. @@ -158,11 +153,11 @@ def get_selected_repos(args, all_repositories: List[Repository]) -> List[Reposit The ignore filter can be bypassed by setting args.include_ignored = True (e.g. via a CLI flag --include-ignored). """ - identifiers: List[str] = getattr(args, "identifiers", []) or [] + identifiers: list[str] = getattr(args, "identifiers", []) or [] use_all: bool = bool(getattr(args, "all", False)) - category_patterns: List[str] = getattr(args, "category", []) or [] + category_patterns: list[str] = getattr(args, "category", []) or [] string_pattern: str = getattr(args, "string", "") or "" - tag_patterns: List[str] = getattr(args, "tag", []) or [] + tag_patterns: list[str] = getattr(args, "tag", []) or [] has_filters = bool(category_patterns or string_pattern or tag_patterns) diff --git a/src/pkgmgr/core/version/installed.py b/src/pkgmgr/core/version/installed.py index 3bd3287..6ef9b60 100644 --- a/src/pkgmgr/core/version/installed.py +++ b/src/pkgmgr/core/version/installed.py @@ -6,7 +6,6 @@ import shutil import subprocess from collections.abc import Iterable from dataclasses import dataclass -from typing import Optional, Tuple @dataclass(frozen=True) @@ -37,7 +36,7 @@ def _unique_candidates(names: Iterable[str]) -> list[str]: return out -def get_installed_python_version(*candidates: str) -> Optional[InstalledVersion]: +def get_installed_python_version(*candidates: str) -> InstalledVersion | None: """ Detect installed Python package version in the CURRENT Python environment. @@ -86,18 +85,17 @@ def get_installed_python_version(*candidates: str) -> Optional[InstalledVersion] return None -def _run_nix(args: list[str]) -> Tuple[int, str, str]: +def _run_nix(args: list[str]) -> tuple[int, str, str]: p = subprocess.run( args, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + capture_output=True, text=True, check=False, ) return p.returncode, p.stdout or "", p.stderr or "" -def _extract_version_from_store_path(path: str) -> Optional[str]: +def _extract_version_from_store_path(path: str) -> str | None: if not path: return None base = path.rstrip("/").split("/")[-1] @@ -109,7 +107,7 @@ def _extract_version_from_store_path(path: str) -> Optional[str]: return None -def get_installed_nix_profile_version(*candidates: str) -> Optional[InstalledVersion]: +def get_installed_nix_profile_version(*candidates: str) -> InstalledVersion | None: """ Detect installed version from the current Nix profile. diff --git a/src/pkgmgr/core/version/semver.py b/src/pkgmgr/core/version/semver.py index 77e4a39..4845ed7 100644 --- a/src/pkgmgr/core/version/semver.py +++ b/src/pkgmgr/core/version/semver.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - """ Utilities for working with semantic versions (SemVer). @@ -12,7 +10,6 @@ from __future__ import annotations from collections.abc import Iterable from dataclasses import dataclass -from typing import List, Optional, Tuple @dataclass(frozen=True, order=True) @@ -79,16 +76,16 @@ def is_semver_tag(tag: str) -> bool: def extract_semver_from_tags( tags: Iterable[str], - major: Optional[int] = None, - minor: Optional[int] = None, -) -> List[Tuple[str, SemVer]]: + major: int | None = None, + minor: int | None = None, +) -> list[tuple[str, SemVer]]: """ Filter and parse tags that match SemVer, optionally restricted to a specific MAJOR or MAJOR.MINOR line. Returns a list of (tag_string, SemVer) pairs. """ - result: List[Tuple[str, SemVer]] = [] + result: list[tuple[str, SemVer]] = [] for tag in tags: try: ver = SemVer.parse(tag) @@ -108,9 +105,9 @@ def extract_semver_from_tags( def find_latest_version( tags: Iterable[str], - major: Optional[int] = None, - minor: Optional[int] = None, -) -> Optional[Tuple[str, SemVer]]: + major: int | None = None, + minor: int | None = None, +) -> tuple[str, SemVer] | None: """ Find the latest SemVer tag from the given tags. diff --git a/src/pkgmgr/core/version/source.py b/src/pkgmgr/core/version/source.py index 2ded27b..e250905 100644 --- a/src/pkgmgr/core/version/source.py +++ b/src/pkgmgr/core/version/source.py @@ -3,14 +3,13 @@ from __future__ import annotations import os import re -from typing import Optional import yaml from pkgmgr.core.repository.paths import resolve_repo_paths -def read_pyproject_version(repo_dir: str) -> Optional[str]: +def read_pyproject_version(repo_dir: str) -> str | None: """ Read the version from pyproject.toml in repo_dir, if present. @@ -88,7 +87,7 @@ def read_flake_version(repo_dir: str) -> Optional[str]: return match.group(1).strip() or None -def read_pkgbuild_version(repo_dir: str) -> Optional[str]: +def read_pkgbuild_version(repo_dir: str) -> str | None: """ Read the version from PKGBUILD (preferring packaging/arch/PKGBUILD). @@ -122,7 +121,7 @@ def read_pkgbuild_version(repo_dir: str) -> Optional[str]: return pkgver or None -def read_debian_changelog_version(repo_dir: str) -> Optional[str]: +def read_debian_changelog_version(repo_dir: str) -> str | None: """ Read the latest version from debian changelog. @@ -140,7 +139,7 @@ def read_debian_changelog_version(repo_dir: str) -> Optional[str]: return None try: - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: for line in f: line = line.strip() if not line: @@ -195,7 +194,7 @@ def read_spec_version(repo_dir: str) -> Optional[str]: return version or None -def read_ansible_galaxy_version(repo_dir: str) -> Optional[str]: +def read_ansible_galaxy_version(repo_dir: str) -> str | None: """ Read the version from Ansible Galaxy metadata. diff --git a/src/pkgmgr/github/check_hadolint_sarif.py b/src/pkgmgr/github/check_hadolint_sarif.py index 45053ce..f8f5830 100644 --- a/src/pkgmgr/github/check_hadolint_sarif.py +++ b/src/pkgmgr/github/check_hadolint_sarif.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 """Fail when a hadolint SARIF report contains warnings or errors.""" from __future__ import annotations diff --git a/tests/e2e/_util.py b/tests/e2e/_util.py index fbf313c..42a3083 100644 --- a/tests/e2e/_util.py +++ b/tests/e2e/_util.py @@ -10,6 +10,7 @@ def run(cmd, *, cwd=None, env=None, shell=False) -> str: text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + check=False, ) print("----- BEGIN COMMAND -----") diff --git a/tests/e2e/test_config_commands.py b/tests/e2e/test_config_commands.py index ea1ab5e..1fe2a6e 100644 --- a/tests/e2e/test_config_commands.py +++ b/tests/e2e/test_config_commands.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - """ Integration tests for the `pkgmgr config` command. diff --git a/tests/e2e/test_make_commands.py b/tests/e2e/test_make_commands.py index 4a4cf78..42da6b5 100644 --- a/tests/e2e/test_make_commands.py +++ b/tests/e2e/test_make_commands.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - """ Integration tests for the `pkgmgr make` command. diff --git a/tests/e2e/test_nix_build_pkgmgr.py b/tests/e2e/test_nix_build_pkgmgr.py index 92f3b29..7dfa33e 100644 --- a/tests/e2e/test_nix_build_pkgmgr.py +++ b/tests/e2e/test_nix_build_pkgmgr.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - """ E2E test to inspect the Nix environment and build the pkgmgr flake in *every* distro container. @@ -36,6 +34,7 @@ def _run_cmd(cmd: list[str]) -> subprocess.CompletedProcess: cmd, text=True, capture_output=True, + check=False, ) print("[STDOUT]\n", proc.stdout) print("[STDERR]\n", proc.stderr) diff --git a/tests/e2e/test_path_commands.py b/tests/e2e/test_path_commands.py index a8f5eed..e61f942 100644 --- a/tests/e2e/test_path_commands.py +++ b/tests/e2e/test_path_commands.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - """ End-to-end tests for the `pkgmgr path` command. diff --git a/tests/e2e/test_release_commands.py b/tests/e2e/test_release_commands.py index 5869313..a8f4fa7 100644 --- a/tests/e2e/test_release_commands.py +++ b/tests/e2e/test_release_commands.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - """ End-to-end style integration tests for the `pkgmgr release` CLI command. @@ -149,9 +147,12 @@ class TestIntegrationReleaseCommand(unittest.TestCase): try: sys.argv = ["pkgmgr", "release", "--help"] # argparse will call sys.exit(), so we expect a SystemExit here. - with contextlib.redirect_stdout(buf), contextlib.redirect_stderr(buf): - with self.assertRaises(SystemExit) as cm: - runpy.run_module("pkgmgr", run_name="__main__") + with ( + contextlib.redirect_stdout(buf), + contextlib.redirect_stderr(buf), + self.assertRaises(SystemExit) as cm, + ): + runpy.run_module("pkgmgr", run_name="__main__") finally: sys.argv = original_argv diff --git a/tests/e2e/test_tools_commands.py b/tests/e2e/test_tools_commands.py index 0886e0a..471b47c 100644 --- a/tests/e2e/test_tools_commands.py +++ b/tests/e2e/test_tools_commands.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - """ Integration tests for the "tools" commands: diff --git a/tests/e2e/test_tools_help.py b/tests/e2e/test_tools_help.py index 68cb440..02a3487 100644 --- a/tests/e2e/test_tools_help.py +++ b/tests/e2e/test_tools_help.py @@ -15,10 +15,9 @@ from __future__ import annotations import runpy import sys import unittest -from typing import List -def _run_main(argv: List[str]) -> None: +def _run_main(argv: list[str]) -> None: """ Helper to run main.py with the given argv. diff --git a/tests/e2e/test_version_commands.py b/tests/e2e/test_version_commands.py index 31fe14b..7e526d0 100644 --- a/tests/e2e/test_version_commands.py +++ b/tests/e2e/test_version_commands.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - """ End-to-end tests for the `pkgmgr version` command. @@ -25,7 +23,6 @@ import os import runpy import sys import unittest -from typing import List from pkgmgr.core.config.load import load_config @@ -54,7 +51,7 @@ def _load_pkgmgr_repo_dir() -> str: directories = cfg.get("directories", {}) base_repos_dir = os.path.expanduser(directories.get("repositories", "")) - candidates: List[dict] = cfg.get("repositories", []) or [] + candidates: list[dict] = cfg.get("repositories", []) or [] for repo in candidates: repo_name = (repo.get("repository") or "").strip() alias = (repo.get("alias") or "").strip() diff --git a/tests/integration/test_branch_cli.py b/tests/integration/test_branch_cli.py index 95bd2fc..63d93d0 100644 --- a/tests/integration/test_branch_cli.py +++ b/tests/integration/test_branch_cli.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - """ Integration tests for the `pkgmgr branch` CLI wiring. diff --git a/tests/integration/test_config_defaults_integration.py b/tests/integration/test_config_defaults_integration.py index 33a61e0..581fe4b 100644 --- a/tests/integration/test_config_defaults_integration.py +++ b/tests/integration/test_config_defaults_integration.py @@ -67,51 +67,53 @@ class ConfigDefaultsIntegrationTest(unittest.TestCase): # Provide fake pkgmgr module so your functions resolve pkg_root correctly fake_pkgmgr = types.SimpleNamespace(__file__=str(pkg_root / "__init__.py")) - with patch.dict(sys.modules, {"pkgmgr": fake_pkgmgr}): - with patch.dict(os.environ, {"HOME": str(home)}): - # A) load_config should fall back to /config/defaults.yaml - merged = load_config(user_config_path) + with ( + patch.dict(sys.modules, {"pkgmgr": fake_pkgmgr}), + patch.dict(os.environ, {"HOME": str(home)}), + ): + # A) load_config should fall back to /config/defaults.yaml + merged = load_config(user_config_path) - self.assertEqual( - merged["directories"]["repositories"], "/opt/Repositories" - ) - self.assertEqual( - merged["directories"]["binaries"], "/usr/local/bin" - ) + self.assertEqual( + merged["directories"]["repositories"], "/opt/Repositories" + ) + self.assertEqual( + merged["directories"]["binaries"], "/usr/local/bin" + ) - # user-only key must still exist (user config merges over defaults) - self.assertEqual(merged["directories"]["user_only"], "/home/user") + # user-only key must still exist (user config merges over defaults) + self.assertEqual(merged["directories"]["user_only"], "/home/user") - self.assertIn("repositories", merged) - self.assertTrue( - any( - r.get("provider") == "github" - and r.get("account") == "acme" - and r.get("repository") == "demo" - for r in merged["repositories"] - ) + self.assertIn("repositories", merged) + self.assertTrue( + any( + r.get("provider") == "github" + and r.get("account") == "acme" + and r.get("repository") == "demo" + for r in merged["repositories"] ) + ) - # B) update_default_configs should copy defaults.yaml to ~/.config/pkgmgr/ - before_config_yaml = (user_cfg_dir / "config.yaml").read_text( - encoding="utf-8" - ) + # B) update_default_configs should copy defaults.yaml to ~/.config/pkgmgr/ + before_config_yaml = (user_cfg_dir / "config.yaml").read_text( + encoding="utf-8" + ) - config_cmd._update_default_configs(user_config_path) + config_cmd._update_default_configs(user_config_path) - self.assertTrue((user_cfg_dir / "defaults.yaml").is_file()) - copied_defaults = yaml.safe_load( - (user_cfg_dir / "defaults.yaml").read_text(encoding="utf-8") - ) - self.assertEqual( - copied_defaults["directories"]["repositories"], - "/opt/Repositories", - ) + self.assertTrue((user_cfg_dir / "defaults.yaml").is_file()) + copied_defaults = yaml.safe_load( + (user_cfg_dir / "defaults.yaml").read_text(encoding="utf-8") + ) + self.assertEqual( + copied_defaults["directories"]["repositories"], + "/opt/Repositories", + ) - after_config_yaml = (user_cfg_dir / "config.yaml").read_text( - encoding="utf-8" - ) - self.assertEqual(after_config_yaml, before_config_yaml) + after_config_yaml = (user_cfg_dir / "config.yaml").read_text( + encoding="utf-8" + ) + self.assertEqual(after_config_yaml, before_config_yaml) if __name__ == "__main__": diff --git a/tests/integration/test_install_repos.py b/tests/integration/test_install_repos.py index 1fe61c1..ecac640 100644 --- a/tests/integration/test_install_repos.py +++ b/tests/integration/test_install_repos.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - import os import tempfile import unittest @@ -134,9 +132,7 @@ class TestInstallReposIntegration(unittest.TestCase): Make _ensure_repo_dir() believe that the repo directories already exist so that it does not attempt cloning. """ - if path in (repo_system_dir, repo_nix_dir): - return True - return False + return path in (repo_system_dir, repo_nix_dir) mock_resolve.side_effect = fake_resolve mock_exists_install.side_effect = fake_exists_install diff --git a/tests/integration/test_mirror_commands.py b/tests/integration/test_mirror_commands.py index ff26d5f..69efd61 100644 --- a/tests/integration/test_mirror_commands.py +++ b/tests/integration/test_mirror_commands.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - """ CLI integration tests for `pkgmgr mirror`. @@ -21,7 +19,6 @@ import runpy import sys import unittest from contextlib import ExitStack, redirect_stderr, redirect_stdout -from typing import Dict, List, Optional from unittest.mock import MagicMock, PropertyMock, patch @@ -29,7 +26,7 @@ class TestIntegrationMirrorCommands(unittest.TestCase): """Integration tests for `pkgmgr mirror` commands.""" def _run_pkgmgr( - self, args: List[str], extra_env: Optional[Dict[str, str]] = None + self, args: list[str], extra_env: dict[str, str] | None = None ) -> str: """Execute pkgmgr with the given arguments and return captured output.""" original_argv = list(sys.argv) @@ -151,8 +148,7 @@ class TestIntegrationMirrorCommands(unittest.TestCase): code = exc.code if isinstance(exc.code, int) else None if code not in (0, None): raise AssertionError( - "%r failed with exit code %r.\n\nOutput:\n%s" - % (cmd_repr, exc.code, buffer.getvalue()) + f"{cmd_repr!r} failed with exit code {exc.code!r}.\n\nOutput:\n{buffer.getvalue()}" ) return buffer.getvalue() diff --git a/tests/integration/test_mirror_probe_detail_and_provision.py b/tests/integration/test_mirror_probe_detail_and_provision.py index 75dc39f..137a68d 100644 --- a/tests/integration/test_mirror_probe_detail_and_provision.py +++ b/tests/integration/test_mirror_probe_detail_and_provision.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - """ Integration test for mirror probing + provisioning after refactor. diff --git a/tests/integration/test_recursive_capabilities.py b/tests/integration/test_recursive_capabilities.py index 8927c57..45f8bbb 100644 --- a/tests/integration/test_recursive_capabilities.py +++ b/tests/integration/test_recursive_capabilities.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - """ Integration tests for recursive capability resolution and installer shadowing. @@ -17,7 +15,6 @@ import shutil import tempfile import unittest from collections.abc import Sequence -from typing import List, Tuple from unittest.mock import patch import pkgmgr.actions.install as install_mod @@ -29,7 +26,7 @@ from pkgmgr.actions.install.installers.os_packages.arch_pkgbuild import ( ) from pkgmgr.actions.install.installers.python import PythonInstaller -InstallerSpec = Tuple[str, object] +InstallerSpec = tuple[str, object] class TestRecursiveCapabilitiesIntegration(unittest.TestCase): @@ -54,7 +51,7 @@ class TestRecursiveCapabilitiesIntegration(unittest.TestCase): repo_dir: str, installers: Sequence[InstallerSpec], selected_repos=None, - ) -> List[str]: + ) -> list[str]: """ Run install_repos() with a custom INSTALLERS list and capture which installer labels actually run. @@ -69,7 +66,7 @@ class TestRecursiveCapabilitiesIntegration(unittest.TestCase): else: all_repos = selected_repos - called_installers: List[str] = [] + called_installers: list[str] = [] patched_installers = [] for label, inst in installers: diff --git a/tests/integration/test_token_resolver_flow.py b/tests/integration/test_token_resolver_flow.py index 0e5afe9..82b6f29 100644 --- a/tests/integration/test_token_resolver_flow.py +++ b/tests/integration/test_token_resolver_flow.py @@ -21,62 +21,46 @@ class TestTokenResolverIntegration(unittest.TestCase): resolver = TokenResolver() - # ------------------------------------------------------------------ - # 1) ENV: empty - # ------------------------------------------------------------------ - with patch.dict("os.environ", {}, clear=True): - # ------------------------------------------------------------------ - # 2) GH CLI is available - # ------------------------------------------------------------------ - with patch( + def validate_side_effect( + provider_kind: str, + host: str, + token: str, + ) -> bool: + return False # gh + keyring invalid + + with ( + patch.dict("os.environ", {}, clear=True), + patch( "pkgmgr.core.credentials.providers.gh.shutil.which", return_value="/usr/bin/gh", - ): - with patch( - "pkgmgr.core.credentials.providers.gh.subprocess.check_output", - return_value="gh-invalid-token\n", - ): - # ------------------------------------------------------------------ - # 3) Keyring returns an existing (invalid) token - # ------------------------------------------------------------------ - with patch( - "pkgmgr.core.credentials.providers.keyring._import_keyring" - ) as mock_import_keyring: - mock_keyring = mock_import_keyring.return_value - mock_keyring.get_password.return_value = "keyring-invalid-token" + ), + patch( + "pkgmgr.core.credentials.providers.gh.subprocess.check_output", + return_value="gh-invalid-token\n", + ), + patch( + "pkgmgr.core.credentials.providers.keyring._import_keyring" + ) as mock_import_keyring, + patch( + "pkgmgr.core.credentials.providers.prompt.sys.stdin.isatty", + return_value=True, + ), + patch( + "pkgmgr.core.credentials.providers.prompt.getpass", + return_value="new-valid-token", + ), + patch( + "pkgmgr.core.credentials.resolver.validate_token", + side_effect=validate_side_effect, + ) as validate_mock, + ): + mock_keyring = mock_import_keyring.return_value + mock_keyring.get_password.return_value = "keyring-invalid-token" - # ------------------------------------------------------------------ - # 4) Prompt is allowed and returns a NEW token - # ------------------------------------------------------------------ - with patch( - "pkgmgr.core.credentials.providers.prompt.sys.stdin.isatty", - return_value=True, - ): - with patch( - "pkgmgr.core.credentials.providers.prompt.getpass", - return_value="new-valid-token", - ): - # ------------------------------------------------------------------ - # 5) Validation logic: - # - gh token invalid - # - keyring token invalid - # - prompt token is NOT validated (by design) - # ------------------------------------------------------------------ - def validate_side_effect( - provider_kind: str, - host: str, - token: str, - ) -> bool: - return False # gh + keyring invalid - - with patch( - "pkgmgr.core.credentials.resolver.validate_token", - side_effect=validate_side_effect, - ) as validate_mock: - result = resolver.get_token( - provider_kind="github", - host="github.com", - ) + result = resolver.get_token( + provider_kind="github", + host="github.com", + ) # ---------------------------------------------------------------------- # Assertions @@ -93,7 +77,7 @@ class TestTokenResolverIntegration(unittest.TestCase): # Keyring must be overwritten with the new token mock_keyring.set_password.assert_called_once() - service, username, stored_token = mock_keyring.set_password.call_args.args + _service, _username, stored_token = mock_keyring.set_password.call_args.args self.assertEqual(stored_token, "new-valid-token") diff --git a/tests/integration/test_update_silent_continues.py b/tests/integration/test_update_silent_continues.py index 8d8af1b..f42e84b 100644 --- a/tests/integration/test_update_silent_continues.py +++ b/tests/integration/test_update_silent_continues.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - from __future__ import annotations import unittest diff --git a/tests/integration/test_visibility_integration.py b/tests/integration/test_visibility_integration.py index 4156069..5cc38c8 100644 --- a/tests/integration/test_visibility_integration.py +++ b/tests/integration/test_visibility_integration.py @@ -7,14 +7,14 @@ import tempfile import types import unittest from contextlib import redirect_stdout -from typing import Any, Dict, List, Optional, Tuple +from typing import Any from unittest.mock import patch from pkgmgr.actions.mirror.setup_cmd import setup_mirrors from pkgmgr.actions.mirror.visibility_cmd import set_mirror_visibility from pkgmgr.core.remote_provisioning.types import RepoSpec -Repository = Dict[str, Any] +Repository = dict[str, Any] class _FakeRegistry: @@ -45,13 +45,13 @@ class FakeProvider: def __init__(self) -> None: # maps (host, owner, name) -> private(bool) - self.privacy: Dict[Tuple[str, str, str], bool] = {} - self.calls: List[Tuple[str, Any]] = [] + self.privacy: dict[tuple[str, str, str], bool] = {} + self.calls: list[tuple[str, Any]] = [] def can_handle(self, host: str) -> bool: return True - def _candidate_hosts(self, host: str) -> List[str]: + def _candidate_hosts(self, host: str) -> list[str]: """ Be tolerant against host normalization differences: - may contain scheme (https://...) @@ -75,7 +75,7 @@ class FakeProvider: candidates.append(c.split(":", 1)[0]) # de-dup - out: List[str] = [] + out: list[str] = [] for c in candidates: if c not in out: out.append(c) @@ -94,7 +94,7 @@ class FakeProvider: self.privacy[(spec.host, spec.owner, spec.name)] = bool(spec.private) return types.SimpleNamespace(status="created", message="created", url=None) - def get_repo_private(self, token: str, spec: RepoSpec) -> Optional[bool]: + def get_repo_private(self, token: str, spec: RepoSpec) -> bool | None: self.calls.append(("get_repo_private", (token, spec))) for h in self._candidate_hosts(spec.host): key = (h, spec.owner, spec.name) @@ -113,7 +113,7 @@ class FakeProvider: self.privacy[(spec.host, spec.owner, spec.name)] = bool(private) -def _mk_ctx(*, identifier: str, repo_dir: str, mirrors: Dict[str, str]) -> Any: +def _mk_ctx(*, identifier: str, repo_dir: str, mirrors: dict[str, str]) -> Any: return types.SimpleNamespace( identifier=identifier, repo_dir=repo_dir, diff --git a/tests/unit/pkgmgr/actions/branch/test_open_branch.py b/tests/unit/pkgmgr/actions/branch/test_open_branch.py index 502692b..0131e63 100644 --- a/tests/unit/pkgmgr/actions/branch/test_open_branch.py +++ b/tests/unit/pkgmgr/actions/branch/test_open_branch.py @@ -54,9 +54,11 @@ class TestOpenBranch(unittest.TestCase): push_upstream.assert_called_once_with("origin", "auto-branch", cwd=".") def test_open_branch_rejects_empty_name(self) -> None: - with patch("builtins.input", return_value=""): - with self.assertRaises(RuntimeError): - open_branch(None) + with ( + patch("builtins.input", return_value=""), + self.assertRaises(RuntimeError), + ): + open_branch(None) if __name__ == "__main__": diff --git a/tests/unit/pkgmgr/actions/code_scanning/test_code_scanning.py b/tests/unit/pkgmgr/actions/code_scanning/test_code_scanning.py index 17a3d76..52781ce 100644 --- a/tests/unit/pkgmgr/actions/code_scanning/test_code_scanning.py +++ b/tests/unit/pkgmgr/actions/code_scanning/test_code_scanning.py @@ -87,9 +87,11 @@ class TestDownloadCodeScanning(unittest.TestCase): ) def test_errors_when_gh_missing(self) -> None: - with mock.patch("pkgmgr.actions.code_scanning.shutil.which", return_value=None): - with self.assertRaises(CodeScanningError): - download_code_scanning(output_dir="/tmp/should-not-be-created") + with ( + mock.patch("pkgmgr.actions.code_scanning.shutil.which", return_value=None), + self.assertRaises(CodeScanningError), + ): + download_code_scanning(output_dir="/tmp/should-not-be-created") if __name__ == "__main__": # pragma: no cover diff --git a/tests/unit/pkgmgr/actions/install/installers/nix/_fakes.py b/tests/unit/pkgmgr/actions/install/installers/nix/_fakes.py index 025ca01..9684f8f 100644 --- a/tests/unit/pkgmgr/actions/install/installers/nix/_fakes.py +++ b/tests/unit/pkgmgr/actions/install/installers/nix/_fakes.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any, Optional +from typing import Any @dataclass @@ -18,7 +18,7 @@ class FakeRunner: - Generic runner.run(ctx, cmd, allow_failure=...) """ - def __init__(self, mapping: Optional[dict[str, Any]] = None, default: Any = None): + def __init__(self, mapping: dict[str, Any] | None = None, default: Any = None): self.mapping = mapping or {} self.default = default if default is not None else FakeRunResult(0, "", "") self.calls: list[tuple[Any, str, bool]] = [] diff --git a/tests/unit/pkgmgr/actions/install/installers/nix/test_legacy.py b/tests/unit/pkgmgr/actions/install/installers/nix/test_legacy.py index 9595cda..d9048bb 100644 --- a/tests/unit/pkgmgr/actions/install/installers/nix/test_legacy.py +++ b/tests/unit/pkgmgr/actions/install/installers/nix/test_legacy.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - """ Unit tests for NixFlakeInstaller using unittest (no pytest). @@ -19,7 +17,6 @@ import subprocess import tempfile import unittest from contextlib import redirect_stdout -from typing import List from unittest.mock import patch from pkgmgr.actions.install.installers.nix import NixFlakeInstaller @@ -73,8 +70,8 @@ class TestNixFlakeInstaller(unittest.TestCase): which_patch.return_value = "/usr/bin/nix" @staticmethod - def _install_cmds_from_calls(call_args_list) -> List[str]: - cmds: List[str] = [] + def _install_cmds_from_calls(call_args_list) -> list[str]: + cmds: list[str] = [] for c in call_args_list: if not c.args: continue diff --git a/tests/unit/pkgmgr/actions/install/installers/test_makefile_installer.py b/tests/unit/pkgmgr/actions/install/installers/test_makefile_installer.py index a6f96ec..055ed3f 100644 --- a/tests/unit/pkgmgr/actions/install/installers/test_makefile_installer.py +++ b/tests/unit/pkgmgr/actions/install/installers/test_makefile_installer.py @@ -50,7 +50,6 @@ class TestMakefileInstaller(unittest.TestCase): # Ensure we checked the Makefile and then called make install. mock_file.assert_called_once_with( os.path.join(self.ctx.repo_dir, "Makefile"), - "r", encoding="utf-8", errors="ignore", ) @@ -77,7 +76,6 @@ class TestMakefileInstaller(unittest.TestCase): # We should have read the Makefile, but not called run_command(). mock_file.assert_called_once_with( os.path.join(self.ctx.repo_dir, "Makefile"), - "r", encoding="utf-8", errors="ignore", ) diff --git a/tests/unit/pkgmgr/actions/install/test_install_repos.py b/tests/unit/pkgmgr/actions/install/test_install_repos.py index ee785d4..18dfe73 100644 --- a/tests/unit/pkgmgr/actions/install/test_install_repos.py +++ b/tests/unit/pkgmgr/actions/install/test_install_repos.py @@ -1,13 +1,11 @@ -#!/usr/bin/env python3 - import os import unittest -from typing import Any, Dict, List +from typing import Any from unittest.mock import MagicMock, patch from pkgmgr.actions.install import install_repos -Repository = Dict[str, Any] +Repository = dict[str, Any] class TestInstallReposOrchestration(unittest.TestCase): @@ -27,7 +25,7 @@ class TestInstallReposOrchestration(unittest.TestCase): "alias": "repo-two", "verified": {"gpg_keys": ["FAKEKEY"]}, } - self.all_repos: List[Repository] = [self.repo1, self.repo2] + self.all_repos: list[Repository] = [self.repo1, self.repo2] @patch("pkgmgr.actions.install.InstallationPipeline") @patch("pkgmgr.actions.install.clone_repos") diff --git a/tests/unit/pkgmgr/actions/install/test_layers.py b/tests/unit/pkgmgr/actions/install/test_layers.py index b7f58c4..0544b28 100644 --- a/tests/unit/pkgmgr/actions/install/test_layers.py +++ b/tests/unit/pkgmgr/actions/install/test_layers.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - import os import unittest diff --git a/tests/unit/pkgmgr/actions/install/test_pipeline.py b/tests/unit/pkgmgr/actions/install/test_pipeline.py index c3d34c4..717dc94 100644 --- a/tests/unit/pkgmgr/actions/install/test_pipeline.py +++ b/tests/unit/pkgmgr/actions/install/test_pipeline.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +from __future__ import annotations import unittest from unittest.mock import MagicMock, patch diff --git a/tests/unit/pkgmgr/actions/mirror/test_context.py b/tests/unit/pkgmgr/actions/mirror/test_context.py index 9e0c38e..65871c5 100644 --- a/tests/unit/pkgmgr/actions/mirror/test_context.py +++ b/tests/unit/pkgmgr/actions/mirror/test_context.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - from __future__ import annotations import unittest diff --git a/tests/unit/pkgmgr/actions/mirror/test_diff_cmd.py b/tests/unit/pkgmgr/actions/mirror/test_diff_cmd.py index 2fc28f2..d1a6cea 100644 --- a/tests/unit/pkgmgr/actions/mirror/test_diff_cmd.py +++ b/tests/unit/pkgmgr/actions/mirror/test_diff_cmd.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - from __future__ import annotations import io diff --git a/tests/unit/pkgmgr/actions/mirror/test_io.py b/tests/unit/pkgmgr/actions/mirror/test_io.py index 4fba34f..0377f92 100644 --- a/tests/unit/pkgmgr/actions/mirror/test_io.py +++ b/tests/unit/pkgmgr/actions/mirror/test_io.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - from __future__ import annotations import os @@ -61,13 +59,7 @@ class TestMirrorIO(unittest.TestCase): p = os.path.join(tmpdir, "MIRRORS") with open(p, "w", encoding="utf-8") as fh: fh.write( - "\n".join( - [ - "https://github.com/alice/repo1", - "https://github.com/alice/repo2", - "ssh://git@git.veen.world:2201/alice/repo3.git", - ] - ) + "https://github.com/alice/repo1\nhttps://github.com/alice/repo2\nssh://git@git.veen.world:2201/alice/repo3.git" + "\n" ) @@ -99,7 +91,7 @@ class TestMirrorIO(unittest.TestCase): p = os.path.join(tmpdir, "MIRRORS") self.assertTrue(os.path.exists(p)) - with open(p, "r", encoding="utf-8") as fh: + with open(p, encoding="utf-8") as fh: content = fh.read() self.assertEqual( diff --git a/tests/unit/pkgmgr/actions/mirror/test_list_cmd.py b/tests/unit/pkgmgr/actions/mirror/test_list_cmd.py index f0ac9fd..f4ac558 100644 --- a/tests/unit/pkgmgr/actions/mirror/test_list_cmd.py +++ b/tests/unit/pkgmgr/actions/mirror/test_list_cmd.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - from __future__ import annotations import io diff --git a/tests/unit/pkgmgr/actions/mirror/test_remote_provision.py b/tests/unit/pkgmgr/actions/mirror/test_remote_provision.py index 1ce0567..d8d6772 100644 --- a/tests/unit/pkgmgr/actions/mirror/test_remote_provision.py +++ b/tests/unit/pkgmgr/actions/mirror/test_remote_provision.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - from __future__ import annotations import unittest diff --git a/tests/unit/pkgmgr/actions/mirror/test_url_utils.py b/tests/unit/pkgmgr/actions/mirror/test_url_utils.py index 3b1832b..8f5af1e 100644 --- a/tests/unit/pkgmgr/actions/mirror/test_url_utils.py +++ b/tests/unit/pkgmgr/actions/mirror/test_url_utils.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - from __future__ import annotations import unittest diff --git a/tests/unit/pkgmgr/actions/release/test_files.py b/tests/unit/pkgmgr/actions/release/test_files.py index 552a08a..328725b 100644 --- a/tests/unit/pkgmgr/actions/release/test_files.py +++ b/tests/unit/pkgmgr/actions/release/test_files.py @@ -37,7 +37,7 @@ class TestUpdatePyprojectVersion(unittest.TestCase): update_pyproject_version(path, "1.2.3", preview=False) - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: content = f.read() self.assertIn('version = "1.2.3"', content) @@ -62,7 +62,7 @@ class TestUpdatePyprojectVersion(unittest.TestCase): update_pyproject_version(path, "1.2.3", preview=True) - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: content = f.read() self.assertEqual(content, original) @@ -129,7 +129,7 @@ class TestUpdateFlakeVersion(unittest.TestCase): update_flake_version(path, "1.2.3", preview=False) - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: content = f.read() self.assertIn('version = "1.2.3";', content) @@ -144,7 +144,7 @@ class TestUpdateFlakeVersion(unittest.TestCase): update_flake_version(path, "1.2.3", preview=True) - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: content = f.read() self.assertEqual(content, original) @@ -170,7 +170,7 @@ class TestUpdatePkgbuildVersion(unittest.TestCase): update_pkgbuild_version(path, "1.2.3", preview=False) - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: content = f.read() self.assertIn("pkgver=1.2.3", content) @@ -196,7 +196,7 @@ class TestUpdatePkgbuildVersion(unittest.TestCase): update_pkgbuild_version(path, "1.2.3", preview=True) - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: content = f.read() self.assertEqual(content, original) @@ -222,7 +222,7 @@ class TestUpdateSpecVersion(unittest.TestCase): update_spec_version(path, "1.2.3", preview=False) - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: content = f.read() self.assertIn("Version: 1.2.3", content) @@ -249,7 +249,7 @@ class TestUpdateSpecVersion(unittest.TestCase): update_spec_version(path, "1.2.3", preview=True) - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: content = f.read() self.assertEqual(content, original) @@ -264,7 +264,7 @@ class TestUpdateChangelog(unittest.TestCase): update_changelog(path, "1.2.3", message="First release", preview=False) self.assertTrue(os.path.exists(path)) - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: content = f.read() # New file must lead with an H1 so markdownlint MD041 is happy. @@ -280,7 +280,7 @@ class TestUpdateChangelog(unittest.TestCase): update_changelog(path, "1.0.0", message="Second release", preview=False) - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: content = f.read() # H1 still on top, new entry above the existing one. @@ -303,7 +303,7 @@ class TestUpdateChangelog(unittest.TestCase): update_changelog(path, "1.0.0", message=None, preview=False) - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: content = f.read() # An H1 is added so MD041 is satisfied even for legacy files. @@ -315,7 +315,7 @@ class TestUpdateChangelog(unittest.TestCase): path = os.path.join(tmpdir, "CHANGELOG.md") update_changelog(path, "1.2.3", message="* Provided bullet", preview=False) - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: content = f.read() self.assertIn("\n\n* Provided bullet\n", content) @@ -331,7 +331,7 @@ class TestUpdateChangelog(unittest.TestCase): preview=False, ) - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: content = f.read() self.assertIn("**Summary**", content) @@ -348,7 +348,7 @@ class TestUpdateChangelog(unittest.TestCase): update_changelog(path, "1.0.0", message="Preview only", preview=True) - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: content = f.read() self.assertEqual(content, original) @@ -374,7 +374,7 @@ class TestUpdateDebianChangelog(unittest.TestCase): preview=False, ) - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: content = f.read() self.assertIn("package-manager (1.2.3-1) unstable; urgency=medium", content) @@ -402,7 +402,7 @@ class TestUpdateDebianChangelog(unittest.TestCase): preview=True, ) - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: content = f.read() self.assertEqual(content, original) @@ -428,7 +428,7 @@ class TestUpdateDebianChangelog(unittest.TestCase): preview=False, ) - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: content = f.read() self.assertIn(" * First bullet", content) @@ -484,7 +484,7 @@ class TestUpdateSpecChangelog(unittest.TestCase): preview=False, ) - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: content = f.read() self.assertIn("%changelog", content) @@ -526,7 +526,7 @@ class TestUpdateSpecChangelog(unittest.TestCase): preview=True, ) - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: content = f.read() self.assertEqual(content, original) @@ -567,7 +567,7 @@ class TestUpdateSpecChangelog(unittest.TestCase): preview=False, ) - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: content = f.read() self.assertIn("- * First bullet", content) diff --git a/tests/unit/pkgmgr/actions/release/test_package_name.py b/tests/unit/pkgmgr/actions/release/test_package_name.py index be761e0..1d24560 100644 --- a/tests/unit/pkgmgr/actions/release/test_package_name.py +++ b/tests/unit/pkgmgr/actions/release/test_package_name.py @@ -11,7 +11,6 @@ import os import tempfile import unittest from pathlib import Path -from typing import Optional from pkgmgr.actions.release.package_name import resolve_package_name from pkgmgr.core.repository.paths import RepoPaths @@ -20,9 +19,9 @@ from pkgmgr.core.repository.paths import RepoPaths def _paths( repo_dir: str, *, - debian_control: Optional[str] = None, - arch_pkgbuild: Optional[str] = None, - rpm_spec: Optional[str] = None, + debian_control: str | None = None, + arch_pkgbuild: str | None = None, + rpm_spec: str | None = None, ) -> RepoPaths: return RepoPaths( repo_dir=repo_dir, diff --git a/tests/unit/pkgmgr/actions/repository/test_deinstall.py b/tests/unit/pkgmgr/actions/repository/test_deinstall.py index 905ae62..ec05373 100644 --- a/tests/unit/pkgmgr/actions/repository/test_deinstall.py +++ b/tests/unit/pkgmgr/actions/repository/test_deinstall.py @@ -36,9 +36,7 @@ class TestDeinstallRepos(unittest.TestCase): def exists_side_effect(path): if path == "/home/u/.local/bin/demo": return True - if path == "/repos/github.com/alice/demo/Makefile": - return True - return False + return path == "/repos/github.com/alice/demo/Makefile" mock_exists.side_effect = exists_side_effect diff --git a/tests/unit/pkgmgr/cli/commands/test_release.py b/tests/unit/pkgmgr/cli/commands/test_release.py index 522e524..ed0131d 100644 --- a/tests/unit/pkgmgr/cli/commands/test_release.py +++ b/tests/unit/pkgmgr/cli/commands/test_release.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - """ Unit tests for pkgmgr.cli.commands.release. @@ -16,7 +14,6 @@ from __future__ import annotations import argparse import unittest from types import SimpleNamespace -from typing import List from unittest.mock import call, patch @@ -25,7 +22,7 @@ class TestReleaseCommand(unittest.TestCase): Tests for the `pkgmgr release` CLI wiring. """ - def _make_ctx(self, all_repos: List[dict]) -> SimpleNamespace: + def _make_ctx(self, all_repos: list[dict]) -> SimpleNamespace: """ Create a minimal CLIContext-like object for tests. @@ -39,7 +36,7 @@ class TestReleaseCommand(unittest.TestCase): user_config_path="/tmp/config.yaml", ) - def _parse_release_args(self, argv: List[str]) -> argparse.Namespace: + def _parse_release_args(self, argv: list[str]) -> argparse.Namespace: """ Build a real top-level parser and parse the given argv list to obtain the Namespace for the `release` command. diff --git a/tests/unit/pkgmgr/cli/commands/test_repos.py b/tests/unit/pkgmgr/cli/commands/test_repos.py index 2f32a21..85f8de6 100644 --- a/tests/unit/pkgmgr/cli/commands/test_repos.py +++ b/tests/unit/pkgmgr/cli/commands/test_repos.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - """ Unit tests for pkgmgr.cli.commands.repos @@ -26,17 +24,17 @@ import io import unittest from contextlib import redirect_stdout from types import SimpleNamespace -from typing import Any, Dict, List +from typing import Any from unittest.mock import patch from pkgmgr.cli.commands.repos import handle_repos_command from pkgmgr.cli.context import CLIContext -Repository = Dict[str, Any] +Repository = dict[str, Any] class TestReposCommand(unittest.TestCase): - def _make_ctx(self, repositories: List[Repository]) -> CLIContext: + def _make_ctx(self, repositories: list[Repository]) -> CLIContext: """ Helper to build a minimal CLIContext for tests. """ @@ -57,7 +55,7 @@ class TestReposCommand(unittest.TestCase): When repository["directory"] is present, handle_repos_command("path") should print this value directly without calling get_repo_dir(). """ - repos: List[Repository] = [ + repos: list[Repository] = [ { "provider": "github.com", "account": "kevinveenbirkenbach", @@ -93,7 +91,7 @@ class TestReposCommand(unittest.TestCase): should call get_repo_dir(ctx.repositories_base_dir, repo) and print the returned value. """ - repos: List[Repository] = [ + repos: list[Repository] = [ { "provider": "github.com", "account": "kevinveenbirkenbach", @@ -155,7 +153,7 @@ class TestReposCommand(unittest.TestCase): 'shell' should resolve the repository directory and pass it as cwd to run_command(), along with the full shell command string. """ - repos: List[Repository] = [ + repos: list[Repository] = [ { "provider": "github.com", "account": "kevinveenbirkenbach", @@ -196,7 +194,7 @@ class TestReposCommand(unittest.TestCase): """ 'shell' without -c/--command should print an error and exit with code 2. """ - repos: List[Repository] = [] + repos: list[Repository] = [] ctx = self._make_ctx(repos) args = SimpleNamespace( diff --git a/tests/unit/pkgmgr/cli/commands/test_tools.py b/tests/unit/pkgmgr/cli/commands/test_tools.py index 074fc93..061b26f 100644 --- a/tests/unit/pkgmgr/cli/commands/test_tools.py +++ b/tests/unit/pkgmgr/cli/commands/test_tools.py @@ -2,12 +2,12 @@ from __future__ import annotations import unittest from types import SimpleNamespace -from typing import Any, Dict, List +from typing import Any from unittest.mock import call, patch from pkgmgr.cli.commands.tools import handle_tools_command -Repository = Dict[str, Any] +Repository = dict[str, Any] class _Args: @@ -29,7 +29,7 @@ class TestHandleToolsCommand(unittest.TestCase): """ def setUp(self) -> None: - self.repos: List[Repository] = [ + self.repos: list[Repository] = [ {"alias": "repo1", "directory": "/tmp/repo1"}, {"alias": "repo2", "directory": "/tmp/repo2"}, ] diff --git a/tests/unit/pkgmgr/cli/test_cli.py b/tests/unit/pkgmgr/cli/test_cli.py index 5755fa5..1713230 100644 --- a/tests/unit/pkgmgr/cli/test_cli.py +++ b/tests/unit/pkgmgr/cli/test_cli.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - """ Unit tests for the pkgmgr CLI (version command). @@ -23,13 +21,13 @@ import tempfile import textwrap import unittest from contextlib import redirect_stdout -from typing import Any, Dict, List +from typing import Any from unittest import mock from pkgmgr import cli -def _fake_config() -> Dict[str, Any]: +def _fake_config() -> dict[str, Any]: """ Provide a minimal configuration dict sufficient for cli.main() to start without touching real config files. @@ -132,7 +130,7 @@ class TestCliVersion(unittest.TestCase): def _run_cli_version_and_capture( self, - extra_args: List[str] | None = None, + extra_args: list[str] | None = None, ) -> str: """ Run 'pkgmgr version [extra_args]' via cli.main() and return captured stdout. diff --git a/tests/unit/pkgmgr/cli/test_handle_branch.py b/tests/unit/pkgmgr/cli/test_handle_branch.py index 0e40a18..e78de46 100644 --- a/tests/unit/pkgmgr/cli/test_handle_branch.py +++ b/tests/unit/pkgmgr/cli/test_handle_branch.py @@ -45,7 +45,7 @@ class TestCliBranch(unittest.TestCase): handle_branch(args, ctx) mock_open_branch.assert_called_once() - call_args, call_kwargs = mock_open_branch.call_args + _call_args, call_kwargs = mock_open_branch.call_args self.assertEqual(call_kwargs.get("name"), "feature/cli-test") self.assertEqual(call_kwargs.get("base_branch"), "develop") self.assertEqual(call_kwargs.get("cwd"), ".") diff --git a/tests/unit/pkgmgr/cli/tools/test_vscode.py b/tests/unit/pkgmgr/cli/tools/test_vscode.py index 18d5803..338a484 100644 --- a/tests/unit/pkgmgr/cli/tools/test_vscode.py +++ b/tests/unit/pkgmgr/cli/tools/test_vscode.py @@ -5,10 +5,10 @@ import os import tempfile import unittest from types import SimpleNamespace -from typing import Any, Dict, List +from typing import Any from unittest.mock import patch -Repository = Dict[str, Any] +Repository = dict[str, Any] class TestOpenVSCodeWorkspace(unittest.TestCase): @@ -27,13 +27,15 @@ class TestOpenVSCodeWorkspace(unittest.TestCase): from pkgmgr.cli.tools.vscode import open_vscode_workspace ctx = SimpleNamespace(config_merged={}, all_repositories=[]) - selected: List[Repository] = [ + selected: list[Repository] = [ {"provider": "github.com", "account": "x", "repository": "y"} ] - with patch("pkgmgr.cli.tools.vscode.shutil.which", return_value=None): - with self.assertRaises(RuntimeError) as cm: - open_vscode_workspace(ctx, selected) + with ( + patch("pkgmgr.cli.tools.vscode.shutil.which", return_value=None), + self.assertRaises(RuntimeError) as cm, + ): + open_vscode_workspace(ctx, selected) self.assertIn("VS Code CLI ('code') not found", str(cm.exception)) @@ -44,7 +46,7 @@ class TestOpenVSCodeWorkspace(unittest.TestCase): config_merged={"directories": {"workspaces": "~/Workspaces"}}, all_repositories=[], ) - selected: List[Repository] = [ + selected: list[Repository] = [ {"provider": "github.com", "account": "x", "repository": "y"} ] @@ -74,7 +76,7 @@ class TestOpenVSCodeWorkspace(unittest.TestCase): all_repositories=[], repositories_base_dir=os.path.join(tmp, "Repos"), ) - selected: List[Repository] = [ + selected: list[Repository] = [ { "provider": "github.com", "account": "kevin", @@ -101,7 +103,7 @@ class TestOpenVSCodeWorkspace(unittest.TestCase): workspace_file = os.path.join(workspaces_dir, "dotlinker.code-workspace") self.assertTrue(os.path.exists(workspace_file)) - with open(workspace_file, "r", encoding="utf-8") as f: + with open(workspace_file, encoding="utf-8") as f: data = json.load(f) self.assertEqual(data["folders"], [{"path": repo_path}]) @@ -125,7 +127,7 @@ class TestOpenVSCodeWorkspace(unittest.TestCase): config_merged={"directories": {"workspaces": workspaces_dir}}, all_repositories=[], ) - selected: List[Repository] = [ + selected: list[Repository] = [ { "provider": "github.com", "account": "kevin", @@ -149,7 +151,7 @@ class TestOpenVSCodeWorkspace(unittest.TestCase): ): open_vscode_workspace(ctx, selected) - with open(workspace_file, "r", encoding="utf-8") as f: + with open(workspace_file, encoding="utf-8") as f: data = json.load(f) self.assertEqual(data, original) diff --git a/tests/unit/pkgmgr/core/command/test_resolve.py b/tests/unit/pkgmgr/core/command/test_resolve.py index 17828b4..d562b67 100644 --- a/tests/unit/pkgmgr/core/command/test_resolve.py +++ b/tests/unit/pkgmgr/core/command/test_resolve.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +from __future__ import annotations import os import stat diff --git a/tests/unit/pkgmgr/core/command/test_run.py b/tests/unit/pkgmgr/core/command/test_run.py index c2c381c..f030020 100644 --- a/tests/unit/pkgmgr/core/command/test_run.py +++ b/tests/unit/pkgmgr/core/command/test_run.py @@ -33,9 +33,11 @@ class TestRunCommand(unittest.TestCase): "import sys; print('oops', file=sys.stderr); sys.exit(2)", ] - with patch.object(run_mod.sys, "exit", side_effect=SystemExit(2)) as exit_mock: - with self.assertRaises(SystemExit) as ctx: - run_mod.run_command(cmd, allow_failure=False) + with ( + patch.object(run_mod.sys, "exit", side_effect=SystemExit(2)) as exit_mock, + self.assertRaises(SystemExit) as ctx, + ): + run_mod.run_command(cmd, allow_failure=False) self.assertEqual(ctx.exception.code, 2) exit_mock.assert_called_once_with(2) diff --git a/tests/unit/pkgmgr/core/config/test_load.py b/tests/unit/pkgmgr/core/config/test_load.py index 4dfb98b..8bb3c25 100644 --- a/tests/unit/pkgmgr/core/config/test_load.py +++ b/tests/unit/pkgmgr/core/config/test_load.py @@ -256,9 +256,11 @@ class LoadConfigIntegrationUnitTests(unittest.TestCase): ) fake_pkgmgr = types.SimpleNamespace(__file__=str(pkg_root / "__init__.py")) - with patch.dict(sys.modules, {"pkgmgr": fake_pkgmgr}): - with patch.dict(os.environ, {"HOME": str(home)}): - merged = load_config(user_config_path) + with ( + patch.dict(sys.modules, {"pkgmgr": fake_pkgmgr}), + patch.dict(os.environ, {"HOME": str(home)}), + ): + merged = load_config(user_config_path) # directories are merged: defaults then user self.assertEqual(merged["directories"]["repositories"], "/PKG/Repos") diff --git a/tests/unit/pkgmgr/core/git/queries/test_remote_check.py b/tests/unit/pkgmgr/core/git/queries/test_remote_check.py index fe89eb7..01e653c 100644 --- a/tests/unit/pkgmgr/core/git/queries/test_remote_check.py +++ b/tests/unit/pkgmgr/core/git/queries/test_remote_check.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - from __future__ import annotations import unittest diff --git a/tests/unit/pkgmgr/core/git/test_run.py b/tests/unit/pkgmgr/core/git/test_run.py index fce0f42..efd365c 100644 --- a/tests/unit/pkgmgr/core/git/test_run.py +++ b/tests/unit/pkgmgr/core/git/test_run.py @@ -38,9 +38,7 @@ class TestGitRun(unittest.TestCase): self.assertEqual(kwargs["cwd"], "/repo") self.assertTrue(kwargs["check"]) self.assertTrue(kwargs["text"]) - # ensure pipes are used (matches implementation intent) - self.assertIsNotNone(kwargs["stdout"]) - self.assertIsNotNone(kwargs["stderr"]) + self.assertTrue(kwargs["capture_output"]) def test_failure_raises_giterror_with_details(self) -> None: # Build a CalledProcessError with stdout/stderr populated @@ -57,9 +55,11 @@ class TestGitRun(unittest.TestCase): exc.stdout = "OUT!" exc.stderr = "ERR!" - with patch("pkgmgr.core.git.run.subprocess.run", side_effect=exc): - with self.assertRaises(GitRunError) as ctx: - run(["status"], cwd="/bad/repo", preview=False) + with ( + patch("pkgmgr.core.git.run.subprocess.run", side_effect=exc), + self.assertRaises(GitRunError) as ctx, + ): + run(["status"], cwd="/bad/repo", preview=False) msg = str(ctx.exception) self.assertIn("Git command failed in '/bad/repo': git status", msg) diff --git a/tests/unit/pkgmgr/core/remote_provisioning/test_visibility.py b/tests/unit/pkgmgr/core/remote_provisioning/test_visibility.py index c2f2ca4..cf38c31 100644 --- a/tests/unit/pkgmgr/core/remote_provisioning/test_visibility.py +++ b/tests/unit/pkgmgr/core/remote_provisioning/test_visibility.py @@ -132,7 +132,7 @@ class TestSetRepoVisibility(unittest.TestCase): self.assertEqual(res.status, "updated") provider.set_repo_private.assert_called_once() - args, kwargs = provider.set_repo_private.call_args + _args, kwargs = provider.set_repo_private.call_args self.assertEqual(kwargs.get("private"), False) def test_provider_hint_overrides_registry_resolution(self) -> None: diff --git a/tests/unit/pkgmgr/core/repository/test_ignored.py b/tests/unit/pkgmgr/core/repository/test_ignored.py index 8255b13..09d844d 100644 --- a/tests/unit/pkgmgr/core/repository/test_ignored.py +++ b/tests/unit/pkgmgr/core/repository/test_ignored.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - import unittest from pkgmgr.core.repository.ignored import filter_ignored diff --git a/tests/unit/pkgmgr/core/repository/test_selected.py b/tests/unit/pkgmgr/core/repository/test_selected.py index 66a39c6..bc6f9b3 100644 --- a/tests/unit/pkgmgr/core/repository/test_selected.py +++ b/tests/unit/pkgmgr/core/repository/test_selected.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +from __future__ import annotations import os import unittest diff --git a/tests/unit/pkgmgr/core/test_create_ink.py b/tests/unit/pkgmgr/core/test_create_ink.py index 4ff7605..6e2c58b 100644 --- a/tests/unit/pkgmgr/core/test_create_ink.py +++ b/tests/unit/pkgmgr/core/test_create_ink.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - import unittest from unittest.mock import patch diff --git a/tests/unit/pkgmgr/core/test_git_utils.py b/tests/unit/pkgmgr/core/test_git_utils.py index 5884a93..5c08c52 100644 --- a/tests/unit/pkgmgr/core/test_git_utils.py +++ b/tests/unit/pkgmgr/core/test_git_utils.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - import unittest from unittest.mock import patch diff --git a/tests/unit/pkgmgr/core/test_versioning.py b/tests/unit/pkgmgr/core/test_versioning.py index a72a2cd..e1762e6 100644 --- a/tests/unit/pkgmgr/core/test_versioning.py +++ b/tests/unit/pkgmgr/core/test_versioning.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - import unittest from pkgmgr.core.version.semver import (