patchy 3.0.0__tar.gz → 3.1.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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchy
3
- Version: 3.0.0
3
+ Version: 3.1.0
4
4
  Summary: Patch the inner source of python functions at runtime.
5
5
  Keywords: patchy
6
6
  Author: Adam Johnson
@@ -4,7 +4,7 @@ requires = ["uv-build>=0.12.1,<0.13"]
4
4
 
5
5
  [project]
6
6
  name = "patchy"
7
- version = "3.0.0"
7
+ version = "3.1.0"
8
8
  description = "Patch the inner source of python functions at runtime."
9
9
  readme = "README.rst"
10
10
  keywords = ["patchy"]
@@ -6,7 +6,7 @@ requires = [
6
6
 
7
7
  [project]
8
8
  name = "patchy"
9
- version = "3.0.0"
9
+ version = "3.1.0"
10
10
  description = "Patch the inner source of python functions at runtime."
11
11
  readme = "README.rst"
12
12
  keywords = [
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import ast
4
4
  import inspect
5
+ import linecache
5
6
  from collections.abc import Callable
6
7
  from functools import wraps
7
8
  from textwrap import dedent
@@ -72,7 +73,7 @@ class temp_patch:
72
73
  @wraps(decorable)
73
74
  def wrapper(*args: Any, **kwargs: Any) -> Any:
74
75
  with self:
75
- decorable(*args, **kwargs)
76
+ return decorable(*args, **kwargs)
76
77
 
77
78
  return cast(AnyFunc, wrapper)
78
79
 
@@ -185,6 +186,10 @@ def _set_source(func: Callable[..., Any], func_source: str) -> None:
185
186
  real_func = _get_real_func(func)
186
187
  # Figure out any future headers that may be required
187
188
  feature_flags = real_func.__code__.co_flags & FEATURE_MASK
189
+ # Compile with a per-function virtual filename, under which the patched
190
+ # source is stored in linecache below. Line numbers relative to the
191
+ # patched source then match, so tools show the right lines.
192
+ filename = f"<patchy: {real_func.__module__}.{real_func.__qualname__}>"
188
193
 
189
194
  class_name = _class_name(func)
190
195
 
@@ -193,7 +198,7 @@ def _set_source(func: Callable[..., Any], func_source: str) -> None:
193
198
  flags: int = 0,
194
199
  ) -> CodeType | ast.Module:
195
200
  result: CodeType | ast.Module = compile(
196
- code, "<patchy>", "exec", flags=feature_flags | flags, dont_inherit=True
201
+ code, filename, "exec", flags=feature_flags | flags, dont_inherit=True
197
202
  )
198
203
  return result
199
204
 
@@ -233,9 +238,36 @@ def _set_source(func: Callable[..., Any], func_source: str) -> None:
233
238
  else:
234
239
  fv_force_use = []
235
240
  _ast = _parse(func_source).body[0]
236
- _ast.body = _ast.body + fv_force_use # type: ignore [attr-defined]
241
+ assert isinstance(_ast, (ast.FunctionDef, ast.AsyncFunctionDef))
242
+ _replace_defaults(_ast)
243
+ _ast.body = _ast.body + fv_force_use
237
244
  return _def, _ast, fv_body
238
245
 
246
+ def _replace_defaults(
247
+ func_ast: ast.FunctionDef | ast.AsyncFunctionDef,
248
+ ) -> None:
249
+ """
250
+ Replace default value expressions with None. Only the code object of
251
+ the new function is transplanted onto the original function, so the
252
+ recompiled defaults are discarded and the original ones remain in use.
253
+ The expressions may not even evaluate at patch time, since they can
254
+ refer to names that were only available when the function was
255
+ originally defined, such as class attributes or enclosing function
256
+ locals.
257
+ """
258
+ args = func_ast.args
259
+ args.defaults = [
260
+ ast.copy_location(ast.Constant(None), default) for default in args.defaults
261
+ ]
262
+ args.kw_defaults = [
263
+ (
264
+ None
265
+ if default is None
266
+ else ast.copy_location(ast.Constant(None), default)
267
+ )
268
+ for default in args.kw_defaults
269
+ ]
270
+
239
271
  def _process_method() -> ast.Module:
240
272
  """
241
273
  Wrap the new method in a class to ensure the same mangling as would
@@ -294,6 +326,16 @@ def _set_source(func: Callable[..., Any], func_source: str) -> None:
294
326
  # Store the modified source. This used to be attached to the function but
295
327
  # that is a bit naughty
296
328
  _source_map[real_func] = func_source
329
+ # Store the modified source in linecache, under the virtual filename, so
330
+ # tools that read source through it, like tracebacks, pdb, and
331
+ # inspect.getsource(), show it. Done the same way as doctest and timeit.
332
+ # The None mtime means linecache.checkcache() never drops the entry.
333
+ linecache.cache[filename] = (
334
+ len(func_source),
335
+ None,
336
+ func_source.splitlines(keepends=True),
337
+ filename,
338
+ )
297
339
 
298
340
 
299
341
  def _get_real_func(func: Callable[..., Any]) -> Callable[..., Any]:
File without changes
File without changes
File without changes
File without changes
File without changes