Cached parameter annotations introduce cross-test pollution when Path is mocked
#1954
Replies: 2 comments 2 replies
|
Here is a comprehensive breakdown of what is happening under the hood and direct answers to your 5 questions: 1. When defined at module level, is a
|
|
@CAOShurong's breakdown is right that Typer holds no cache of its own, but point 2 needs a correction that turns out to be the whole explanation for the order-dependence you are seeing — and it depends on which Python you run.
That is true up to 3.13 and false from 3.14, where PEP 649 makes annotations lazy. They are evaluated on first access and cached from then on. Measured on the same script, swapping So on 3.14 a module-level Why it is sticky, and why the last test failsThe part that produces cross-test pollution is that on both versions, once materialised the value is frozen: Only the timing of that one-time materialisation differs — definition time on ≤3.13, first-access time on 3.14+. Either way the first resolution wins permanently, so whichever of your A fix that does not require redefining the commandMake the annotation deferred, so Typer's Concretely, either quote it or add the future import at the top of the module holding the command: from __future__ import annotations
def outer_echo(path: Annotated[pathlib.Path, typer.Argument()]) -> None:
print(path)Because One caveat worth stating plainly: this makes the annotation resolution order-independent, but it does not fix the underlying pyfakefs issue you linked (#1334) — |
Uh oh!
There was an error while loading. Please reload this page.
First Check
Example Code
Description
First of all, thanks for your hard work, @tiangolo. Typer is truly an amazing project!
I'm opening this thread:
Context
typer, which takes apathlib.Pathas an argumentpathlib.Pathwork as expectedpyfakefsto abstract I/O operations for unit testing, which replacespathlib.Pathwith a subclassFakePath; these tests may fail based on their execution orderSample test cases
The code snippet shared above implements a simple
echocommand in two different ways:outer_echofunction, defined at module-levelinner_echofunction, defined at fixture level, which provides a "clean" command to every test caseA clean
typer.Typerapplication is provided to every test.Each test case runs twice:
pathlib.Pathpyfakefs.fake_pathlib.FakePathRequired packages
pyfakefspytestpytest-random-order(to validate cross-test pollution)Test results
v0.27.2: allFakeFstests failTest execution: (include the trailing
]in the pattern)Key findings:
FakePathget_click_typepyfakefs(see issue#1334); can be partially mitigated by replacingPath→pathlib.PathPatch: module imports
I've worked around the
pyfakefsbug by replacing direct class imports with module imports. See: module_import.patchThis patch passes both tests using the
inner_echocommand, i.e., when the function is redefined for each test:However, the tests using the
outer_echocommand are still failing. In particular, the last executed test fails. Below, the test with the realpathlib.Pathis executed last - and fails:Below, the test with the in-memory
fake_pathlib.FakePathis executed last - and fails:Patch using
lenient_issubclassI have applied a further patch, replacing
annotation == Path→lenient_issubclass(annotation, pathlib.Path). See:lenient_subclass.patch
With this patch, both test cases with
outer_echopass when theFakeFsis executed first:However, the
FakeFstest fails when it's executed last:An introspection with
inspectin the latter test case shows that:annotationmodule is/home/user/.local/share/uv/python/cpython-3.14.6-linux-x86_64-gnu/lib/python3.14/pathlib/__init__.pypathlib.Pathmodule is/path/to/.venv/lib/python3.14/site-packages/pyfakefs/fake_pathlib.py, because it has been replaced byFakePathAs I understand it:
outer_echocommand is registered once, when the first test runs.pathlib.Pathis run first, the command is registered with apathlib.Pathannotation.pathlib.Pathwhen the second test runs.Pathhas been replaced byFakePath. The test fails becausePathis not a subclass ofFakePath(but the other way around).Note that all
bash/test.shtest cases pass with this patch.Patch: double
lenient_issubclassFinally, I applied a double subclass check in double_subclass.patch, shown briefly below:
All tests pass, regardless of order.
Caveats:
The "double issubclass" patch is unintuitive from a readability perspective.
The patch relies on undocumented behavior, namely, that
Pathmay be something else and that eitherannotationorPathcan be a subclass of the other, depending on the circumstances.It breaks a few test cases, raising the error:
Questions
typercommand created once? Example:outer_echoin the test above.pathlib.Pathannotation, but subsequently the type is replaced bypyfakefs.fake_pathlib.FakePath.typer.Typerapplication in every test. Yet, there is cross-test pollution, indicating that the parameter annotations are cached somewhere else.Path→pathlib.Pathis a no-op.Files
Operating System
Linux
Operating System Details
Debian 7.1.3-1
Project Version
0.27.2
Python Version
3.14.6
Additional Context
All reactions