diff --git a/.github/workflows/run.yml b/.github/workflows/run.yml index 967a19e5..c147c53c 100644 --- a/.github/workflows/run.yml +++ b/.github/workflows/run.yml @@ -61,7 +61,7 @@ jobs: strategy: matrix: platform: [ubuntu-latest, macos-latest, windows-latest] - python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] + python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] runs-on: ${{ matrix.platform }} permissions: contents: read diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 28cb702b..c880398c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,7 +49,7 @@ jobs: - run: coverage run --source=dfetch --append -m behave features # Run features tests - run: coverage xml -o coverage.xml # Create XML report - run: pyroma --directory --min=10 . # Check pyproject - - run: find dfetch -name "*.py" | xargs pyupgrade --py39-plus # Check syntax + - run: find dfetch -name "*.py" | xargs pyupgrade --py310-plus # Check syntax - name: Run codacy-coverage-reporter uses: codacy/codacy-coverage-reporter-action@a38818475bb21847788496e9f0fddaa4e84955ba # master diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 740157c5..e6ccbada 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -124,7 +124,7 @@ repos: entry: pyupgrade language: python files: ^dfetch/ - args: [--py39-plus] + args: [--py310-plus] types: [file, python] - repo: https://github.com/gitleaks/gitleaks diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e5255083..6b49c45a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,7 @@ Release 0.12.0 (unreleased) * Group logging under a project name header (#953) * Introduce new ``update-patch`` command (#614) * Introduce new ``format-patch`` command (#943) +* Drop python 3.9 support (#988) Release 0.11.0 (released 2026-01-03) ==================================== diff --git a/dfetch/__main__.py b/dfetch/__main__.py index 99caa518..8edfd8c4 100644 --- a/dfetch/__main__.py +++ b/dfetch/__main__.py @@ -6,7 +6,6 @@ import argparse import sys from collections.abc import Sequence -from typing import Optional from rich.console import Console @@ -65,7 +64,7 @@ def _help(_: argparse.Namespace) -> None: parser.print_help() -def run(argv: Sequence[str], console: Optional[Console] = None) -> None: +def run(argv: Sequence[str], console: Console | None = None) -> None: """Start dfetch.""" args = create_parser().parse_args(argv) diff --git a/dfetch/commands/update_patch.py b/dfetch/commands/update_patch.py index d66e0353..efbb4710 100644 --- a/dfetch/commands/update_patch.py +++ b/dfetch/commands/update_patch.py @@ -32,7 +32,6 @@ import argparse import pathlib -from typing import Optional import dfetch.commands.command import dfetch.manifest.project @@ -154,7 +153,7 @@ def _update_patch( root: pathlib.Path, project_name: str, patch_text: str, - ) -> Optional[pathlib.Path]: + ) -> pathlib.Path | None: """Update the specified patch file with new patch text.""" patch_path = pathlib.Path(patch_to_update).resolve() diff --git a/dfetch/log.py b/dfetch/log.py index 1b039dee..52476ffa 100644 --- a/dfetch/log.py +++ b/dfetch/log.py @@ -4,7 +4,7 @@ import os import sys from contextlib import nullcontext -from typing import Any, Optional, Union, cast +from typing import Any, cast from rich.console import Console from rich.highlighter import NullHighlighter @@ -23,7 +23,7 @@ def make_console(no_color: bool = False) -> Console: ) -def configure_root_logger(console: Optional[Console] = None) -> None: +def configure_root_logger(console: Console | None = None) -> None: """Configure the root logger with RichHandler using the provided Console.""" console = console or make_console() @@ -94,10 +94,10 @@ def error(self, msg: object, *args: Any, **kwargs: Any) -> None: def status( self, name: str, message: str, spinner: str = "dots", enabled: bool = True - ) -> Union[Status, nullcontext[None]]: + ) -> Status | nullcontext[None]: """Show status message with spinner if enabled.""" rich_console = None - logger: Optional[logging.Logger] = self + logger: logging.Logger | None = self while logger: for handler in getattr(logger, "handlers", []): if isinstance(handler, RichHandler): @@ -143,7 +143,7 @@ def filter(self, record: logging.LogRecord) -> bool: return True -def setup_root(name: str, console: Optional[Console] = None) -> DLogger: +def setup_root(name: str, console: Console | None = None) -> DLogger: """Create and return the root logger.""" logging.setLoggerClass(DLogger) configure_root_logger(console) @@ -173,7 +173,7 @@ def increase_verbosity() -> None: logger_.setLevel(new_level) -def get_logger(name: str, console: Optional[Console] = None) -> DLogger: +def get_logger(name: str, console: Console | None = None) -> DLogger: """Get logger for a module, optionally configuring console colors.""" logging.setLoggerClass(DLogger) logger = logging.getLogger(name) diff --git a/dfetch/manifest/manifest.py b/dfetch/manifest/manifest.py index d80541c5..0f30e983 100644 --- a/dfetch/manifest/manifest.py +++ b/dfetch/manifest/manifest.py @@ -24,7 +24,7 @@ import re from collections.abc import Sequence from dataclasses import dataclass -from typing import IO, Any, Optional, Union +from typing import IO, Any import yaml from typing_extensions import NotRequired, TypedDict @@ -95,11 +95,9 @@ def _guess_project(self, names: Sequence[str]) -> Sequence[str]: class ManifestDict(TypedDict, total=True): # pylint: disable=too-many-ancestors """Serialized dict types.""" - version: Union[int, str] - remotes: NotRequired[Sequence[Union[RemoteDict, Remote]]] - projects: Sequence[ - Union[ProjectEntryDict, ProjectEntry, dict[str, Union[str, list[str]]]] - ] + version: int | str + remotes: NotRequired[Sequence[RemoteDict | Remote]] + projects: Sequence[ProjectEntryDict | ProjectEntry | dict[str, str | list[str]]] class Manifest: @@ -113,8 +111,8 @@ class Manifest: def __init__( self, manifest: ManifestDict, - text: Optional[str] = None, - path: Optional[Union[str, os.PathLike[str]]] = None, + text: str | None = None, + path: str | os.PathLike[str] | None = None, ) -> None: """Create the manifest.""" self.__version: str = str(manifest.get("version", self.CURRENT_VERSION)) @@ -142,7 +140,7 @@ def __init__( def _init_projects( self, projects: Sequence[ - Union[ProjectEntryDict, ProjectEntry, dict[str, Union[str, list[str]]]] + ProjectEntryDict | ProjectEntry | dict[str, str | list[str]] ], ) -> dict[str, ProjectEntry]: """Iterate over projects from manifest and initialize ProjectEntries from it. @@ -189,7 +187,7 @@ def _init_projects( @staticmethod def _determine_remotes( - remotes_from_manifest: Sequence[Union[RemoteDict, Remote]], + remotes_from_manifest: Sequence[RemoteDict | Remote], ) -> tuple[dict[str, Remote], list[Remote]]: default_remotes: list[Remote] = [] remotes: dict[str, Remote] = {} @@ -208,8 +206,8 @@ def _determine_remotes( @staticmethod def from_yaml( - text: Union[io.TextIOWrapper, str, IO[str]], - path: Optional[Union[str, os.PathLike[str]]] = None, + text: io.TextIOWrapper | str | IO[str], + path: str | os.PathLike[str] | None = None, ) -> "Manifest": """Create a manifest from a file like object.""" if isinstance(text, (io.TextIOWrapper, IO)): @@ -228,7 +226,7 @@ def from_yaml( return Manifest(manifest, text=text, path=path) @staticmethod - def _load_yaml(text: Union[io.TextIOWrapper, str, IO[str]]) -> Any: + def _load_yaml(text: io.TextIOWrapper | str | IO[str]) -> Any: try: return yaml.safe_load(text) except yaml.YAMLError as exc: @@ -306,9 +304,9 @@ def _as_dict(self) -> dict[str, ManifestDict]: if len(remotes) == 1: remotes[0].pop("default", None) - projects: list[dict[str, Union[str, list[str]]]] = [] + projects: list[dict[str, str | list[str]]] = [] for project in self.projects: - project_yaml: dict[str, Union[str, list[str]]] = project.as_yaml() + project_yaml: dict[str, str | list[str]] = project.as_yaml() if len(remotes) == 1: project_yaml.pop("remote", None) projects.append(project_yaml) diff --git a/dfetch/manifest/parse.py b/dfetch/manifest/parse.py index 5685a99c..9483443b 100644 --- a/dfetch/manifest/parse.py +++ b/dfetch/manifest/parse.py @@ -2,7 +2,7 @@ import os import pathlib -from typing import Any, Optional, cast +from typing import Any, cast from strictyaml import StrictYAMLError, YAMLValidationError, load @@ -82,7 +82,7 @@ def find_manifest() -> str: return os.path.realpath(paths[0]) -def get_childmanifests(skip: Optional[list[str]] = None) -> list[Manifest]: +def get_childmanifests(skip: list[str] | None = None) -> list[Manifest]: """Parse & validate any manifest file in cwd and return a list of all valid manifests.""" skip = skip or [] logger.debug("Looking for sub-manifests") diff --git a/dfetch/manifest/project.py b/dfetch/manifest/project.py index 567cfe15..da0a2998 100644 --- a/dfetch/manifest/project.py +++ b/dfetch/manifest/project.py @@ -274,7 +274,6 @@ import copy from collections.abc import Sequence -from typing import Optional, Union from typing_extensions import Required, TypedDict @@ -291,7 +290,7 @@ "src": str, "dst": str, "url": str, - "patch": Union[str, list[str]], + "patch": str | list[str], "repo": str, "branch": str, "tag": str, @@ -315,7 +314,7 @@ def __init__(self, kwargs: ProjectEntryDict) -> None: self._revision: str = kwargs.get("revision", "") self._remote: str = kwargs.get("remote", "") - self._remote_obj: Optional[Remote] = None + self._remote_obj: Remote | None = None self._src: str = kwargs.get("src", "") # noqa self._dst: str = kwargs.get("dst", self._name) self._url: str = kwargs.get("url", "") @@ -332,7 +331,7 @@ def __init__(self, kwargs: ProjectEntryDict) -> None: @classmethod def from_yaml( cls, - yamldata: Union[dict[str, Union[str, list[str]]], ProjectEntryDict], + yamldata: dict[str, str | list[str]] | ProjectEntryDict, default_remote: str = "", ) -> "ProjectEntry": """Create a Project Entry from yaml data. @@ -461,7 +460,7 @@ def as_recommendation(self) -> "ProjectEntry": recommendation._repo_path = "" # pylint: disable=protected-access return recommendation - def as_yaml(self) -> dict[str, Union[str, list[str]]]: + def as_yaml(self) -> dict[str, str | list[str]]: """Get this project as yaml dictionary.""" yamldata = { "name": self._name, diff --git a/dfetch/manifest/remote.py b/dfetch/manifest/remote.py index 8fda4606..7e538399 100644 --- a/dfetch/manifest/remote.py +++ b/dfetch/manifest/remote.py @@ -18,8 +18,6 @@ url-base: https://github.com/ """ -from typing import Optional, Union - from typing_extensions import TypedDict _MandatoryRemoteDict = TypedDict("_MandatoryRemoteDict", {"name": str, "url-base": str}) @@ -28,7 +26,7 @@ class RemoteDict(_MandatoryRemoteDict, total=False): """Class representing data types of Remote class construction.""" - default: Optional[bool] + default: bool | None class Remote: @@ -41,7 +39,7 @@ def __init__(self, kwargs: RemoteDict) -> None: self._default: bool = bool(kwargs.get("default", False)) @classmethod - def from_yaml(cls, yamldata: Union[dict[str, str], RemoteDict]) -> "Remote": + def from_yaml(cls, yamldata: dict[str, str] | RemoteDict) -> "Remote": """Create a remote entry in the manifest from yaml data. Returns: diff --git a/dfetch/project/__init__.py b/dfetch/project/__init__.py index 7c45da7a..57dfde14 100644 --- a/dfetch/project/__init__.py +++ b/dfetch/project/__init__.py @@ -2,7 +2,6 @@ import os import pathlib -from typing import Union import dfetch.manifest.project from dfetch.log import get_logger @@ -48,7 +47,7 @@ def create_super_project() -> SuperProject: return determine_superproject_vcs(root_directory)(manifest, root_directory) -def determine_superproject_vcs(path: Union[str, pathlib.Path]) -> type[SuperProject]: +def determine_superproject_vcs(path: str | pathlib.Path) -> type[SuperProject]: """Determine correct VCS type of the superproject in the given path.""" for project_type in SUPPORTED_SUPERPROJECT_TYPES: if project_type.check(path): diff --git a/dfetch/project/metadata.py b/dfetch/project/metadata.py index b06af09c..0f611c81 100644 --- a/dfetch/project/metadata.py +++ b/dfetch/project/metadata.py @@ -2,7 +2,6 @@ import datetime import os -from typing import Optional, Union import yaml from typing_extensions import TypedDict @@ -27,7 +26,7 @@ class Options(TypedDict): # pylint: disable=too-many-ancestors remote_url: str destination: str hash: str - patch: Union[str, list[str]] + patch: str | list[str] class Metadata: @@ -78,7 +77,7 @@ def from_file(cls, path: str) -> "Metadata": return cls(data) def fetched( - self, version: Version, hash_: str = "", patch_: Optional[list[str]] = None + self, version: Version, hash_: str = "", patch_: list[str] | None = None ) -> None: """Update metadata.""" self._last_fetch = datetime.datetime.now() diff --git a/dfetch/project/subproject.py b/dfetch/project/subproject.py index 965771d4..d787c9bd 100644 --- a/dfetch/project/subproject.py +++ b/dfetch/project/subproject.py @@ -5,7 +5,6 @@ import pathlib from abc import ABC, abstractmethod from collections.abc import Sequence -from typing import Optional from dfetch.log import get_logger from dfetch.manifest.project import ProjectEntry @@ -42,7 +41,7 @@ def _running_in_ci() -> bool: ci_env_var = os.getenv("CI", "") return bool(ci_env_var) and ci_env_var[0].lower() in ("t", "1", "y") - def check_wanted_with_local(self) -> tuple[Optional[Version], Optional[Version]]: + def check_wanted_with_local(self) -> tuple[Version | None, Version | None]: """Given the project entry in the manifest, get the relevant version from disk. Returns: @@ -74,7 +73,7 @@ def check_wanted_with_local(self) -> tuple[Optional[Version], Optional[Version]] Version(revision=on_disk.revision, branch=on_disk_branch), ) - def update_is_required(self, force: bool = False) -> Optional[Version]: + def update_is_required(self, force: bool = False) -> Version | None: """Check if this project should be upgraded. Args: @@ -93,7 +92,7 @@ def update_is_required(self, force: bool = False) -> Optional[Version]: def update( self, force: bool = False, - files_to_ignore: Optional[Sequence[str]] = None, + files_to_ignore: Sequence[str] | None = None, patch_count: int = -1, ) -> None: """Update this subproject if required. @@ -298,7 +297,7 @@ def _list_of_tags(self) -> list[str]: def list_tool_info() -> None: """Print out version information.""" - def on_disk_version(self) -> Optional[Version]: + def on_disk_version(self) -> Version | None: """Get the version of the project on disk. Returns: @@ -317,7 +316,7 @@ def on_disk_version(self) -> Optional[Version]: ) return None - def _on_disk_hash(self) -> Optional[str]: + def _on_disk_hash(self) -> str | None: """Get the hash of the project on disk. Returns: @@ -336,7 +335,7 @@ def _on_disk_hash(self) -> Optional[str]: ) return None - def _check_for_newer_version(self) -> Optional[Version]: + def _check_for_newer_version(self) -> Version | None: """Check if a newer version is available on the given branch. In case wanted_version does not exist (anymore) on the remote return None. @@ -400,3 +399,5 @@ def is_license_file(filename: str) -> bool: @abstractmethod def create_formatted_patch_header(self, patch_info: PatchInfo) -> str: """Create a formatted patch header for the given patch info.""" + del patch_info + return "" diff --git a/dfetch/util/cmdline.py b/dfetch/util/cmdline.py index d0de8245..d3bfdc9d 100644 --- a/dfetch/util/cmdline.py +++ b/dfetch/util/cmdline.py @@ -4,7 +4,7 @@ import os import subprocess # nosec from collections.abc import Mapping -from typing import Any, Optional +from typing import Any class SubprocessCommandError(Exception): @@ -16,7 +16,7 @@ class SubprocessCommandError(Exception): def __init__( self, - cmd: Optional[list[str]] = None, + cmd: list[str] | None = None, stdout: str = "", stderr: str = "", returncode: int = 0, @@ -39,7 +39,7 @@ def message(self) -> str: def run_on_cmdline( logger: logging.Logger, cmd: list[str], - env: Optional[Mapping[str, str]] = None, + env: Mapping[str, str] | None = None, ) -> "subprocess.CompletedProcess[Any]": """Run a command and log the output, and raise if something goes wrong.""" logger.debug(f"Running {cmd}") diff --git a/dfetch/util/license.py b/dfetch/util/license.py index 47a2bb61..bf6a693b 100644 --- a/dfetch/util/license.py +++ b/dfetch/util/license.py @@ -2,7 +2,6 @@ from dataclasses import dataclass from os import PathLike -from typing import Optional, Union import infer_license from infer_license.types import License as InferredLicense @@ -21,7 +20,7 @@ class License: name: str #: SPDX Full name spdx_id: str #: SPDX Identifier - trove_classifier: Optional[str] #: Python package classifier + trove_classifier: str | None #: Python package classifier probability: float #: Confidence level of the license inference @staticmethod @@ -46,8 +45,8 @@ def from_inferred( def guess_license_in_file( - filename: Union[str, PathLike[str]], -) -> Optional[License]: + filename: str | PathLike[str], +) -> License | None: """Attempt to identify the license of a given file. Tries UTF-8 encoding first, falling back to Latin-1 for legacy license files. diff --git a/dfetch/util/purl.py b/dfetch/util/purl.py index 1cdbb99e..f7a7a819 100644 --- a/dfetch/util/purl.py +++ b/dfetch/util/purl.py @@ -4,7 +4,6 @@ """ import re -from typing import Optional from urllib.parse import urlparse from packageurl import PackageURL @@ -54,8 +53,8 @@ def _namespace_and_name_from_domain_and_path(domain: str, path: str) -> tuple[st def _known_purl_types( - remote_url: str, version: Optional[str] = None, subpath: Optional[str] = None -) -> Optional[PackageURL]: + remote_url: str, version: str | None = None, subpath: str | None = None +) -> PackageURL | None: match = GITHUB_REGEX.match(remote_url) if match: return PackageURL( @@ -79,7 +78,7 @@ def _known_purl_types( def remote_url_to_purl( - remote_url: str, version: Optional[str] = None, subpath: Optional[str] = None + remote_url: str, version: str | None = None, subpath: str | None = None ) -> PackageURL: """Convert a remote URL to a valid PackageURL object. diff --git a/dfetch/util/util.py b/dfetch/util/util.py index 5ee2a450..b5f83b20 100644 --- a/dfetch/util/util.py +++ b/dfetch/util/util.py @@ -8,7 +8,7 @@ from collections.abc import Generator, Iterator, Sequence from contextlib import contextmanager from pathlib import Path -from typing import Any, Optional, Union +from typing import Any from _hashlib import HASH @@ -42,7 +42,7 @@ def find_matching_files(directory: str, patterns: Sequence[str]) -> Iterator[Pat yield Path(path) -def safe_rm(path: Union[str, Path]) -> None: +def safe_rm(path: str | Path) -> None: """Delete an file or directory safely.""" if os.path.isdir(path): safe_rmtree(str(path)) @@ -63,7 +63,7 @@ def safe_rmtree(path: str) -> None: @contextmanager -def in_directory(path: Union[str, Path]) -> Generator[str, None, None]: +def in_directory(path: str | Path) -> Generator[str, None, None]: """Work temporarily in a given directory.""" pwd = os.getcwd() if not os.path.isdir(path): @@ -77,7 +77,7 @@ def in_directory(path: Union[str, Path]) -> Generator[str, None, None]: @contextmanager def catch_runtime_exceptions( - exc_list: Optional[list[str]] = None, + exc_list: list[str] | None = None, ) -> Generator[list[str], None, None]: """Catch all runtime errors and add it to list of strings.""" exc_list = exc_list or [] @@ -105,7 +105,7 @@ def find_file(name: str, path: str = ".") -> list[str]: ] -def hash_directory(path: str, skiplist: Optional[list[str]]) -> str: +def hash_directory(path: str, skiplist: list[str] | None) -> str: """Hash a directory with all its files.""" digest = hashlib.md5(usedforsecurity=False) skiplist = skiplist or [] @@ -136,7 +136,7 @@ def hash_file(file_path: str, digest: HASH) -> HASH: return digest -def always_str_list(data: Union[str, list[str]]) -> list[str]: +def always_str_list(data: str | list[str]) -> list[str]: """Convert a string or list of strings into a list of strings. Args: @@ -148,7 +148,7 @@ def always_str_list(data: Union[str, list[str]]) -> list[str]: return data if not isinstance(data, str) else [data] if data else [] -def str_if_possible(data: list[str]) -> Union[str, list[str]]: +def str_if_possible(data: list[str]) -> str | list[str]: """Convert a single-element list to a string, otherwise keep as list. Args: @@ -161,7 +161,7 @@ def str_if_possible(data: list[str]) -> Union[str, list[str]]: return "" if not data else data[0] if len(data) == 1 else data -def resolve_absolute_path(path: Union[str, Path]) -> Path: +def resolve_absolute_path(path: str | Path) -> Path: """Return a guaranteed absolute Path, resolving symlinks. Args: diff --git a/dfetch/util/versions.py b/dfetch/util/versions.py index 6142d46d..cca2302e 100644 --- a/dfetch/util/versions.py +++ b/dfetch/util/versions.py @@ -2,7 +2,6 @@ import re from collections import defaultdict -from typing import Optional from semver.version import Version @@ -20,7 +19,7 @@ ) -def coerce(version: str) -> tuple[str, Optional[Version], str]: +def coerce(version: str) -> tuple[str, Version | None, str]: """Convert an incomplete version string into a semver-compatible Version object. * Tries to detect a "basic" version string (``major.minor.patch``). diff --git a/dfetch/vcs/git.py b/dfetch/vcs/git.py index 9c47cf67..dbb28f66 100644 --- a/dfetch/vcs/git.py +++ b/dfetch/vcs/git.py @@ -7,7 +7,7 @@ import tempfile from collections.abc import Generator, Sequence from pathlib import Path, PurePath -from typing import NamedTuple, Optional, Union +from typing import NamedTuple from dfetch.log import get_logger from dfetch.util.cmdline import SubprocessCommandError, run_on_cmdline @@ -234,7 +234,7 @@ class GitLocalRepo: METADATA_DIR = ".git" - def __init__(self, path: Union[str, Path] = ".") -> None: + def __init__(self, path: str | Path = ".") -> None: """Create a local git repo.""" self._path = str(path) @@ -255,9 +255,9 @@ def checkout_version( # pylint: disable=too-many-arguments *, remote: str, version: str, - src: Optional[str] = None, - must_keeps: Optional[list[str]] = None, - ignore: Optional[Sequence[str]] = None, + src: str | None = None, + must_keeps: list[str] | None = None, + ignore: Sequence[str] | None = None, ) -> str: """Checkout a specific version from a given remote. @@ -332,7 +332,7 @@ def move_src_folder_up(self, remote: str, src: str) -> None: @staticmethod def _determine_ignore_paths( - src: Optional[str], ignore: Sequence[str] + src: str | None, ignore: Sequence[str] ) -> Generator[str, None, None]: """Determine the ignore patterns relative to the given src.""" if not src: @@ -380,9 +380,9 @@ def get_remote_url() -> str: def create_diff( self, - old_hash: Optional[str], - new_hash: Optional[str], - ignore: Optional[Sequence[str]] = None, + old_hash: str | None, + new_hash: str | None, + ignore: Sequence[str] | None = None, reverse: bool = False, ) -> str: """Generate a relative diff patch.""" @@ -457,7 +457,7 @@ def any_changes_or_untracked(path: str) -> bool: .splitlines() ) - def untracked_files_patch(self, ignore: Optional[Sequence[str]] = None) -> str: + def untracked_files_patch(self, ignore: Sequence[str] | None = None) -> str: """Create a diff for untracked files.""" with in_directory(self._path): untracked_files = ( diff --git a/dfetch/vcs/patch.py b/dfetch/vcs/patch.py index 44e6a650..23591759 100644 --- a/dfetch/vcs/patch.py +++ b/dfetch/vcs/patch.py @@ -9,7 +9,6 @@ from dataclasses import dataclass, field from email.utils import format_datetime from pathlib import Path -from typing import Union import patch_ng @@ -250,7 +249,7 @@ def to_svn_header(self) -> str: return "" -def parse_patch(file_path: Union[str, Path]) -> patch_ng.PatchSet: +def parse_patch(file_path: str | Path) -> patch_ng.PatchSet: """Parse the patch from file_path.""" patch = patch_ng.fromfile(str(file_path)) if not patch or not patch.items: diff --git a/dfetch/vcs/svn.py b/dfetch/vcs/svn.py index 02d62667..793eff3a 100644 --- a/dfetch/vcs/svn.py +++ b/dfetch/vcs/svn.py @@ -5,7 +5,7 @@ import re from collections.abc import Sequence from pathlib import Path -from typing import NamedTuple, Optional, Union +from typing import NamedTuple from dfetch.log import get_logger from dfetch.util.cmdline import SubprocessCommandError, run_on_cmdline @@ -77,7 +77,7 @@ class SvnRepo: def __init__( self, - path: Union[str, pathlib.Path] = ".", + path: str | pathlib.Path = ".", ) -> None: """Create a svn repo.""" self._path = str(path) @@ -111,7 +111,7 @@ def externals(self) -> list[External]: # Pattern matches: "path - ..." where path is the local directory path_pattern = r"([^\s^-]+)\s+-" for entry in result.stdout.decode().split(os.linesep * 2): - match: Optional[re.Match[str]] = None + match: re.Match[str] | None = None local_path: str = "" for match in re.finditer(path_pattern, entry): pass @@ -203,7 +203,7 @@ def get_info_from_target(target: str = "") -> dict[str, str]: } @staticmethod - def get_last_changed_revision(target: Union[str, Path]) -> str: + def get_last_changed_revision(target: str | Path) -> str: """Get the last changed revision of the given target.""" target_str = str(target).strip() if os.path.isdir(target_str): @@ -316,7 +316,7 @@ def any_changes_or_untracked(path: str) -> bool: def create_diff( self, old_revision: str, - new_revision: Optional[str], + new_revision: str | None, ignore: Sequence[str], ) -> str: """Generate a relative diff patch.""" diff --git a/pyproject.toml b/pyproject.toml index 88f1ef44..bc97b494 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "dfetch" authors = [{ name = "Ben Spoor", email = "dfetch@spoor.cc" }] description = "A vendoring tool for fetching and managing external dependencies." readme = { file = "README.md", content-type = "text/markdown" } -requires-python = ">=3.9" +requires-python = ">=3.10" keywords = [ "dfetch", "dependency management", @@ -28,7 +28,6 @@ classifiers = [ "Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: Developers", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -191,7 +190,7 @@ exclude = ["doc/static/uml/generate_diagram.py", "./doc/_ext/sphinxcontrib_ascii # standard = ["dfetch", "features"] reportMissingImports = false reportMissingModuleSource = false -pythonVersion = "3.9" +pythonVersion = "3.10" [tool.nuitka] mode = "standalone"