From 6898b0c220ccffbdf528ff1af1e0cf705006d4a2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 17:55:18 +0000 Subject: [PATCH] Modernize for a Python 3.13 minimum Raise the supported Python floor from 3.9 to 3.13 and update the code and docs to match. - pyproject.toml: requires-python >=3.13 and ty environment python-version 3.13; enable ruff's pyupgrade (UP) rules to keep syntax modern. - CI: drop the 3.9-3.12 test matrix entries, keeping 3.13 and 3.14. - types.py: use PEP 695 `type` aliases and PEP 604 `|` unions instead of typing.Union. - pointer.py / produce.py: import Hashable, Iterable and Callable from collections.abc instead of the deprecated typing aliases; drop now- redundant string-quoted forward references. - README and docs: state Python 3.13 as the minimum. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01SmQNvz1tx2xncasEyUoVdR --- .github/workflows/ci.yml | 8 -------- README.md | 2 +- docs/getting-started/installation.md | 2 +- docs/index.md | 2 +- patchdiff/pointer.py | 7 ++++--- patchdiff/produce.py | 9 +++++---- patchdiff/types.py | 6 +++--- pyproject.toml | 5 +++-- 8 files changed, 18 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4abb8f4..3bbfa3c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,14 +54,6 @@ jobs: fail-fast: false matrix: include: - - name: Linux py39 - pyversion: "3.9" - - name: Linux py310 - pyversion: "3.10" - - name: Linux py311 - pyversion: "3.11" - - name: Linux py312 - pyversion: "3.12" - name: Linux py313 pyversion: "3.13" - name: Linux py314 diff --git a/README.md b/README.md index 7bccc90..c2a14a3 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ With `in_place=True`, mutations (and patches applied with `iapply`) write straig pip install patchdiff # or: uv add patchdiff ``` -No dependencies, Python >= 3.9. +No dependencies, Python >= 3.13. ## Learn more diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index ff7921d..f254f36 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -12,6 +12,6 @@ Or with [uv](https://docs.astral.sh/uv/): uv add patchdiff ``` -There are no dependencies. Python 3.9 or newer is required. +There are no dependencies. Python 3.13 or newer is required. Next up: the [quick start](quick-start.md). diff --git a/docs/index.md b/docs/index.md index 468b431..d9ffab9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ # Patchdiff 🔍 -Based on [rfc6902](https://github.com/chbrown/rfc6902) this library provides a simple API to generate **bi-directional** diffs between composite Python data structures composed out of lists, sets, tuples and dicts. The diffs are JSON-patch compliant, and can optionally be serialized to JSON format. Patchdiff has no dependencies and works on Python 3.9 and up. +Based on [rfc6902](https://github.com/chbrown/rfc6902) this library provides a simple API to generate **bi-directional** diffs between composite Python data structures composed out of lists, sets, tuples and dicts. The diffs are JSON-patch compliant, and can optionally be serialized to JSON format. Patchdiff has no dependencies and works on Python 3.13 and up. A single call to [`diff`][patchdiff.diff.diff] gives you the patches in **both directions**: diff --git a/patchdiff/pointer.py b/patchdiff/pointer.py index 5570f28..d8da8d9 100644 --- a/patchdiff/pointer.py +++ b/patchdiff/pointer.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import Any, Hashable, Iterable, cast +from collections.abc import Hashable, Iterable +from typing import Any, cast from .types import Diffable @@ -35,7 +36,7 @@ def __init__(self, tokens: Iterable[Hashable] | None = None) -> None: self.tokens = tuple(tokens) @staticmethod - def from_str(path: str) -> "Pointer": + def from_str(path: str) -> Pointer: """Parse an escaped JSON pointer string (e.g. `"/a/0/b"`) into a Pointer. All tokens are parsed as strings; numeric list indices become @@ -104,6 +105,6 @@ def evaluate(self, obj: Diffable) -> tuple[Diffable | None, Hashable, Any]: cursor = None return parent, key, cursor - def append(self, token: Hashable) -> "Pointer": + def append(self, token: Hashable) -> Pointer: """Return a new Pointer with `token` appended; self is unchanged.""" return Pointer((*self.tokens, token)) diff --git a/patchdiff/produce.py b/patchdiff/produce.py index 0b65802..358b64e 100644 --- a/patchdiff/produce.py +++ b/patchdiff/produce.py @@ -13,8 +13,9 @@ from __future__ import annotations +from collections.abc import Callable, Hashable from copy import deepcopy -from typing import Any, Callable, Hashable +from typing import Any from weakref import ref from .pointer import Pointer @@ -125,7 +126,7 @@ def __init__(self): # their memoized _location() results. self.epoch: int = 0 - def finalize(self, root: "_Proxy") -> None: + def finalize(self, root: _Proxy) -> None: """Put the reverse patches in reverse application order and detach the root proxy. @@ -236,7 +237,7 @@ def __init__( self, data: Any, recorder: PatchRecorder, - parent: "_Proxy" | None = None, + parent: _Proxy | None = None, key: Hashable = None, ): self._data = data @@ -340,7 +341,7 @@ def _detach_all(self) -> None: self._proxies.clear() self._recorder.epoch += 1 - def _in_parent_chain(self, proxy: "_Proxy") -> bool: + def _in_parent_chain(self, proxy: _Proxy) -> bool: """True when proxy is self or an ancestor of self.""" node = self while node is not None: diff --git a/patchdiff/types.py b/patchdiff/types.py index 71d3038..0217361 100644 --- a/patchdiff/types.py +++ b/patchdiff/types.py @@ -1,13 +1,13 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Literal, TypedDict, Union +from typing import TYPE_CHECKING, Any, Literal, TypedDict if TYPE_CHECKING: from .pointer import Pointer # The structures patchdiff diffs and patches. Consumers may also pass # duck-typed container look-alikes (e.g. observ reactive proxies). -Diffable = Union[dict, list, set] +type Diffable = dict | list | set class AddOperation(TypedDict): @@ -33,4 +33,4 @@ class ReplaceOperation(TypedDict): value: Any -Operation = Union[AddOperation, RemoveOperation, ReplaceOperation] +type Operation = AddOperation | RemoveOperation | ReplaceOperation diff --git a/pyproject.toml b/pyproject.toml index 7db700c..05fc50f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ authors = [ { name = "Korijn van Golen", email = "korijn@gmail.com" }, { name = "Berend Klein Haneveld", email = "berendkleinhaneveld@gmail.com" }, ] -requires-python = ">=3.9" +requires-python = ">=3.13" readme = "README.md" license = "MIT" classifiers = ["Typing :: Typed"] @@ -42,6 +42,7 @@ extend-select = [ "N", # pep8-naming "T10", # flake8-debugger "T20", # flake8-print + "UP", # pyupgrade (keep syntax modern for the supported Python floor) "RUF", # ruff ] @@ -56,7 +57,7 @@ include = ["patchdiff"] [tool.ty.environment] # Match the oldest supported Python version (requires-python), so # that the type checker catches use of newer typing features -python-version = "3.9" +python-version = "3.13" [build-system] requires = ["hatchling"]