Skip to content

Commit 95d5d01

Browse files
Merge branch 'release/2.3.0'
2 parents 0ae09fd + de09467 commit 95d5d01

23 files changed

Lines changed: 866 additions & 87 deletions

AGENTS.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Guidance for AI agents working on wrapt
2+
3+
## Testing
4+
5+
Read `TESTING.md` before doing any test-related work. It documents the test
6+
directory layout, the version-specific test file naming convention, the mypy
7+
pair tests, and the environment variables controlling use of C extensions.
8+
9+
Always prefer the `Justfile` recipes over invoking pytest or mypy in ad hoc
10+
virtual environments. The recipes handle building the C extension, selecting
11+
compatible tool versions, and cleaning stale artifacts.
12+
13+
- Quick iteration while developing: run pytest against a specific test file
14+
in a development virtual environment, remembering to exercise both the C
15+
extension and pure Python implementations (`WRAPT_DISABLE_EXTENSIONS=true`).
16+
- Verifying a single Python version: `just test-version 3.13`. This runs the
17+
full test suite in all three C extension variants (pure Python install,
18+
C extension enabled, C extension disabled at runtime).
19+
- Definition of done for changes to `src/wrapt/` or `src/wrapt/_wrappers.c`:
20+
`just test`, which runs the full matrix across all supported Python
21+
versions. This takes a while; if it is impractical to run, say so
22+
explicitly in your report rather than silently skipping it.
23+
- Type checking: `just test-mypy` (all versions) or
24+
`just test-mypy-version 3.13`.
25+
- Changes to the stubs in `src/wrapt-stubs/` must be checked with
26+
`just test-stubtest` against `tests/stubtest_allowlist.txt`.
27+
28+
The mypy pair tests under `tests/mypy/` compare mypy output against checked
29+
in `.out` files and only pass with the mypy version pinned by `mypy_version`
30+
in the `Justfile` (older pin for Python 3.9). Running them with any other
31+
mypy version produces false failures, so do not diagnose mismatches there as
32+
pre-existing breakage before checking the mypy version in use.

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@AGENTS.md

Justfile

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,11 +153,17 @@ test-mypy-version version:
153153
uv run --with 'mypy=={{ if version == "3.9" { "1.19.1" } else { mypy_version } }}' mypy --python-version {{version}} src/wrapt
154154

155155
# Run stubtest to compare the wrapt-stubs package against the installed
156-
# wrapt runtime. Intentional stub-vs-runtime divergences are listed in
156+
# wrapt runtime, once against the C extension and once against the pure
157+
# Python implementation, as the runtime surface stubtest sees differs
158+
# between the two. Intentional stub-vs-runtime divergences are listed in
157159
# tests/stubtest_allowlist.txt; adding new ones should be accompanied by
158-
# a comment there explaining why the divergence is deliberate.
160+
# a comment there explaining why the divergence is deliberate. The pure
161+
# Python run passes --ignore-unused-allowlist since entries which only
162+
# apply to the C extension are legitimately unused in that run.
159163
test-stubtest:
160-
uv run --with 'mypy=={{mypy_version}}' --with-editable . python -m mypy.stubtest wrapt --allowlist tests/stubtest_allowlist.txt
164+
WRAPT_INSTALL_EXTENSIONS=true uv run --reinstall-package wrapt --with 'mypy=={{mypy_version}}' --with-editable . python -c "import wrapt.__wrapt__; assert wrapt.__wrapt__._using_c_extension, 'C extension not loaded'"
165+
WRAPT_INSTALL_EXTENSIONS=true uv run --with 'mypy=={{mypy_version}}' --with-editable . python -m mypy.stubtest wrapt --allowlist tests/stubtest_allowlist.txt
166+
WRAPT_DISABLE_EXTENSIONS=true uv run --with 'mypy=={{mypy_version}}' --with-editable . python -m mypy.stubtest wrapt --allowlist tests/stubtest_allowlist.txt --ignore-unused-allowlist
161167

162168
view-mypy-test test:
163169
MYPYPATH=src/ uv run --with 'mypy=={{mypy_version}}' mypy --strict --show-error-codes tests/mypy/{{test}}.py

TESTING.md

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,12 @@ The project uses a special naming convention to ensure certain tests only run on
1717

1818
Test files can include version suffixes to indicate minimum Python version requirements:
1919

20-
- `test_name_py3.py` - Runs only on Python 3.x (skipped on Python 2.x)
21-
- `test_name_py2.py` - Runs only on Python 2.x (skipped on Python 3.x)
22-
- `test_name_py36.py` - Runs only on Python 3.6 and later
23-
- `test_name_py37.py` - Runs only on Python 3.7 and later
24-
- `test_name_py38.py` - Runs only on Python 3.8 and later
20+
- `test_name_py310.py` - Runs only on Python 3.10 and later
21+
- `test_name_py312.py` - Runs only on Python 3.12 and later
2522

26-
### Examples in the Codebase
27-
28-
- `test_class_py37.py` - Tests class-related features that require Python 3.7+
29-
- `test_descriptors_py36.py` - Tests descriptor features that require Python 3.6+
30-
- `test_inheritance_py37.py` - Tests inheritance features that require Python 3.7+
31-
- `test_adapter_py33.py` - Tests adapter features that require Python 3.3+ (keyword-only arguments)
23+
There are currently no version-specific test files in the codebase, as all
24+
tests run on the oldest supported Python version, but the mechanism remains
25+
available for tests which need features from a newer Python version.
3226

3327
### How It Works
3428

@@ -83,6 +77,12 @@ just test-mypy-version 3.13
8377
```
8478
Runs mypy type checking for a specific Python version. Replace `3.13` with any supported version.
8579

80+
#### Checking Type Stubs with stubtest
81+
```bash
82+
just test-stubtest
83+
```
84+
Runs `stubtest` to compare the `wrapt-stubs` package against the installed wrapt runtime. Intentional stub-vs-runtime divergences are listed in `tests/stubtest_allowlist.txt`; new entries should be accompanied by a comment there explaining why the divergence is deliberate.
85+
8686
### Test Variants
8787

8888
Each `test-version` run includes three important test scenarios:
@@ -106,7 +106,7 @@ Each `test-version` run includes three important test scenarios:
106106

107107
## Mypy Type Checking Tests
108108

109-
The project includes custom pytest handlers for testing mypy type checking behavior. These tests ensure that the wrapt library's type annotations work correctly and produce expected mypy error messages. Note that `mypy` must exist in your `PATH` else the tests related to type checking which be skipped.
109+
The project includes custom pytest handlers for testing mypy type checking behavior. These tests ensure that the wrapt library's type annotations work correctly and produce expected mypy error messages. Note that `mypy` must exist in your `PATH` else the tests related to type checking will be skipped.
110110

111111
### Test File Convention
112112

@@ -119,11 +119,13 @@ Mypy tests follow a specific naming pattern in the `tests/mypy/` directory:
119119

120120
The custom test handler in `conftest.py` automatically discovers pairs of `mypy_*.py` and `mypy_*.out` files and:
121121

122-
1. Runs `mypy --show-error-codes --python-version X.Y` on the `.py` file
122+
1. Runs `mypy --strict --show-error-codes --python-version X.Y` on the `.py` file
123123
2. Compares the actual output with the expected output in the `.out` file
124124
3. Fails the test if the outputs don't match
125125

126-
These tests only run on Python 3.9+ to ensure consistent mypy behavior.
126+
These tests only run on Python 3.10+ to ensure consistent mypy behavior, and are skipped on PyPy.
127+
128+
The expected `.out` files are generated with the mypy version pinned by the `mypy_version` variable in the `Justfile`. Running the tests with a different mypy version may produce differing output and false failures, so always use the `Justfile` recipes, or install the pinned mypy version, when working with these tests.
127129

128130
### Creating New Mypy Tests
129131

@@ -144,12 +146,12 @@ To create a new mypy test case:
144146
just save-mypy-test mypy_your_test_name
145147
```
146148

147-
3. **Verify the output**: Check the output from running the test against expected output, by running:
149+
4. **Verify the output**: Check the output from running the test against expected output, by running:
148150
```bash
149-
just check-mypy-test mypy_your_test_name
151+
just verify-mypy-test mypy_your_test_name
150152
```
151153

152-
4. **Run the test**: The test will automatically be discovered and run with pytest:
154+
5. **Run the test**: The test will automatically be discovered and run with pytest:
153155
```bash
154156
uv run pytest tests/ -k mypy_your_test_name
155157
```

docs/changes.rst

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,89 @@
11
Release Notes
22
=============
33

4+
Version 2.3.0
5+
-------------
6+
7+
**New Features**
8+
9+
* The ``__trunc__()``, ``__floor__()`` and ``__ceil__()`` special methods
10+
are now implemented by object proxies, delegating to ``math.trunc()``,
11+
``math.floor()`` and ``math.ceil()`` applied to the wrapped object. As
12+
with other special methods, these are only looked up on the class type
13+
and not the instance, so they cannot rely on the ``__getattr__()``
14+
fallback of the proxy and must be implemented explicitly. Previously
15+
calling ``math.trunc()`` on an object proxy raised ``TypeError``. These
16+
special methods sit somewhat outside the core Python object model in that
17+
they are not used by any builtin operators, with the ``math`` module
18+
being their only consumer. They are however documented as part of the
19+
Python data model and the ``math`` module is a key module in the standard
20+
library, so supporting them is warranted, in the same way as the existing
21+
support for ``__round__()``, which is consumed by the ``round()``
22+
builtin. Note that although ``math.floor()`` and ``math.ceil()``
23+
previously appeared to work when used on an object proxy, they were
24+
silently falling back to converting the proxy using ``__float__()``. If
25+
the wrapped object provided its own ``__floor__()`` or ``__ceil__()``
26+
special methods these were ignored and the result could differ from that
27+
when the wrapped object was used directly. These now yield the same
28+
result as using the wrapped object directly. With thanks to Vincent Gao
29+
for `pull request #344
30+
<https://github.com/GrahamDumpleton/wrapt/pull/344>`_.
31+
32+
* The ``__fspath__()`` special method of the ``os.PathLike`` protocol has
33+
been added to the set of dunder methods which ``AutoObjectProxy``
34+
detects on the wrapped object and adds to the class it generates, so a
35+
proxy it creates around a path-like object can now be used with
36+
``os.fspath()``, the builtin ``open()`` and other standard library
37+
functions accepting paths. Note that ``__fspath__()`` is deliberately
38+
not implemented by the base object proxy, since its presence on the
39+
proxy type would cause every proxy to be classified as path-like by
40+
code branching on ``isinstance(obj, os.PathLike)``. Also be aware that
41+
``AutoObjectProxy`` creates a new class for every proxy instance, so it
42+
should not be used to wrap path-like objects in large numbers due to
43+
the memory overhead. For high-frequency use define a custom proxy class
44+
which adds an explicit ``__fspath__()`` method instead. See the section
45+
on wrapping path-like objects in the known issues documentation for
46+
more details.
47+
48+
**Features Changed**
49+
50+
* The type stubs have been aligned with the runtime behaviour of the code
51+
and are now verified by ``stubtest`` against both the C extension and
52+
pure Python implementations. If using a type checker there are a couple
53+
of changes in what will be accepted which may be noticed. The
54+
``__self_dict__`` attribute of proxy objects is now declared as a read
55+
only property, matching the runtime where assignment to it deliberately
56+
raises ``AttributeError``, so assignment to it will now be rejected by
57+
type checkers. The ``PartialCallableObjectProxy``, ``WeakFunctionProxy``
58+
and ``bind_state_to_wrapper`` classes, which always derived from the base
59+
object proxy at runtime, are now also declared that way in the stubs.
60+
This means use of the object proxy interface on instances of these
61+
classes, such as accessing ``__wrapped__``, is no longer falsely
62+
rejected, although as a consequence of inheriting the permissive
63+
``__getattr__()`` of the proxy, attribute typos on these classes will no
64+
longer be caught. The ``__class_getitem__()`` special method and the
65+
``__bound_function_wrapper__`` attribute of ``FunctionWrapper`` are now
66+
also declared in the stubs. Finally, the pure Python implementation was
67+
brought into line with the C implementation and the descriptor protocol
68+
in two small ways. The ``owner`` argument of ``__get__()`` on function
69+
wrappers now defaults to ``None``, so manual binding using the one
70+
argument form works, and the argument to ``__class_getitem__()`` is now
71+
positional only.
72+
73+
**Bugs Fixed**
74+
75+
* Calling ``bytes()`` on an object proxy did not match calling ``bytes()``
76+
on the wrapped object directly when the wrapped object did not implement
77+
``__bytes__()``. The C extension implementation of ``__bytes__()`` used
78+
``PyObject_Bytes()``, which only honours the ``__bytes__()`` protocol, so
79+
``bytes(wrapt.ObjectProxy(3))`` raised ``TypeError`` even though
80+
``bytes(3)`` returns a zero filled buffer. The pure Python implementation
81+
already used the ``bytes()`` constructor and was unaffected. The C
82+
extension now uses the ``bytes()`` constructor as well, so both
83+
implementations yield the same result as using the wrapped object
84+
directly. With thanks to Sanjay Santhanam for `pull request #345
85+
<https://github.com/GrahamDumpleton/wrapt/pull/345>`_.
86+
487
Version 2.2.2
588
-------------
689

0 commit comments

Comments
 (0)