Skip to content

Disable assertion rewriting external modules #13421

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 26 commits into
base: main
Choose a base branch
from

Conversation

Tusenka
Copy link

@Tusenka Tusenka commented May 13, 2025

Disable assertion rewriting of external modules. Closes 13403.

@Tusenka
Copy link
Author

Tusenka commented May 13, 2025

Need to squash commits - OK to close, I'll reopen a new one with one commit

@psf-chronographer psf-chronographer bot added the bot:chronographer:provided (automation) changelog entry is part of PR label May 13, 2025
Copy link
Member

@RonnyPfannschmidt RonnyPfannschmidt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you for getting this started

i believe we need to put the path handling into assertion state so it can correctly pass from the configuration and include the invocation dir/rootdir in a more safe manner than the current heusterics

@Tusenka Tusenka force-pushed the disable_assertion_rewriting_external_modules branch from 23dcf0c to 54f46d7 Compare May 16, 2025 05:01
@nicoddemus
Copy link
Member

nicoddemus commented May 16, 2025

I skimmed through the issue (I'm short on time so I cannot do a more through research), but looking at the code is not immediately clear to me so thought I would ask:

Note that we want to rewrite asserts for files belonging to a pytest plugin, even if they are not test_*.py files. How does this patch relate to that? This is an important behavior that should be kept.

@Tusenka Tusenka force-pushed the disable_assertion_rewriting_external_modules branch from 6a9b406 to 1592f85 Compare May 16, 2025 16:10
@Tusenka
Copy link
Author

Tusenka commented May 16, 2025

I skimmed through the issue (I'm short on time so I cannot do a more through research), but looking at the code is not immediately clear to me so thought I would ask:

Note that we want to rewrite asserts for files belonging to a pytest plugin, even if they are not test_*.py files. How does this patch relate to that? This is an important behavior that should be kept.

At present time the fix is applied only for path which applies test_*py. The plugins are processed separately. I'll add some tests against that important part.

@Tusenka
Copy link
Author

Tusenka commented May 18, 2025

I skimmed through the issue (I'm short on time so I cannot do a more through research), but looking at the code is not immediately clear to me so thought I would ask:
Note that we want to rewrite asserts for files belonging to a pytest plugin, even if they are not test_*.py files. How does this patch relate to that? This is an important behavior that should be kept.

At present time the fix is applied only for path which applies test_*py. The plugins are processed separately. I'll add some tests against that important part.

Added some tests for plugin rewriting, it works now

@Tusenka Tusenka force-pushed the disable_assertion_rewriting_external_modules branch from 16f3fc9 to ba4263d Compare May 19, 2025 04:13
with mock.patch.object(hook, "fnpats", ["*.py"]):
assert hook.find_spec("file") is None

def test_assert_rewrite_correct_for_conftfest(
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mamy name it in another way - test_assert_rewrite_for_conftfest

@Tusenka Tusenka force-pushed the disable_assertion_rewriting_external_modules branch from 4e1f8b9 to db7dcd4 Compare May 22, 2025 04:12
@@ -108,6 +109,19 @@ def __init__(self, config: Config, mode) -> None:
self.trace = config.trace.root.get("assertion")
self.hook: rewrite.AssertionRewritingHook | None = None

@property
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this must mot use getwd instead use the invoction params

Copy link
Author

@Tusenka Tusenka May 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then we cannot change it on runtime as far as invocation param for pytester changes rootpath

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry for the type - this code shoud use the rootdir or the invocationdir from the invocation params of the config
see https://docs.pytest.org/en/stable/reference/reference.html#pytest.Config.invocation_params as well as the config rootdir

Copy link
Author

@Tusenka Tusenka May 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My error, I mean as far as Invocation Param is frozen - it can't be changed on runtime. Pytester starts after the config has been loaded. So to change the rootpath for pytester I need to rewrite the rootpath in someway. I could mock all invocation params or I could use getcwd alongside with config rootdir for the testing purpose.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The complete object can be replaced with a changed one

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok. would be done)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But is config.invocation_params.dir the correct thing to use? If I execute the tests using pytest src/tests/foo.py, I expect pytest to continue to rewrite the same files as if I execute just pytest.

I feel we should use config.rootpath here? What do you think @RonnyPfannschmidt ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed - the rootpath - else we miss imports to rewrite

@Tusenka Tusenka force-pushed the disable_assertion_rewriting_external_modules branch 2 times, most recently from 3b36041 to 23e0d70 Compare May 27, 2025 15:53
Copy link
Member

@nicoddemus nicoddemus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @Tusenka,

Left some comments, please take a look.

self.hook: rewrite.AssertionRewritingHook | None = None

@property
def rootpath(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have rootpath in Config, but this property returns something different so this is a bit misleading. I suggest we rename it to something else to avoid this confusion, how about:

Suggested change
def rootpath(self):
def invocation_path(self):

@@ -108,6 +109,19 @@ def __init__(self, config: Config, mode) -> None:
self.trace = config.trace.root.get("assertion")
self.hook: rewrite.AssertionRewritingHook | None = None

@property
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But is config.invocation_params.dir the correct thing to use? If I execute the tests using pytest src/tests/foo.py, I expect pytest to continue to rewrite the same files as if I execute just pytest.

I feel we should use config.rootpath here? What do you think @RonnyPfannschmidt ?

Comment on lines +1322 to +1327
@pytest.mark.skipif(
sys.platform.startswith("win32"), reason="cannot remove cwd on Windows"
)
@pytest.mark.skipif(
sys.platform.startswith("sunos5"), reason="cannot remove cwd on Solaris"
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these skipif marks still necessary?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is sunos still a supported python platform

As far as I'm aware sun is down

self, pytester: Pytester, hook: AssertionRewritingHook, monkeypatch
) -> None:
"""If test files contained outside the rootpath, then skip them"""
pytester.makepyfile(
Copy link
Member

@nicoddemus nicoddemus Jun 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to avoid mocking here, instead using a real scenario to ensure Python files outside of the rootpath do not have their assertions rewritten.

Please create a simple but real-world project using pytester, something like:

myproject/
   /venv
     /lib
        /external_lib.py
     pyvenv.cfg  

   /src
      main.py
   /tests
      conftest.py
      test_main.py
   pytest.ini
outside.py

pytest.ini should have these contents:

[pytest]
python_files = *.py

Then execute pytest.runpytest(), which will execute from the root of the above tree.

If I understand the objective of the issue correctly:

  • myproject/venv/lib/external_lib.py: should not have assertions rewritten -- the file is inside a virtual environment and is not part of a pytest plugin. The pyvenv.cfg file is how pytest detects virtual environments.
  • myproject/src/main.py: rewritten -- inside rootpath and matches python_files.
  • myproject/tests/conftest.py: rewritten -- conftest.py files are always rewritten.
  • myproject/tests/test_main.py: rewritten -- test files are always rewritten.
  • outside.py: should not have assertions rewritten -- outside the rootpath.

I think the scenario above should cover what needs to be tested.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to include an external plugin here too, to test the same scenario as in test_assert_rewrite_correct_for_plugins.

Copy link
Author

@Tusenka Tusenka Jun 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried to create the structure along with a some conftest plugin. Even witout changes it doen't rewrite conftest plugins assertions
image

Example:
https://github.com/Tusenka/pytest/tree/disable_assertion_rewriting_external_modules_draft/testing/example_scripts/rewrite
image

However it sees only top-level plugins:
['anyio', '_hypothesis_ftz_detector', '_hypothesis_globals', '_hypothesis_pytestplugin', 'hypothesis', 'pytest_twisted', 'typeguard']
Logs for rewriting

Does it an expected behavior? How does pytest provide plugins which are needed to be rewritten at the import stage? May be I do something wrong

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should rewrite the conftest files

If it doesn't then we have a bug we missed that needs a additional regression test

@Tusenka Tusenka force-pushed the disable_assertion_rewriting_external_modules branch from 26b5195 to c4e1bae Compare June 12, 2025 13:04
@Tusenka
Copy link
Author

Tusenka commented Jun 12, 2025

Do we need to rewrite contest plugins? It's the not original behavior

@RonnyPfannschmidt
Copy link
Member

Conftest files that are part of the collection should rewrite as far as I remember

@@ -110,8 +110,14 @@ class AssertionState:
def __init__(self, config: Config, mode) -> None:
self.mode = mode
self.trace = config.trace.root.get("assertion")
self.config = config
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's perhaps just pass in the invocation details

Id like to avoid full config in more objects

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. I need to think.
In this case we couldn't change the invocation path for pytester at the simplest way.
The config file is loaded on the initial import stage. Pytester is loaded after that stage and still doesn't know anything about AssertionState.
I need to think what is the best way to do that.

@@ -108,6 +109,19 @@ def __init__(self, config: Config, mode) -> None:
self.trace = config.trace.root.get("assertion")
self.hook: rewrite.AssertionRewritingHook | None = None

@property
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed - the rootpath - else we miss imports to rewrite

@@ -749,6 +750,13 @@ def chdir(self) -> None:
This is done automatically upon instantiation.
"""
self._monkeypatch.chdir(self.path)
self._monkeypatch.setattr(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do i gather correct that this bit is to alter the assertion rewtiter state

we should alter only that state, not the full pytest config of the surrounding pytest - this looks like a assertion rewriter shortcoming that needs a followup after this

but we certainly need to alter the assertion hook in sys metapath instead of a pytest config

Copy link
Author

@Tusenka Tusenka Jun 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pytester doesn't know anything about the AssertionState yet. In order to solve that I need to think in someway how to do this better

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should go with a temporary solution and add a followup issue

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this a artifact of a missing gitignore?

@RonnyPfannschmidt
Copy link
Member

my understanding is that rewrite of conftest plugins was always happening as part of the original assertion hooking

@Tusenka Tusenka force-pushed the disable_assertion_rewriting_external_modules branch from ace23c1 to 1d5a612 Compare June 13, 2025 05:00
@Tusenka Tusenka force-pushed the disable_assertion_rewriting_external_modules branch from 179b723 to edcf484 Compare June 13, 2025 06:44
@Tusenka
Copy link
Author

Tusenka commented Jun 13, 2025

But it still doen't see conftest plugins
image
Result of provided plugins:
['pytest', 'zope.interface', 'idna', 'anyio', 'sniffio', 'filelock', 'cachetools', 'incremental', 'greenlet', 'pluggy', 'hypothesis', 'Automat', 'wheel', 'virtualenv', 'coverage', 'pip', 'distlib', 'mock', 'pre_commit', 'typing_extensions', 'elementpath', 'pytest-twisted', 'PyYAML', 'pyproject-api', 'identify', 'colorama', 'urllib3', 'hyperlink', 'Pygments', 'sortedcontainers', 'constantly', 'nodeenv', 'xmlschema', 'setuptools', 'attrs', 'requests', 'pytest', 'exceptiongroup', 'decorator', 'platformdirs', 'packaging', 'charset-normalizer', 'cfgv', 'Twisted', 'certifi', 'chardet', 'tox', 'argcomplete', 'iniconfig', 'jaraco.collections', 'zipp', 'typeguard', 'wheel', 'importlib_metadata', 'jaraco.text', 'jaraco.context', 'autocommand', 'tomli', 'typing_extensions', 'more-itertools', 'jaraco.functools', 'packaging', 'platformdirs', 'inflect', 'backports.tarfile']

The conftest plugins are loaded after the PEP302/PEP451 import hook considers files to rewrite.
I propose a separate ticket for conftest plugins rewritng.
I'll try to "play" with this.

config = pytester.parseconfig()
state = AssertionState(config, "rewrite")
assert state.invocation_path == str(config.invocation_params.dir)
new_rootpath = str(pytester.path / "test")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please stay with the pathlib api instead of switching to Str and then back

self, pytester: Pytester, hook: AssertionRewritingHook, monkeypatch
) -> None:
"""If test files contained outside the rootpath, then skip them"""
pytester.makepyfile(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should rewrite the conftest files

If it doesn't then we have a bug we missed that needs a additional regression test

@@ -749,6 +750,13 @@ def chdir(self) -> None:
This is done automatically upon instantiation.
"""
self._monkeypatch.chdir(self.path)
self._monkeypatch.setattr(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should go with a temporary solution and add a followup issue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bot:chronographer:provided (automation) changelog entry is part of PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Disable assertion rewriting of external modules for python_files = *.py
3 participants