From 412f80f0fcffef49814e5d235b781f77ec79fd5f Mon Sep 17 00:00:00 2001 From: David Seddon Date: Tue, 4 Nov 2025 07:26:55 +0000 Subject: [PATCH 1/4] Add autofix-python recipe --- justfile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/justfile b/justfile index 90debc13..2f09c250 100644 --- a/justfile +++ b/justfile @@ -114,6 +114,11 @@ lint-rust: autofix-rust: @cargo clippy --all-targets --all-features --fix --allow-staged --allow-dirty +# Fix any ruff errors +[group('linting')] +autofix-python: + @uv run ruff check --fix + # Run linters. [group('linting')] lint: From 9d236ab99ab439a29795ff8967456907160ffdbc Mon Sep 17 00:00:00 2001 From: David Seddon Date: Mon, 3 Nov 2025 14:43:27 +0000 Subject: [PATCH 2/4] Add upgrade-python recipe --- justfile | 7 +++++++ pyproject.toml | 1 + 2 files changed, 8 insertions(+) diff --git a/justfile b/justfile index 2f09c250..b0db10a3 100644 --- a/justfile +++ b/justfile @@ -156,6 +156,13 @@ show-benchmark-results: benchmark-ci: @uv run --group=benchmark-ci pytest --codspeed +# Upgrade Python code to the supplied version. (E.g. just upgrade 310) +[group('maintenance')] +upgrade-python MIN_VERSION: + @find {docs,src,tests} -name "*.py" -not -path "tests/assets/*" -exec uv run pyupgrade --py{{MIN_VERSION}}-plus --exit-zero-even-if-changed {} + + @just autofix-python + @just format-python + # Run all linters, build docs and tests. Worth running before pushing to Github. [group('prepush')] full-check: diff --git a/pyproject.toml b/pyproject.toml index 26ed187d..0447b64b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,6 +61,7 @@ dev = [ "requests==2.32.3", "sqlalchemy==2.0.35", "google-cloud-audit-log==0.3.0", + "pyupgrade>=3.21.0", ] docs = [ "sphinx>=7.4.7", From 27cf856952b79a69eba3f1fa44b4961abc4f1a6c Mon Sep 17 00:00:00 2001 From: David Seddon Date: Tue, 4 Nov 2025 07:29:26 +0000 Subject: [PATCH 3/4] Drop support for Python 3.9 --- .github/workflows/main.yml | 2 +- .github/workflows/release.yml | 4 ---- CHANGELOG.rst | 5 +++++ pyproject.toml | 3 +-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 89e602f5..836dc23d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -39,7 +39,7 @@ jobs: strategy: matrix: python-version: [ - "3.9", "3.10", "3.11", "3.12", "3.13", "3.14" + "3.10", "3.11", "3.12", "3.13", "3.14" ] os: [ubuntu-latest, macos-latest, windows-latest] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 19667b85..a829eb4f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,7 +37,6 @@ jobs: - uses: actions/setup-python@v6 with: python-version: | - 3.9 3.10 3.11 3.12 @@ -75,7 +74,6 @@ jobs: - uses: actions/setup-python@v6 with: python-version: | - 3.9 3.10 3.11 3.12 @@ -109,7 +107,6 @@ jobs: - uses: actions/setup-python@v6 with: python-version: | - 3.9 3.10 3.11 3.12 @@ -143,7 +140,6 @@ jobs: - uses: actions/setup-python@v6 with: python-version: | - 3.9 3.10 3.11 3.12 diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7669c4b3..2617c15e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,11 @@ Changelog ========= +latest +------ + +* Drop support for Python 3.9. + 3.13 (2025-10-29) ----------------- diff --git a/pyproject.toml b/pyproject.toml index 0447b64b..49efea25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ description = "Builds a queryable graph of the imports within one or more Python authors = [ {name = "David Seddon", email = "david@seddonym.me"}, ] -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = [ "typing-extensions>=3.10.0.0", ] @@ -26,7 +26,6 @@ classifiers = [ "Operating System :: POSIX", "Operating System :: Microsoft :: Windows", "Programming Language :: Python", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", From 06a831df4762f34be9b41e748cb9272fbaedcfcf Mon Sep 17 00:00:00 2001 From: David Seddon Date: Tue, 4 Nov 2025 07:34:06 +0000 Subject: [PATCH 4/4] Upgrade Python code to 3.10 --- docs/conf.py | 1 - src/grimp/adaptors/caching.py | 34 +++++++++---------- src/grimp/adaptors/filesystem.py | 6 ++-- src/grimp/adaptors/modulefinder.py | 4 +-- src/grimp/adaptors/packagefinder.py | 6 ++-- src/grimp/application/graph.py | 35 ++++++++++---------- src/grimp/application/ports/caching.py | 14 ++++---- src/grimp/application/ports/filesystem.py | 8 ++--- src/grimp/application/ports/modulefinder.py | 3 +- src/grimp/application/scanning.py | 6 ++-- src/grimp/application/usecases.py | 29 ++++++++-------- src/grimp/domain/analysis.py | 2 +- src/grimp/domain/valueobjects.py | 3 +- src/grimp/exceptions.py | 5 +-- tests/adaptors/filesystem.py | 21 ++++++------ tests/adaptors/modulefinder.py | 3 +- tests/adaptors/packagefinder.py | 4 +-- tests/functional/test_build_and_use_graph.py | 3 +- tests/functional/test_caching.py | 2 +- tests/unit/adaptors/test_caching.py | 3 +- tests/unit/adaptors/test_filesystem.py | 3 +- tests/unit/application/graph/test_chains.py | 16 ++++----- tests/unit/application/test_scanning.py | 4 +-- tests/unit/application/test_usecases.py | 7 ++-- 24 files changed, 102 insertions(+), 120 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 1ad93587..cf4bf0ae 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Configuration file for the Sphinx documentation builder. # diff --git a/src/grimp/adaptors/caching.py b/src/grimp/adaptors/caching.py index 13fe9167..0782923c 100644 --- a/src/grimp/adaptors/caching.py +++ b/src/grimp/adaptors/caching.py @@ -2,7 +2,7 @@ import json import logging -from typing import Dict, List, Optional, Set, Tuple, Type +from typing import Optional from grimp.application.ports.filesystem import AbstractFileSystem from grimp.application.ports.modulefinder import FoundPackage, ModuleFile @@ -13,7 +13,7 @@ from grimp import _rustgrimp as rust # type: ignore[attr-defined] logger = logging.getLogger(__name__) -PrimitiveFormat = Dict[str, List[Tuple[str, Optional[int], str]]] +PrimitiveFormat = dict[str, list[tuple[str, Optional[int], str]]] class CacheFileNamer: @@ -24,7 +24,7 @@ def make_meta_file_name(cls, found_package: FoundPackage) -> str: @classmethod def make_data_file_name( cls, - found_packages: Set[FoundPackage], + found_packages: set[FoundPackage], include_external_packages: bool, exclude_type_checking_imports: bool, ) -> str: @@ -42,7 +42,7 @@ def make_data_file_name( @classmethod def make_data_file_unique_string( cls, - found_packages: Set[FoundPackage], + found_packages: set[FoundPackage], include_external_packages: bool, exclude_type_checking_imports: bool, ) -> str: @@ -65,24 +65,24 @@ def make_data_file_unique_string( class Cache(AbstractCache): DEFAULT_CACHE_DIR = ".grimp_cache" - def __init__(self, *args, namer: Type[CacheFileNamer], **kwargs) -> None: + def __init__(self, *args, namer: type[CacheFileNamer], **kwargs) -> None: """ Don't instantiate Cache directly; use Cache.setup(). """ super().__init__(*args, **kwargs) - self._mtime_map: Dict[str, float] = {} - self._data_map: Dict[Module, Set[DirectImport]] = {} + self._mtime_map: dict[str, float] = {} + self._data_map: dict[Module, set[DirectImport]] = {} self._namer = namer @classmethod def setup( cls, file_system: AbstractFileSystem, - found_packages: Set[FoundPackage], + found_packages: set[FoundPackage], include_external_packages: bool, exclude_type_checking_imports: bool = False, - cache_dir: Optional[str] = None, - namer: Type[CacheFileNamer] = CacheFileNamer, + cache_dir: str | None = None, + namer: type[CacheFileNamer] = CacheFileNamer, ) -> "Cache": cache = cls( file_system=file_system, @@ -98,10 +98,10 @@ def setup( return cache @classmethod - def cache_dir_or_default(cls, cache_dir: Optional[str]) -> str: + def cache_dir_or_default(cls, cache_dir: str | None) -> str: return cache_dir or cls.DEFAULT_CACHE_DIR - def read_imports(self, module_file: ModuleFile) -> Set[DirectImport]: + def read_imports(self, module_file: ModuleFile) -> set[DirectImport]: try: cached_mtime = self._mtime_map[module_file.module.name] except KeyError: @@ -118,7 +118,7 @@ def read_imports(self, module_file: ModuleFile) -> Set[DirectImport]: def write( self, - imports_by_module: Dict[Module, Set[DirectImport]], + imports_by_module: dict[Module, set[DirectImport]], ) -> None: self._write_marker_files_if_not_already_there() # Write data file. @@ -165,13 +165,13 @@ def write( def _build_mtime_map(self) -> None: self._mtime_map = self._read_mtime_map_files() - def _read_mtime_map_files(self) -> Dict[str, float]: - all_mtimes: Dict[str, float] = {} + def _read_mtime_map_files(self) -> dict[str, float]: + all_mtimes: dict[str, float] = {} for found_package in self.found_packages: all_mtimes.update(self._read_mtime_map_file(found_package)) return all_mtimes - def _read_mtime_map_file(self, found_package: FoundPackage) -> Dict[str, float]: + def _read_mtime_map_file(self, found_package: FoundPackage) -> dict[str, float]: meta_cache_filename = self.file_system.join( self.cache_dir, self._namer.make_meta_file_name(found_package) ) @@ -191,7 +191,7 @@ def _read_mtime_map_file(self, found_package: FoundPackage) -> Dict[str, float]: def _build_data_map(self) -> None: self._data_map = self._read_data_map_file() - def _read_data_map_file(self) -> Dict[Module, Set[DirectImport]]: + def _read_data_map_file(self) -> dict[Module, set[DirectImport]]: data_cache_filename = self.file_system.join( self.cache_dir, self._namer.make_data_file_name( diff --git a/src/grimp/adaptors/filesystem.py b/src/grimp/adaptors/filesystem.py index 83b54436..cded5eab 100644 --- a/src/grimp/adaptors/filesystem.py +++ b/src/grimp/adaptors/filesystem.py @@ -1,6 +1,6 @@ import os import tokenize -from typing import Iterator, List, Tuple +from collections.abc import Iterator from grimp.application.ports.filesystem import AbstractFileSystem, BasicFileSystem from grimp import _rustgrimp as rust # type: ignore[attr-defined] @@ -18,13 +18,13 @@ def sep(self) -> str: def dirname(self, filename: str) -> str: return os.path.dirname(filename) - def walk(self, directory_name: str) -> Iterator[Tuple[str, List[str], List[str]]]: + def walk(self, directory_name: str) -> Iterator[tuple[str, list[str], list[str]]]: yield from os.walk(directory_name, followlinks=True) def join(self, *components: str) -> str: return os.path.join(*components) - def split(self, file_name: str) -> Tuple[str, str]: + def split(self, file_name: str) -> tuple[str, str]: return os.path.split(file_name) def read(self, file_name: str) -> str: diff --git a/src/grimp/adaptors/modulefinder.py b/src/grimp/adaptors/modulefinder.py index 36d51a59..934190f9 100644 --- a/src/grimp/adaptors/modulefinder.py +++ b/src/grimp/adaptors/modulefinder.py @@ -1,5 +1,5 @@ import logging -from typing import Iterable, List +from collections.abc import Iterable from grimp.application.ports import modulefinder from grimp.application.ports.filesystem import AbstractFileSystem @@ -14,7 +14,7 @@ def find_package( ) -> modulefinder.FoundPackage: self.file_system = file_system - module_files: List[modulefinder.ModuleFile] = [] + module_files: list[modulefinder.ModuleFile] = [] for module_filename in self._get_python_files_inside_package(package_directory): module_name = self._module_name_from_filename( diff --git a/src/grimp/adaptors/packagefinder.py b/src/grimp/adaptors/packagefinder.py index 8c95abb8..e87f644e 100644 --- a/src/grimp/adaptors/packagefinder.py +++ b/src/grimp/adaptors/packagefinder.py @@ -19,10 +19,8 @@ def determine_package_directory( # Attempt to locate the package file. spec = importlib.util.find_spec(package_name) if not spec: - logger.debug("sys.path: {}".format(sys.path)) - raise ValueError( - "Could not find package '{}' in your Python path.".format(package_name) - ) + logger.debug(f"sys.path: {sys.path}") + raise ValueError(f"Could not find package '{package_name}' in your Python path.") if spec.has_location and spec.origin: if not self._is_a_package(spec, file_system) or self._has_a_non_namespace_parent(spec): diff --git a/src/grimp/application/graph.py b/src/grimp/application/graph.py index 4c31f52b..0847511c 100644 --- a/src/grimp/application/graph.py +++ b/src/grimp/application/graph.py @@ -1,5 +1,6 @@ from __future__ import annotations -from typing import List, Optional, Sequence, Set, Tuple, TypedDict +from typing import TypedDict +from collections.abc import Sequence from grimp.domain.analysis import PackageDependency, Route from grimp.domain.valueobjects import Layer from grimp import _rustgrimp as rust # type: ignore[attr-defined] @@ -18,7 +19,7 @@ class Import(TypedDict): # Corresponds to importer, imported. # Prefer this form to Import, as it's both more lightweight, and hashable. -ImportTuple = Tuple[str, str] +ImportTuple = tuple[str, str] class DetailedImport(Import): @@ -33,14 +34,14 @@ class ImportGraph: def __init__(self) -> None: super().__init__() - self._cached_modules: Set[str] | None = None + self._cached_modules: set[str] | None = None self._rustgraph = rust.Graph() # Mechanics # --------- @property - def modules(self) -> Set[str]: + def modules(self) -> set[str]: """ The names of all the modules in the graph. """ @@ -48,7 +49,7 @@ def modules(self) -> Set[str]: self._cached_modules = self._rustgraph.get_modules() return self._cached_modules - def find_matching_modules(self, expression: str) -> Set[str]: + def find_matching_modules(self, expression: str) -> set[str]: """ Find all modules matching the passed expression. @@ -135,8 +136,8 @@ def add_import( *, importer: str, imported: str, - line_number: Optional[int] = None, - line_contents: Optional[str] = None, + line_number: int | None = None, + line_contents: str | None = None, ) -> None: """ Add a direct import between two modules to the graph. If the modules are not already @@ -166,7 +167,7 @@ def count_imports(self) -> int: # Descendants # ----------- - def find_children(self, module: str) -> Set[str]: + def find_children(self, module: str) -> set[str]: """ Find all modules one level below the module. For example, the children of foo.bar might be foo.bar.one and foo.bar.two, but not foo.bar.two.green. @@ -180,7 +181,7 @@ def find_children(self, module: str) -> Set[str]: raise ValueError("Cannot find children of a squashed module.") return self._rustgraph.find_children(module) - def find_descendants(self, module: str) -> Set[str]: + def find_descendants(self, module: str) -> set[str]: """ Find all modules below the module. For example, the descendants of foo.bar might be foo.bar.one and foo.bar.two and foo.bar.two.green. @@ -213,16 +214,16 @@ def direct_import_exists( importer=importer, imported=imported, as_packages=as_packages ) - def find_modules_directly_imported_by(self, module: str) -> Set[str]: + def find_modules_directly_imported_by(self, module: str) -> set[str]: return self._rustgraph.find_modules_directly_imported_by(module) - def find_modules_that_directly_import(self, module: str) -> Set[str]: + def find_modules_that_directly_import(self, module: str) -> set[str]: if self._rustgraph.contains_module(module): # TODO panics if module isn't in modules. return self._rustgraph.find_modules_that_directly_import(module) return set() - def get_import_details(self, *, importer: str, imported: str) -> List[DetailedImport]: + def get_import_details(self, *, importer: str, imported: str) -> list[DetailedImport]: """ Return available metadata relating to the direct imports between two modules, in the form: [ @@ -246,7 +247,7 @@ def get_import_details(self, *, importer: str, imported: str) -> List[DetailedIm imported=imported, ) - def find_matching_direct_imports(self, import_expression: str) -> List[Import]: + def find_matching_direct_imports(self, import_expression: str) -> list[Import]: """ Find all direct imports matching the passed expressions. @@ -290,7 +291,7 @@ def find_matching_direct_imports(self, import_expression: str) -> List[Import]: # Indirect imports # ---------------- - def find_downstream_modules(self, module: str, as_package: bool = False) -> Set[str]: + def find_downstream_modules(self, module: str, as_package: bool = False) -> set[str]: """ Return a set of the names of all the modules that import (even indirectly) the supplied module name. @@ -312,7 +313,7 @@ def find_downstream_modules(self, module: str, as_package: bool = False) -> Set[ """ return self._rustgraph.find_downstream_modules(module, as_package) - def find_upstream_modules(self, module: str, as_package: bool = False) -> Set[str]: + def find_upstream_modules(self, module: str, as_package: bool = False) -> set[str]: """ Return a set of the names of all the modules that are imported (even indirectly) by the supplied module. @@ -352,7 +353,7 @@ def find_shortest_chain( def find_shortest_chains( self, importer: str, imported: str, as_packages: bool = True - ) -> Set[Tuple[str, ...]]: + ) -> set[tuple[str, ...]]: """ Find the shortest import chains that exist between the importer and imported, and between any modules contained within them if as_packages is True. Only one chain per @@ -474,7 +475,7 @@ def __repr__(self) -> str: stringified_modules = "empty" return f"<{self.__class__.__name__}: {stringified_modules}>" - def __deepcopy__(self, memodict: dict) -> "ImportGraph": + def __deepcopy__(self, memodict: dict) -> ImportGraph: new_graph = ImportGraph() new_graph._rustgraph = self._rustgraph.clone() return new_graph diff --git a/src/grimp/application/ports/caching.py b/src/grimp/application/ports/caching.py index d3f58f9b..e04a1362 100644 --- a/src/grimp/application/ports/caching.py +++ b/src/grimp/application/ports/caching.py @@ -1,5 +1,3 @@ -from typing import Dict, Optional, Set - from grimp.application.ports.modulefinder import FoundPackage, ModuleFile from grimp.domain.valueobjects import DirectImport, Module @@ -16,7 +14,7 @@ def __init__( file_system: AbstractFileSystem, include_external_packages: bool, exclude_type_checking_imports: bool, - found_packages: Set[FoundPackage], + found_packages: set[FoundPackage], cache_dir: str, ) -> None: """ @@ -32,11 +30,11 @@ def __init__( def setup( cls, file_system: AbstractFileSystem, - found_packages: Set[FoundPackage], + found_packages: set[FoundPackage], *, include_external_packages: bool, exclude_type_checking_imports: bool = False, - cache_dir: Optional[str] = None, + cache_dir: str | None = None, ) -> "Cache": cache = cls( file_system=file_system, @@ -47,15 +45,15 @@ def setup( ) return cache - def read_imports(self, module_file: ModuleFile) -> Set[DirectImport]: + def read_imports(self, module_file: ModuleFile) -> set[DirectImport]: raise NotImplementedError def write( self, - imports_by_module: Dict[Module, Set[DirectImport]], + imports_by_module: dict[Module, set[DirectImport]], ) -> None: raise NotImplementedError @classmethod - def cache_dir_or_default(cls, cache_dir: Optional[str]) -> str: + def cache_dir_or_default(cls, cache_dir: str | None) -> str: raise NotImplementedError diff --git a/src/grimp/application/ports/filesystem.py b/src/grimp/application/ports/filesystem.py index 5e966cc7..6597f8ac 100644 --- a/src/grimp/application/ports/filesystem.py +++ b/src/grimp/application/ports/filesystem.py @@ -1,6 +1,6 @@ from __future__ import annotations import abc -from typing import Iterator, List, Tuple +from collections.abc import Iterator from typing import Protocol @@ -28,7 +28,7 @@ def dirname(self, filename: str) -> str: raise NotImplementedError @abc.abstractmethod - def walk(self, directory_name: str) -> Iterator[Tuple[str, List[str], List[str]]]: + def walk(self, directory_name: str) -> Iterator[tuple[str, list[str], list[str]]]: """ Given a directory, walk the file system recursively. @@ -42,7 +42,7 @@ def join(self, *components: str) -> str: raise NotImplementedError @abc.abstractmethod - def split(self, file_name: str) -> Tuple[str, str]: + def split(self, file_name: str) -> tuple[str, str]: """ Split the pathname path into a pair, (head, tail) where tail is the last pathname component and head is everything leading up to that. The tail part will never contain a slash; @@ -105,7 +105,7 @@ class BasicFileSystem(Protocol): def join(self, *components: str) -> str: ... - def split(self, file_name: str) -> Tuple[str, str]: ... + def split(self, file_name: str) -> tuple[str, str]: ... def read(self, file_name: str) -> str: ... diff --git a/src/grimp/application/ports/modulefinder.py b/src/grimp/application/ports/modulefinder.py index 19375a2a..bff66077 100644 --- a/src/grimp/application/ports/modulefinder.py +++ b/src/grimp/application/ports/modulefinder.py @@ -1,6 +1,5 @@ import abc from dataclasses import dataclass -from typing import FrozenSet from grimp.domain.valueobjects import Module @@ -21,7 +20,7 @@ class FoundPackage: name: str directory: str - module_files: FrozenSet[ModuleFile] + module_files: frozenset[ModuleFile] class AbstractModuleFinder(abc.ABC): diff --git a/src/grimp/application/scanning.py b/src/grimp/application/scanning.py index a8ce270f..09eab3c8 100644 --- a/src/grimp/application/scanning.py +++ b/src/grimp/application/scanning.py @@ -1,4 +1,4 @@ -from typing import Collection, Set, Dict +from collections.abc import Collection from grimp import _rustgrimp as rust # type: ignore[attr-defined] from grimp.domain.valueobjects import DirectImport, Module @@ -10,10 +10,10 @@ def scan_imports( module_files: Collection[ModuleFile], *, - found_packages: Set[FoundPackage], + found_packages: set[FoundPackage], include_external_packages: bool, exclude_type_checking_imports: bool, -) -> Dict[ModuleFile, Set[DirectImport]]: +) -> dict[ModuleFile, set[DirectImport]]: file_system: AbstractFileSystem = settings.FILE_SYSTEM basic_file_system = file_system.convert_to_basic() imports_by_module: dict[Module, set[DirectImport]] = rust.scan_for_imports( diff --git a/src/grimp/application/usecases.py b/src/grimp/application/usecases.py index 45d581f8..ef5b858a 100644 --- a/src/grimp/application/usecases.py +++ b/src/grimp/application/usecases.py @@ -2,7 +2,8 @@ Use cases handle application logic. """ -from typing import Dict, Sequence, Set, Type, Union, cast, Iterable +from typing import cast +from collections.abc import Sequence, Iterable from .scanning import scan_imports from ..application.ports import caching @@ -23,7 +24,7 @@ def build_graph( *additional_package_names, include_external_packages: bool = False, exclude_type_checking_imports: bool = False, - cache_dir: Union[str, Type[NotSupplied], None] = NotSupplied, + cache_dir: str | type[NotSupplied] | None = NotSupplied, ) -> ImportGraph: """ Build and return an import graph for the supplied package name(s). @@ -69,13 +70,13 @@ def build_graph( def _find_packages( file_system: AbstractFileSystem, package_names: Sequence[object] -) -> Set[FoundPackage]: +) -> set[FoundPackage]: package_names = _validate_package_names_are_strings(package_names) module_finder: AbstractModuleFinder = settings.MODULE_FINDER package_finder: AbstractPackageFinder = settings.PACKAGE_FINDER - found_packages: Set[FoundPackage] = set() + found_packages: set[FoundPackage] = set() for package_name in package_names: package_directory = package_finder.determine_package_directory( @@ -100,12 +101,12 @@ def _validate_package_names_are_strings( def _scan_packages( - found_packages: Set[FoundPackage], + found_packages: set[FoundPackage], file_system: AbstractFileSystem, include_external_packages: bool, exclude_type_checking_imports: bool, - cache_dir: Union[str, Type[NotSupplied], None], -) -> Dict[Module, Set[DirectImport]]: + cache_dir: str | type[NotSupplied] | None, +) -> dict[Module, set[DirectImport]]: if cache_dir is not None: cache_dir_if_supplied = cache_dir if cache_dir != NotSupplied else None cache: caching.Cache = settings.CACHE_CLASS.setup( @@ -122,7 +123,7 @@ def _scan_packages( for module_file in found_package.module_files } - imports_by_module_file: Dict[ModuleFile, Set[DirectImport]] = {} + imports_by_module_file: dict[ModuleFile, set[DirectImport]] = {} if cache_dir is not None: imports_by_module_file.update(_read_imports_from_cache(module_files_to_scan, cache=cache)) @@ -138,7 +139,7 @@ def _scan_packages( ) ) - imports_by_module: Dict[Module, Set[DirectImport]] = { + imports_by_module: dict[Module, set[DirectImport]] = { k.module: v for k, v in imports_by_module_file.items() } @@ -149,8 +150,8 @@ def _scan_packages( def _assemble_graph( - found_packages: Set[FoundPackage], - imports_by_module: Dict[Module, Set[DirectImport]], + found_packages: set[FoundPackage], + imports_by_module: dict[Module, set[DirectImport]], ) -> ImportGraph: graph: ImportGraph = settings.IMPORT_GRAPH_CLASS() @@ -175,7 +176,7 @@ def _assemble_graph( return graph -def _is_external(module: Module, package_modules: Set[Module]) -> bool: +def _is_external(module: Module, package_modules: set[Module]) -> bool: return not any( module.is_descendant_of(package_module) or module == package_module for package_module in package_modules @@ -184,8 +185,8 @@ def _is_external(module: Module, package_modules: Set[Module]) -> bool: def _read_imports_from_cache( module_files: Iterable[ModuleFile], *, cache: caching.Cache -) -> Dict[ModuleFile, Set[DirectImport]]: - imports_by_module_file: Dict[ModuleFile, Set[DirectImport]] = {} +) -> dict[ModuleFile, set[DirectImport]]: + imports_by_module_file: dict[ModuleFile, set[DirectImport]] = {} for module_file in module_files: try: direct_imports = cache.read_imports(module_file) diff --git a/src/grimp/domain/analysis.py b/src/grimp/domain/analysis.py index 112606e4..3c529c38 100644 --- a/src/grimp/domain/analysis.py +++ b/src/grimp/domain/analysis.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Iterable, Sequence +from collections.abc import Iterable, Sequence @dataclass(frozen=True) diff --git a/src/grimp/domain/valueobjects.py b/src/grimp/domain/valueobjects.py index 35ef338a..ec2e64e9 100644 --- a/src/grimp/domain/valueobjects.py +++ b/src/grimp/domain/valueobjects.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from typing import Set @dataclass(frozen=True) @@ -67,7 +66,7 @@ class Layer: independent. This is the default. """ - module_tails: Set[str] + module_tails: set[str] independent: bool closed: bool diff --git a/src/grimp/exceptions.py b/src/grimp/exceptions.py index a6baa169..43ec9823 100644 --- a/src/grimp/exceptions.py +++ b/src/grimp/exceptions.py @@ -1,6 +1,3 @@ -from typing import Optional - - class GrimpException(Exception): """ Base exception for all custom Grimp exceptions to inherit. @@ -39,7 +36,7 @@ class SourceSyntaxError(GrimpException): Indicates a syntax error in code that was being statically analysed. """ - def __init__(self, filename: str, lineno: Optional[int], text: Optional[str]) -> None: + def __init__(self, filename: str, lineno: int | None, text: str | None) -> None: """ Args: filename: The file which contained the error. diff --git a/tests/adaptors/filesystem.py b/tests/adaptors/filesystem.py index b8f4f3a1..d8ed455d 100644 --- a/tests/adaptors/filesystem.py +++ b/tests/adaptors/filesystem.py @@ -1,4 +1,5 @@ -from typing import Any, Dict, Generator, List, Optional, Tuple +from typing import Any +from collections.abc import Generator import yaml @@ -11,9 +12,9 @@ class FakeFileSystem(AbstractFileSystem): def __init__( self, - contents: Optional[str] = None, - content_map: Optional[Dict[str, str]] = None, - mtime_map: Optional[Dict[str, float]] = None, + contents: str | None = None, + content_map: dict[str, str] | None = None, + mtime_map: dict[str, float] | None = None, ) -> None: """ Files can be declared as existing in the file system in two different ways, either @@ -46,7 +47,7 @@ def __init__( self.contents = self._parse_contents(contents) self._raw_contents = contents self.content_map = content_map if content_map else {} - self.mtime_map: Dict[str, float] = mtime_map if mtime_map else {} + self.mtime_map: dict[str, float] = mtime_map if mtime_map else {} @property def sep(self) -> str: @@ -75,8 +76,8 @@ def walk(self, directory_name): yield from self._walk_contents(directory_contents, containing_directory=directory_name) def _walk_contents( - self, directory_contents: Dict[str, Any], containing_directory: str - ) -> Generator[Tuple[str, List[str], List[str]], None, None]: + self, directory_contents: dict[str, Any], containing_directory: str + ) -> Generator[tuple[str, list[str], list[str]], None, None]: directories = [] files = [] for key, value in directory_contents.items(): @@ -97,7 +98,7 @@ def _walk_contents( def join(self, *components: str) -> str: return self.sep.join(c.rstrip(self.sep) for c in components) - def split(self, file_name: str) -> Tuple[str, str]: + def split(self, file_name: str) -> tuple[str, str]: components = file_name.split(self.sep) if len(components) == 2: # Handle case where file is child of the root, i.e. /some-file.txt. @@ -106,7 +107,7 @@ def split(self, file_name: str) -> Tuple[str, str]: components.insert(0, "") return (self.sep.join(components[:-1]), components[-1]) - def _parse_contents(self, raw_contents: Optional[str]): + def _parse_contents(self, raw_contents: str | None): """ Returns the raw contents parsed in the form: { @@ -142,7 +143,7 @@ def _parse_contents(self, raw_contents: Optional[str]): return yaml.safe_load(yamlified_string) - def _dedent(self, lines: List[str]) -> List[str]: + def _dedent(self, lines: list[str]) -> list[str]: """ Dedent all lines by the same amount. """ diff --git a/tests/adaptors/modulefinder.py b/tests/adaptors/modulefinder.py index 2a29cff0..423fb3db 100644 --- a/tests/adaptors/modulefinder.py +++ b/tests/adaptors/modulefinder.py @@ -1,10 +1,9 @@ from grimp.application.ports.modulefinder import AbstractModuleFinder, FoundPackage, ModuleFile from grimp.application.ports.filesystem import AbstractFileSystem -from typing import FrozenSet, Dict class BaseFakeModuleFinder(AbstractModuleFinder): - module_files_by_package_name: Dict[str, FrozenSet[ModuleFile]] = {} + module_files_by_package_name: dict[str, frozenset[ModuleFile]] = {} def find_package( self, package_name: str, package_directory: str, file_system: AbstractFileSystem diff --git a/tests/adaptors/packagefinder.py b/tests/adaptors/packagefinder.py index e06418b8..b242f26d 100644 --- a/tests/adaptors/packagefinder.py +++ b/tests/adaptors/packagefinder.py @@ -1,11 +1,9 @@ -from typing import Dict - from grimp.application.ports.packagefinder import AbstractPackageFinder from grimp.application.ports.filesystem import AbstractFileSystem class BaseFakePackageFinder(AbstractPackageFinder): - directory_map: Dict[str, str] = {} + directory_map: dict[str, str] = {} def determine_package_directory( self, package_name: str, file_system: AbstractFileSystem diff --git a/tests/functional/test_build_and_use_graph.py b/tests/functional/test_build_and_use_graph.py index 35261567..ed151598 100644 --- a/tests/functional/test_build_and_use_graph.py +++ b/tests/functional/test_build_and_use_graph.py @@ -1,5 +1,4 @@ from grimp import build_graph -from typing import Set, Tuple, Optional import pytest @@ -247,7 +246,7 @@ def test_find_shortest_chain(): ), ], ) -def test_find_shortest_chains(as_packages: Optional[bool], expected_result: Set[Tuple]): +def test_find_shortest_chains(as_packages: bool | None, expected_result: set[tuple]): importer = "testpackage.three" imported = "testpackage.one.alpha" diff --git a/tests/functional/test_caching.py b/tests/functional/test_caching.py index 6a7dbc3f..1916abf9 100644 --- a/tests/functional/test_caching.py +++ b/tests/functional/test_caching.py @@ -107,7 +107,7 @@ def test_build_graph_uses_cache(copied_cachingpackage): def _manipulate_data_file(data_file: Path, snippet: str, replacement: str) -> None: - with open(data_file, "r") as file: + with open(data_file) as file: filedata = file.read() filedata = filedata.replace(snippet, replacement) diff --git a/tests/unit/adaptors/test_caching.py b/tests/unit/adaptors/test_caching.py index 749f14b3..77673520 100644 --- a/tests/unit/adaptors/test_caching.py +++ b/tests/unit/adaptors/test_caching.py @@ -1,6 +1,5 @@ import json import logging -from typing import Set import pytest # type: ignore @@ -21,7 +20,7 @@ class SimplisticFileNamer(CacheFileNamer): @classmethod def make_data_file_name( cls, - found_packages: Set[FoundPackage], + found_packages: set[FoundPackage], include_external_packages: bool, exclude_type_checking_imports: bool, ) -> str: diff --git a/tests/unit/adaptors/test_filesystem.py b/tests/unit/adaptors/test_filesystem.py index 68d86f45..6644b7bc 100644 --- a/tests/unit/adaptors/test_filesystem.py +++ b/tests/unit/adaptors/test_filesystem.py @@ -1,5 +1,4 @@ from copy import copy -from typing import Type import pytest # type: ignore from grimp.application.ports.filesystem import BasicFileSystem from tests.adaptors.filesystem import FakeFileSystem @@ -11,7 +10,7 @@ class _Base: Tests for methods that AbstractFileSystem and BasicFileSystem share. """ - file_system_cls: Type[BasicFileSystem] + file_system_cls: type[BasicFileSystem] @pytest.mark.parametrize("path", ("/path/to", "/path/to/")) def test_join(self, path): diff --git a/tests/unit/application/graph/test_chains.py b/tests/unit/application/graph/test_chains.py index 02f44a66..3cf207ae 100644 --- a/tests/unit/application/graph/test_chains.py +++ b/tests/unit/application/graph/test_chains.py @@ -1,5 +1,3 @@ -from typing import Set, Tuple - import pytest # type: ignore from grimp.application.graph import ImportGraph @@ -175,7 +173,7 @@ def test_demonstrate_nondeterminism_of_equal_chains(self): (True, ("green.foo", "blue.bar")), ), ) - def test_as_packages(self, as_packages: bool, expected_result: Set[Tuple]): + def test_as_packages(self, as_packages: bool, expected_result: set[tuple]): graph = ImportGraph() graph.add_module("green") graph.add_module("blue") @@ -245,7 +243,7 @@ def test_top_level_import(self, as_packages: bool): (True, {("green.foo", "blue.bar")}), ), ) - def test_first_level_child_import(self, as_packages: bool, expected_result: Set[Tuple]): + def test_first_level_child_import(self, as_packages: bool, expected_result: set[tuple]): graph = ImportGraph() graph.add_module("green") graph.add_module("blue") @@ -277,7 +275,7 @@ def test_no_results_in_reverse_direction(self, as_packages: bool): (True, {("green.foo.one", "blue.bar.two")}), ), ) - def test_grandchildren_import(self, as_packages: bool, expected_result: Set[Tuple]): + def test_grandchildren_import(self, as_packages: bool, expected_result: set[tuple]): graph = ImportGraph() graph.add_module("green") graph.add_module("blue") @@ -297,7 +295,7 @@ def test_grandchildren_import(self, as_packages: bool, expected_result: Set[Tupl ), ) def test_import_between_child_and_top_level( - self, as_packages: bool, expected_result: Set[Tuple] + self, as_packages: bool, expected_result: set[tuple] ): graph = ImportGraph() graph.add_module("green") @@ -317,7 +315,7 @@ def test_import_between_child_and_top_level( ), ) def test_import_between_top_level_and_child( - self, as_packages: bool, expected_result: Set[Tuple] + self, as_packages: bool, expected_result: set[tuple] ): graph = ImportGraph() graph.add_module("blue") @@ -342,7 +340,7 @@ def test_import_between_top_level_and_child( ), ), ) - def test_short_indirect_import(self, as_packages: bool, expected_result: Set[Tuple]): + def test_short_indirect_import(self, as_packages: bool, expected_result: set[tuple]): graph = ImportGraph() graph.add_module("green") graph.add_module("blue") @@ -375,7 +373,7 @@ def test_short_indirect_import(self, as_packages: bool, expected_result: Set[Tup ), ), ) - def test_long_indirect_import(self, as_packages: bool, expected_result: Set[Tuple]): + def test_long_indirect_import(self, as_packages: bool, expected_result: set[tuple]): graph = ImportGraph() graph.add_module("green") graph.add_module("blue") diff --git a/tests/unit/application/test_scanning.py b/tests/unit/application/test_scanning.py index b4a457d3..e7cbfa54 100644 --- a/tests/unit/application/test_scanning.py +++ b/tests/unit/application/test_scanning.py @@ -1,5 +1,3 @@ -from typing import Set - import pytest # type: ignore from grimp.application.ports.modulefinder import FoundPackage, ModuleFile @@ -945,5 +943,5 @@ def _module_to_module_file(module: Module) -> ModuleFile: return ModuleFile(module=module, mtime=some_mtime) -def _modules_to_module_files(modules: Set[Module]) -> Set[ModuleFile]: +def _modules_to_module_files(modules: set[Module]) -> set[ModuleFile]: return {_module_to_module_file(module) for module in modules} diff --git a/tests/unit/application/test_usecases.py b/tests/unit/application/test_usecases.py index c38bfaab..5f7adbe8 100644 --- a/tests/unit/application/test_usecases.py +++ b/tests/unit/application/test_usecases.py @@ -1,4 +1,3 @@ -from typing import Dict, Optional, Set from unittest.mock import sentinel import pytest # type: ignore @@ -92,7 +91,7 @@ class FakePackageFinder(BaseFakePackageFinder): class AssertingCache(Cache): @classmethod - def cache_dir_or_default(cls, cache_dir: Optional[str]) -> str: + def cache_dir_or_default(cls, cache_dir: str | None) -> str: return cache_dir or SOME_DEFAULT_CACHE_DIR def __init__(self, *args, **kwargs) -> None: @@ -110,12 +109,12 @@ def __init__(self, *args, **kwargs) -> None: ) assert self.cache_dir == expected_cache_dir - def read_imports(self, module_file: ModuleFile) -> Set[DirectImport]: + def read_imports(self, module_file: ModuleFile) -> set[DirectImport]: return set() def write( self, - imports_by_module: Dict[Module, Set[DirectImport]], + imports_by_module: dict[Module, set[DirectImport]], ) -> None: pass