Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
4 changes: 0 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ jobs:
- uses: actions/setup-python@v6
with:
python-version: |
3.9
3.10
3.11
3.12
Expand Down Expand Up @@ -75,7 +74,6 @@ jobs:
- uses: actions/setup-python@v6
with:
python-version: |
3.9
3.10
3.11
3.12
Expand Down Expand Up @@ -109,7 +107,6 @@ jobs:
- uses: actions/setup-python@v6
with:
python-version: |
3.9
3.10
3.11
3.12
Expand Down Expand Up @@ -143,7 +140,6 @@ jobs:
- uses: actions/setup-python@v6
with:
python-version: |
3.9
3.10
3.11
3.12
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
Changelog
=========

latest
------

* Drop support for Python 3.9.

3.13 (2025-10-29)
-----------------

Expand Down
1 change: 0 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# Configuration file for the Sphinx documentation builder.
#
Expand Down
12 changes: 12 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -151,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:
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
Expand All @@ -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",
Expand Down Expand Up @@ -61,6 +60,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",
Expand Down
34 changes: 17 additions & 17 deletions src/grimp/adaptors/caching.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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,
Expand All @@ -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:
Expand All @@ -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.
Expand Down Expand Up @@ -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)
)
Expand All @@ -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(
Expand Down
6 changes: 3 additions & 3 deletions src/grimp/adaptors/filesystem.py
Original file line number Diff line number Diff line change
@@ -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]
Expand All @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions src/grimp/adaptors/modulefinder.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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(
Expand Down
6 changes: 2 additions & 4 deletions src/grimp/adaptors/packagefinder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading