patchy 2.10.0__tar.gz → 3.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,29 +1,30 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchy
3
- Version: 2.10.0
3
+ Version: 3.0.0
4
4
  Summary: Patch the inner source of python functions at runtime.
5
+ Keywords: patchy
6
+ Author: Adam Johnson
5
7
  Author-email: Adam Johnson <me@adamj.eu>
6
8
  License-Expression: MIT
7
- Project-URL: Changelog, https://github.com/adamchainz/patchy/blob/main/CHANGELOG.rst
8
- Project-URL: Funding, https://adamj.eu/books/
9
- Project-URL: Repository, https://github.com/adamchainz/patchy
10
- Keywords: patchy
9
+ License-File: LICENSE
11
10
  Classifier: Development Status :: 5 - Production/Stable
12
11
  Classifier: Intended Audience :: Developers
13
12
  Classifier: Natural Language :: English
14
13
  Classifier: Operating System :: OS Independent
15
14
  Classifier: Programming Language :: Python :: 3 :: Only
16
- Classifier: Programming Language :: Python :: 3.9
17
15
  Classifier: Programming Language :: Python :: 3.10
18
16
  Classifier: Programming Language :: Python :: 3.11
19
17
  Classifier: Programming Language :: Python :: 3.12
20
18
  Classifier: Programming Language :: Python :: 3.13
21
19
  Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Programming Language :: Python :: 3.15
22
21
  Classifier: Typing :: Typed
23
- Requires-Python: >=3.9
22
+ Requires-Dist: unipatch
23
+ Requires-Python: >=3.10
24
+ Project-URL: Changelog, https://github.com/adamchainz/patchy/blob/main/CHANGELOG.rst
25
+ Project-URL: Funding, https://adamj.eu/books/
26
+ Project-URL: Repository, https://github.com/adamchainz/patchy
24
27
  Description-Content-Type: text/x-rst
25
- License-File: LICENSE
26
- Dynamic: license-file
27
28
 
28
29
  ======
29
30
  Patchy
@@ -92,7 +93,7 @@ Use **pip**:
92
93
 
93
94
  python -m pip install patchy
94
95
 
95
- Python 3.9 to 3.14 supported.
96
+ Python 3.10 to 3.15 supported.
96
97
 
97
98
  Why?
98
99
  ====
@@ -107,7 +108,7 @@ I found this with some small but important patches to Django for a project.
107
108
  Since it takes a lot of energy to maintain a fork, writing monkey patches was
108
109
  the chosen quick solution, but then writing actual patches would be better.
109
110
 
110
- The patches are applied with the standard ``patch`` commandline utility.
111
+ The patches are applied in-memory by `unipatch <https://pypi.org/project/unipatch/>`__, a pure-Python implementation of unified diff patching, compatible with the behaviour of the GNU ``patch`` commandline utility.
111
112
 
112
113
 
113
114
  Why not?
@@ -115,8 +116,6 @@ Why not?
115
116
 
116
117
  There are of course a lot of reasons against:
117
118
 
118
- * It’s (relatively) slow (since it writes the source to disk and calls the
119
- ``patch`` command)
120
119
  * If you have a patch file, why not just fork the library and apply it?
121
120
  * At least with monkey-patching you know what end up with, rather than having
122
121
  the changes being done at runtime to source that may have changed.
@@ -128,16 +127,10 @@ solution.
128
127
  How?
129
128
  ====
130
129
 
131
- The standard library function ``inspect.getsource()`` is used to retrieve the
132
- source code of the function, the patch is applied with the commandline utility
133
- ``patch``, the code is recompiled, and the function’s code object is replaced
134
- the new one. Because nothing tends to poke around at code objects apart from
135
- dodgy hacks like this, you don’t need to worry about chasing any references
136
- that may exist to the function, unlike ``mock.patch``.
130
+ The standard library function ``inspect.getsource()`` is used to retrieve the source code of the function, the patch is applied in-memory with `unipatch <https://pypi.org/project/unipatch/>`__, the code is recompiled, and the function’s code object is replaced the new one.
131
+ Because nothing tends to poke around at code objects apart from dodgy hacks like this, you don’t need to worry about chasing any references that may exist to the function, unlike ``mock.patch``.
137
132
 
138
- A little special treatment is given to ``instancemethod``, ``classmethod``, and
139
- ``staticmethod`` objects to make sure the underlying function is what gets
140
- patched and that you don't have to worry about the details.
133
+ A little special treatment is given to ``instancemethod``, ``classmethod``, and ``staticmethod`` objects to make sure the underlying function is what gets patched and that you don't have to worry about the details.
141
134
 
142
135
 
143
136
  API
@@ -65,7 +65,7 @@ Use **pip**:
65
65
 
66
66
  python -m pip install patchy
67
67
 
68
- Python 3.9 to 3.14 supported.
68
+ Python 3.10 to 3.15 supported.
69
69
 
70
70
  Why?
71
71
  ====
@@ -80,7 +80,7 @@ I found this with some small but important patches to Django for a project.
80
80
  Since it takes a lot of energy to maintain a fork, writing monkey patches was
81
81
  the chosen quick solution, but then writing actual patches would be better.
82
82
 
83
- The patches are applied with the standard ``patch`` commandline utility.
83
+ The patches are applied in-memory by `unipatch <https://pypi.org/project/unipatch/>`__, a pure-Python implementation of unified diff patching, compatible with the behaviour of the GNU ``patch`` commandline utility.
84
84
 
85
85
 
86
86
  Why not?
@@ -88,8 +88,6 @@ Why not?
88
88
 
89
89
  There are of course a lot of reasons against:
90
90
 
91
- * It’s (relatively) slow (since it writes the source to disk and calls the
92
- ``patch`` command)
93
91
  * If you have a patch file, why not just fork the library and apply it?
94
92
  * At least with monkey-patching you know what end up with, rather than having
95
93
  the changes being done at runtime to source that may have changed.
@@ -101,16 +99,10 @@ solution.
101
99
  How?
102
100
  ====
103
101
 
104
- The standard library function ``inspect.getsource()`` is used to retrieve the
105
- source code of the function, the patch is applied with the commandline utility
106
- ``patch``, the code is recompiled, and the function’s code object is replaced
107
- the new one. Because nothing tends to poke around at code objects apart from
108
- dodgy hacks like this, you don’t need to worry about chasing any references
109
- that may exist to the function, unlike ``mock.patch``.
102
+ The standard library function ``inspect.getsource()`` is used to retrieve the source code of the function, the patch is applied in-memory with `unipatch <https://pypi.org/project/unipatch/>`__, the code is recompiled, and the function’s code object is replaced the new one.
103
+ Because nothing tends to poke around at code objects apart from dodgy hacks like this, you don’t need to worry about chasing any references that may exist to the function, unlike ``mock.patch``.
110
104
 
111
- A little special treatment is given to ``instancemethod``, ``classmethod``, and
112
- ``staticmethod`` objects to make sure the underlying function is what gets
113
- patched and that you don't have to worry about the details.
105
+ A little special treatment is given to ``instancemethod``, ``classmethod``, and ``staticmethod`` objects to make sure the underlying function is what gets patched and that you don't have to worry about the details.
114
106
 
115
107
 
116
108
  API
@@ -0,0 +1,112 @@
1
+ [build-system]
2
+ build-backend = "uv_build"
3
+ requires = ["uv-build>=0.12.1,<0.13"]
4
+
5
+ [project]
6
+ name = "patchy"
7
+ version = "3.0.0"
8
+ description = "Patch the inner source of python functions at runtime."
9
+ readme = "README.rst"
10
+ keywords = ["patchy"]
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ requires-python = ">=3.10"
14
+ classifiers = [
15
+ "Development Status :: 5 - Production/Stable",
16
+ "Intended Audience :: Developers",
17
+ "Natural Language :: English",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python :: 3.14",
25
+ "Programming Language :: Python :: 3.15",
26
+ "Typing :: Typed",
27
+ ]
28
+ dependencies = ["unipatch"]
29
+
30
+ [[project.authors]]
31
+ name = "Adam Johnson"
32
+ email = "me@adamj.eu"
33
+
34
+ [project.urls]
35
+ Changelog = "https://github.com/adamchainz/patchy/blob/main/CHANGELOG.rst"
36
+ Funding = "https://adamj.eu/books/"
37
+ Repository = "https://github.com/adamchainz/patchy"
38
+
39
+ [dependency-groups]
40
+ test = [
41
+ "coverage[toml]",
42
+ "pkgutil-resolve-name; python_version<'3.9'",
43
+ "pytest>=9",
44
+ "pytest-randomly",
45
+ ]
46
+
47
+ [tool.ruff.lint]
48
+ select = [
49
+ "B",
50
+ "C4",
51
+ "E",
52
+ "F",
53
+ "I",
54
+ "SIM",
55
+ "TID",
56
+ "UP",
57
+ "W",
58
+ ]
59
+ ignore = [
60
+ "B9",
61
+ "E501",
62
+ "SIM105",
63
+ "SIM108",
64
+ ]
65
+ extend-safe-fixes = ["UP006"]
66
+
67
+ [tool.ruff.lint.isort]
68
+ required-imports = ["from __future__ import annotations"]
69
+
70
+ [tool.pyproject-fmt]
71
+ max_supported_python = "3.15"
72
+
73
+ [tool.mypy]
74
+ mypy_path = "src/"
75
+ namespace_packages = false
76
+ warn_unreachable = true
77
+ enable_error_code = [
78
+ "ignore-without-code",
79
+ "redundant-expr",
80
+ "truthy-bool",
81
+ ]
82
+ strict = true
83
+
84
+ [[tool.mypy.overrides]]
85
+ module = "tests.*"
86
+ allow_untyped_defs = true
87
+
88
+ [tool.pytest]
89
+ strict = true
90
+
91
+ [tool.coverage.run]
92
+ branch = true
93
+ data_file = ".coverage/cov"
94
+ parallel = true
95
+ source = [
96
+ "patchy",
97
+ "tests",
98
+ ]
99
+
100
+ [tool.coverage.paths]
101
+ source = [
102
+ "src",
103
+ ".tox/**/site-packages",
104
+ ]
105
+
106
+ [tool.coverage.report]
107
+ show_missing = true
108
+ skip_covered = true
109
+ skip_empty = true
110
+
111
+ [tool.rstcheck]
112
+ report_level = "ERROR"
@@ -1,12 +1,12 @@
1
1
  [build-system]
2
- build-backend = "setuptools.build_meta"
2
+ build-backend = "uv_build"
3
3
  requires = [
4
- "setuptools>=77",
4
+ "uv-build>=0.12.1,<0.13",
5
5
  ]
6
6
 
7
7
  [project]
8
8
  name = "patchy"
9
- version = "2.10.0"
9
+ version = "3.0.0"
10
10
  description = "Patch the inner source of python functions at runtime."
11
11
  readme = "README.rst"
12
12
  keywords = [
@@ -17,29 +17,31 @@ license-files = [ "LICENSE" ]
17
17
  authors = [
18
18
  { name = "Adam Johnson", email = "me@adamj.eu" },
19
19
  ]
20
- requires-python = ">=3.9"
20
+ requires-python = ">=3.10"
21
21
  classifiers = [
22
22
  "Development Status :: 5 - Production/Stable",
23
23
  "Intended Audience :: Developers",
24
24
  "Natural Language :: English",
25
25
  "Operating System :: OS Independent",
26
26
  "Programming Language :: Python :: 3 :: Only",
27
- "Programming Language :: Python :: 3.9",
28
27
  "Programming Language :: Python :: 3.10",
29
28
  "Programming Language :: Python :: 3.11",
30
29
  "Programming Language :: Python :: 3.12",
31
30
  "Programming Language :: Python :: 3.13",
32
31
  "Programming Language :: Python :: 3.14",
32
+ "Programming Language :: Python :: 3.15",
33
33
  "Typing :: Typed",
34
34
  ]
35
- dependencies = [ ]
35
+ dependencies = [
36
+ "unipatch",
37
+ ]
36
38
  urls = { Changelog = "https://github.com/adamchainz/patchy/blob/main/CHANGELOG.rst", Funding = "https://adamj.eu/books/", Repository = "https://github.com/adamchainz/patchy" }
37
39
 
38
40
  [dependency-groups]
39
41
  test = [
40
42
  "coverage[toml]",
41
43
  "pkgutil-resolve-name; python_version<'3.9'",
42
- "pytest",
44
+ "pytest>=9",
43
45
  "pytest-randomly",
44
46
  ]
45
47
 
@@ -81,46 +83,38 @@ lint.extend-safe-fixes = [
81
83
  lint.isort.required-imports = [ "from __future__ import annotations" ]
82
84
 
83
85
  [tool.pyproject-fmt]
84
- max_supported_python = "3.14"
85
-
86
- [tool.pytest.ini_options]
87
- addopts = """\
88
- --strict-config
89
- --strict-markers
90
- """
91
- xfail_strict = true
92
-
93
- [tool.coverage.run]
94
- branch = true
95
- parallel = true
96
- source = [
97
- "patchy",
98
- "tests",
99
- ]
100
-
101
- [tool.coverage.paths]
102
- source = [
103
- "src",
104
- ".tox/**/site-packages",
105
- ]
106
-
107
- [tool.coverage.report]
108
- show_missing = true
86
+ max_supported_python = "3.15"
109
87
 
110
88
  [tool.mypy]
89
+ mypy_path = "src/"
90
+ namespace_packages = false
91
+ warn_unreachable = true
111
92
  enable_error_code = [
112
93
  "ignore-without-code",
113
94
  "redundant-expr",
114
95
  "truthy-bool",
115
96
  ]
116
- mypy_path = "src/"
117
- namespace_packages = false
118
97
  strict = true
119
- warn_unreachable = true
98
+ overrides = [ { module = "tests.*", allow_untyped_defs = true } ]
99
+
100
+ [tool.pytest]
101
+ strict = true
120
102
 
121
- [[tool.mypy.overrides]]
122
- module = "tests.*"
123
- allow_untyped_defs = true
103
+ [tool.coverage]
104
+ run.branch = true
105
+ run.data_file = ".coverage/cov"
106
+ run.parallel = true
107
+ run.source = [
108
+ "patchy",
109
+ "tests",
110
+ ]
111
+ paths.source = [
112
+ "src",
113
+ ".tox/**/site-packages",
114
+ ]
115
+ report.show_missing = true
116
+ report.skip_covered = true
117
+ report.skip_empty = true
124
118
 
125
119
  [tool.rstcheck]
126
120
  report_level = "ERROR"
@@ -2,16 +2,15 @@ from __future__ import annotations
2
2
 
3
3
  import ast
4
4
  import inspect
5
- import os
6
- import shutil
7
- import subprocess
5
+ from collections.abc import Callable
8
6
  from functools import wraps
9
- from tempfile import mkdtemp
10
7
  from textwrap import dedent
11
8
  from types import CodeType, TracebackType
12
- from typing import Any, Callable, TypeVar, cast
9
+ from typing import Any, TypeVar, cast
13
10
  from weakref import WeakKeyDictionary
14
11
 
12
+ from unipatch import HunkApplyError, PatchError, apply_patch
13
+
15
14
  from .cache import PatchingCache
16
15
 
17
16
  if True:
@@ -111,40 +110,34 @@ def _apply_patch(
111
110
  except KeyError:
112
111
  pass
113
112
 
114
- # Write out files
115
- tempdir = mkdtemp(prefix="patchy")
116
113
  try:
117
- source_path = os.path.join(tempdir, name + ".py")
118
- with open(source_path, "w") as source_file:
119
- source_file.write(source)
120
-
121
- patch_path = os.path.join(tempdir, name + ".patch")
122
- with open(patch_path, "w") as patch_file:
123
- patch_file.write(patch_text)
124
- if not patch_text.endswith("\n"):
125
- patch_file.write("\n")
126
-
127
- # Call `patch` command
128
- command = ["patch", "--force"]
129
- if not forwards:
130
- command.append("--reverse")
131
- command.extend([source_path, patch_path])
132
- result = subprocess.run(command, capture_output=True, text=True)
133
-
134
- if result.returncode != 0:
135
- msg = "Could not {action} the patch {prep} '{name}'.".format(
136
- action=("apply" if forwards else "unapply"),
137
- prep=("to" if forwards else "from"),
138
- name=name,
139
- )
140
- msg += f" The message from `patch` was:\n{result.stdout}\n{result.stderr}"
141
- msg += f"\nThe code to patch was:\n{source}\nThe patch was:\n{patch_text}"
142
- raise ValueError(msg)
143
-
144
- with open(source_path) as source_file:
145
- new_source = source_file.read()
146
- finally:
147
- shutil.rmtree(tempdir)
114
+ new_source = apply_patch(source, patch_text, forwards=forwards)
115
+ except PatchError as exc:
116
+ detail = str(exc)
117
+ if isinstance(exc, HunkApplyError):
118
+ try:
119
+ apply_patch(source, patch_text, forwards=not forwards)
120
+ except PatchError:
121
+ pass
122
+ else:
123
+ if forwards:
124
+ detail += (
125
+ " Applying it in reverse succeeds, so it looks like"
126
+ + " the patch has already been applied."
127
+ )
128
+ else:
129
+ detail += (
130
+ " Applying it forwards succeeds, so it looks like the"
131
+ + " patch is unreversed and has not been applied yet."
132
+ )
133
+ msg = "Could not {action} the patch {prep} '{name}'. {detail}".format(
134
+ action=("apply" if forwards else "unapply"),
135
+ prep=("to" if forwards else "from"),
136
+ name=name,
137
+ detail=detail,
138
+ )
139
+ msg += f"\n\nThe code to patch was:\n{source}\nThe patch was:\n{patch_text}"
140
+ raise ValueError(msg) from None
148
141
 
149
142
  _patching_cache.store(source, patch_text, forwards, new_source)
150
143
 
@@ -1,136 +0,0 @@
1
- =========
2
- Changelog
3
- =========
4
-
5
- 2.10.0 (2025-09-09)
6
- -------------------
7
-
8
- * Support Python 3.14.
9
-
10
- 2.9.0 (2024-10-25)
11
- ------------------
12
-
13
- * Drop Python 3.8 support.
14
-
15
- * Support Python 3.13.
16
-
17
- 2.8.0 (2023-07-10)
18
- ------------------
19
-
20
- * Drop Python 3.7 support.
21
-
22
- 2.7.0 (2023-06-16)
23
- ------------------
24
-
25
- * Support Python 3.12.
26
-
27
- 2.6.1 (2023-01-16)
28
- ------------------
29
-
30
- * Use the ``--force`` flag to ``patch``, so it doesn’t hang waiting input for invalid reverse patches.
31
- This seems to happen with the new version on macOS.
32
-
33
- 2.6.0 (2022-05-11)
34
- ------------------
35
-
36
- * Support Python 3.11.
37
-
38
- 2.5.0 (2022-01-10)
39
- ------------------
40
-
41
- * Drop Python 3.6 support.
42
-
43
- 2.4.0 (2021-08-11)
44
- ------------------
45
-
46
- * Add type hints.
47
-
48
- 2.3.0 (2021-05-10)
49
- ------------------
50
-
51
- * Support Python 3.10.
52
-
53
- * Stop distributing tests to reduce package size. Tests are not intended to be
54
- run outside of the tox setup in the repository. Repackagers can use GitHub's
55
- tarballs per tag.
56
-
57
- 2.2.0 (2020-12-13)
58
- ------------------
59
-
60
- * Drop Python 3.5 support.
61
- * Move license from BSD to MIT License.
62
-
63
- 2.1.0 (2020-02-20)
64
- ------------------
65
-
66
- * Support Python 3.9.
67
- * Use Python 3.9's ``pkgutil.resolve_name()``
68
- (`docs <https://docs.python.org/3.9/library/pkgutil.html#pkgutil.resolve_name>`__)
69
- to import names by strings, and depend on the
70
- `backport package <https://pypi.org/project/pkgutil_resolve_name/>`__ on
71
- older Python versions.
72
-
73
- 2.0.0 (2019-11-15)
74
- ------------------
75
-
76
- * Drop Python 2 support, only Python 3.5-3.8 is supported now. Python 3.4 was
77
- dropped as it has reached its end of life.
78
- * Converted setuptools metadata to configuration file. This meant removing the
79
- ``__version__`` attribute from the package. If you want to inspect the
80
- installed version, use
81
- ``importlib.metadata.version("patchy")``
82
- (`docs <https://docs.python.org/3.8/library/importlib.metadata.html#distribution-versions>`__ /
83
- `backport <https://pypi.org/project/importlib-metadata/>`__).
84
-
85
- 1.5.0 (2019-02-15)
86
- ------------------
87
-
88
- * Support cases where the function or class name is not in the freevars. This
89
- allows patching ``__init__`` of a class, or patching a recursive module-level
90
- function.
91
- * Support patching objects provided by dotted path string.
92
-
93
- 1.4.0 (2017-05-13)
94
- ------------------
95
-
96
- * Added new function ``patchy.replace()``, that can be used to directly assign
97
- new source code to a function, without having to make a patch.
98
-
99
- 1.3.2 (2017-03-13)
100
- ------------------
101
-
102
- * Support freevars and methods that call mangled methods.
103
-
104
- 1.3.1 (2016-05-24)
105
- ------------------
106
-
107
- * Fixed ``setup.py`` to not fix the version of ``six`` required.
108
- * Fixed install instruction in README.
109
- * Added ``patchy.mc_patchface()`` as an alias for ``patchy.patch()`` because
110
- memes.
111
-
112
- 1.3.0 (2015-12-09)
113
- ------------------
114
-
115
- * Remove dependency on ``pylru`` by using a simpler caching strategy
116
-
117
- 1.2.0 (2015-07-23)
118
- ------------------
119
-
120
- * Pirate mascot!
121
- * Patching caches the patched and unpatched versions, so unpatching and repeat
122
- patching/unpatching are both faster
123
- * Patching doesn't attach an attribute to the function object any more
124
-
125
- 1.1.0 (2015-06-16)
126
- ------------------
127
-
128
- * Fixed code compilation to use the ``__future__`` flags from the function that
129
- is being patched
130
- * Added ``unpatch`` method
131
- * Added ``temp_patch`` context manager/decorator
132
-
133
- 1.0.0 (2015-06-09)
134
- ---------------------
135
-
136
- * First release on PyPI, featuring ``patch`` function.
patchy-2.10.0/MANIFEST.in DELETED
@@ -1,6 +0,0 @@
1
- include CHANGELOG.rst
2
- include LICENSE
3
- include pirate.png
4
- include pyproject.toml
5
- include README.rst
6
- include src/*/py.typed