Skip to content
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

Pytest async method fixture is run with different self than the test method #633

Closed
2 tasks done
Guerilla94 opened this issue Nov 16, 2023 · 14 comments · Fixed by #807
Closed
2 tasks done

Pytest async method fixture is run with different self than the test method #633

Guerilla94 opened this issue Nov 16, 2023 · 14 comments · Fixed by #807
Labels
bug Something isn't working

Comments

@Guerilla94
Copy link

Things to check first

  • I have searched the existing issues and didn't find my bug already reported there

  • I have checked that my bug is still present in the latest release

AnyIO version

4.0.0

Python version

3.11.4

What happened?

I'm trying to use an fixture class method to initialize some data for tests and store it in attributes. With synchronous fixture everything works as expected, the filled attributes are available in the test methods. But attributes filled in async function are not available in test methods. This happens because different objects are passed as "self" for the fixture and the test method.

I found similar issue for pytest-asyncio which was fixed

How can we reproduce the bug?

import pytest


class TestSomething:
    @pytest.fixture()
    async def do_setup(self):
        self.some_attr = 'value'

        print()
        print('in fixture')
        print(self)
        print(self.__dict__)

    async def test_something(self, do_setup):
        print()
        print('in test')
        print(self)          # different object
        print(self.__dict__) # some_attr missing

Output:

in fixture
<tests.test_anyio.TestSomething object at 0x107b0ca50>
{'some_attr': 'value'}

in test
<tests.test_anyio.TestSomething object at 0x107f06590>
{}
@Guerilla94 Guerilla94 added the bug Something isn't working label Nov 16, 2023
@agronholm
Copy link
Owner

Linking the pytest-asyncio fix PR here: pytest-dev/pytest-asyncio#439

@agronholm
Copy link
Owner

Meanwhile I suggest that you don't assign anything to the instance. That's not the correct way to use fixtures.

@Guerilla94
Copy link
Author

Guerilla94 commented Nov 16, 2023

Assigning something to an instance looks strange when using a fixture directly. We usually use a fixture with autouse=True to initialize data for all tests in a class:

class TestSomething:
    @pytest.fixture(autouse=True)
    def _setup(self):
        self.some_attr = 'value'

In this case, assigning to an instance seems more justifiable. Or in this case assigning to the instance is also not the correct way?

I didn't include autouse in the reproduction example because it doesn't affect the problem

@agronholm
Copy link
Owner

Why not return the value from the fixture?

@Guerilla94
Copy link
Author

To do this, you will have to specify in each test of class the use of the fixture directly instead of autouse. It looks like boilerplate when each test in the class directly uses same setup fixture:

class TestSomething:
    @pytest.fixture()
    async def setup_data(self):
        return {}

    async def test_something(self, setup_data):
        pass

    async def test_something_else(self, setup_data):
        pass

    async def test_something_else2(self, setup_data):
        pass

Additionally, not all tests require data from the setup, but the setup process is still necessary. In this case it is easy to forget to specify setup fixture for test

@agronholm
Copy link
Owner

Is there something preventing you from using autouse=True on that fixture then?

@Guerilla94
Copy link
Author

Thank you, now I understand your point. I haven't thought about using a fixture with autouse=True directly if result is needed. I'll need to rethink the use of fixtures. Looks like it would be better not to use assigning to instance in fixtures.

Therefore, I think that this behavior can be left as is. It seems that the issue can be closed

@agronholm
Copy link
Owner

Therefore, I think that this behavior can be left as is. It seems that the issue can be closed

I disagree. This plugin should not make pytest deviate from how it normally works.

@reneleonhardt
Copy link

@Guerilla94 does this problem still affect you?
The fix for pytest-asyncio has been merged 2 years ago, so anyio would have be fixed too?

@smurfix
Copy link
Collaborator

smurfix commented Oct 10, 2024

Umm, the problem still exists.

import pytest
from contextlib import asynccontextmanager

pytestmark = pytest.mark.anyio

class TestMe(object):
    @pytest.fixture(autouse=True, scope="function")
    async def fix_foo(self):
        print("setup A")
        self.foo = "bar"

    @pytest.fixture(autouse=True, scope="function")
    async def fix_bar(self):
        print("setup B")
        self.bar = "baz"
        yield
        print("teardown")

    async def testSupport(self):
        print("test start", id(self),vars(self))  # error: empty
        assert hasattr(self,"foo")
        assert hasattr(self,"bar")

A pytest run emits:

setup B 140325416335072
setup A 140325416335072
start 140325424259888 {}

and then dies, which is not the intended result at all.

Tested in a fresh venv. pytest 8.3.3, anyio 4.6.0, trio/asyncio doesn't matter.

This is a problem. I'm currently porting the caldav library to anyio; its tests use nontrivial setup/teardown methods which can easily be converted to fixtures (simply replace the "def teardown(…)" with "yield" and prepend @pytest.fixture(autouse=True)). Replacing the fixture with a context manager would re-indent the complete test file, and that basically prevents further merges from upstream and is not at all when I'm trying to achieve.

The above test works perfectly when using sync fixtures, or when using pytest-asyncio instead. I do NOT want to switch to that.

@agronholm
Copy link
Owner

I would welcome a PR to fix this.

@smurfix
Copy link
Collaborator

smurfix commented Oct 10, 2024

So would I. I would also welcome enough free time to acquire enough knowledge of pytest's internals to be able to do so, but the world doesn't seem to work that way. :-/

@agronholm
Copy link
Owner

I'll see if I can fix this quickly. No promises though.

@smurfix
Copy link
Collaborator

smurfix commented Oct 10, 2024

Don't sweat it, for my purposes the pytest-anyio workaround is sufficient for the short term.

agronholm added a commit that referenced this issue Oct 13, 2024
Fixes #633.

---------

Co-authored-by: Thomas Grainger <tagrain@gmail.com>

(cherry picked from commit 65ef48a)
mkjpryor pushed a commit to azimuth-cloud/cluster-api-addon-provider that referenced this issue Oct 14, 2024
Bumps [anyio](https://github.com/agronholm/anyio) from 4.6.0 to 4.6.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/agronholm/anyio/releases">anyio's
releases</a>.</em></p>
<blockquote>
<h2>4.6.2</h2>
<ul>
<li>Fixed regression caused by (<a
href="https://redirect.github.com/agronholm/anyio/pull/807">#807</a>)
that prevented the use of parametrized async fixtures</li>
</ul>
<h2>4.6.1</h2>
<p>This release contains all the changes from both v4.5.1 and v4.6.0,
plus:</p>
<ul>
<li>Fixed TaskGroup and CancelScope producing cyclic references in
tracebacks when raising exceptions (<a
href="https://redirect.github.com/agronholm/anyio/pull/806">#806</a>)
(PR by <a
href="https://github.com/graingert"><code>@​graingert</code></a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst">anyio's
changelog</a>.</em></p>
<blockquote>
<h1>Version history</h1>
<p>This library adheres to <code>Semantic Versioning 2.0
&lt;http://semver.org/&gt;</code>_.</p>
<p><strong>4.6.2</strong></p>
<ul>
<li>Fixed regression caused by
(<code>[#807](agronholm/anyio#807)
&lt;https://github.com/agronholm/anyio/pull/807&gt;</code>_)
that prevented the use of parametrized async fixtures</li>
</ul>
<p><strong>4.6.1</strong></p>
<p>This release contains all the changes from both v4.5.1 and v4.6.0,
plus:</p>
<ul>
<li>Fixed TaskGroup and CancelScope producing cyclic references in
tracebacks
when raising exceptions
(<code>[#806](agronholm/anyio#806)
&lt;https://github.com/agronholm/anyio/pull/806&gt;</code>_)
(PR by <a
href="https://github.com/graingert"><code>@​graingert</code></a>)</li>
</ul>
<p><strong>4.6.0</strong></p>
<p>This release is the successor to v4.5.0 with Python 3.8 support
dropped, and does not
contain the changes from v4.5.1.</p>
<ul>
<li>Dropped support for Python 3.8
(as <code>[#698](agronholm/anyio#698)
&lt;https://github.com/agronholm/anyio/issues/698&gt;</code>_ cannot be
resolved
without cancel message support)</li>
<li>Fixed 100% CPU use on asyncio while waiting for an exiting task
group to finish while
said task group is within a cancelled cancel scope
(<code>[#695](agronholm/anyio#695)
&lt;https://github.com/agronholm/anyio/issues/695&gt;</code>_)</li>
<li>Fixed cancel scopes on asyncio not propagating
<code>CancelledError</code> on exit when the
enclosing cancel scope has been effectively cancelled
(<code>[#698](agronholm/anyio#698)
&lt;https://github.com/agronholm/anyio/issues/698&gt;</code>_)</li>
<li>Fixed asyncio task groups not yielding control to the event loop at
exit if there were
no child tasks to wait on</li>
<li>Fixed inconsistent task uncancellation with asyncio cancel scopes
belonging to a
task group when said task group has child tasks running</li>
</ul>
<p><strong>4.5.1</strong></p>
<p>As Python 3.8 support was dropped in v4.6.0, this interim release was
created to bring a
regression fix to Python 3.8, and adds a few other fixes also present in
v4.6.1.</p>
<ul>
<li>Fixed acquring a lock twice in the same task on asyncio hanging
instead of raising a
<code>RuntimeError</code>
(<code>[#798](agronholm/anyio#798)
&lt;https://github.com/agronholm/anyio/issues/798&gt;</code>_)</li>
<li>Fixed an async fixture's <code>self</code> being different than the
test's <code>self</code> in
class-based tests
(<code>[#633](agronholm/anyio#633)
&lt;https://github.com/agronholm/anyio/issues/633&gt;</code>_)
(PR by <a
href="https://github.com/agronholm"><code>@​agronholm</code></a> and <a
href="https://github.com/graingert"><code>@​graingert</code></a>)</li>
<li>Fixed <code>TypeError</code> with <code>TLSStream</code> on Windows
when a certificate verification
error occurs when using a <code>truststore
&lt;https://github.com/sethmlarson/truststore&gt;</code>_
SSL certificate
(<code>[#795](agronholm/anyio#795)
&lt;https://github.com/agronholm/anyio/issues/795&gt;</code>_)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/agronholm/anyio/commit/5861355e2a882e0e33c209fcab287569c8405189"><code>5861355</code></a>
Bumped up the version</li>
<li><a
href="https://github.com/agronholm/anyio/commit/f6ddfc803e2bc2fb79cd0556091b3b98e94a1de1"><code>f6ddfc8</code></a>
Fixed a regression in the pytest plugin that broke parametrized async
fixtures</li>
<li><a
href="https://github.com/agronholm/anyio/commit/4ecc96330c0dd8789db964a5569bff7f09ca083b"><code>4ecc963</code></a>
Bumped up the version</li>
<li><a
href="https://github.com/agronholm/anyio/commit/5489fbd47148207fa830e1f33e751f9361c25713"><code>5489fbd</code></a>
Fixed tox configuration</li>
<li><a
href="https://github.com/agronholm/anyio/commit/77b94df83e0c48264ef1ad3ba2fa58a28f292887"><code>77b94df</code></a>
Updated Path documentation and made is_junction() conditional (<a
href="https://redirect.github.com/agronholm/anyio/issues/800">#800</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/bf130dc793bcb52d65000e714d1e98b8ab243786"><code>bf130dc</code></a>
Fixed TypeError when TLS handshake fails with truststore SSLContext (<a
href="https://redirect.github.com/agronholm/anyio/issues/801">#801</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/4cb89a525cfaf2f02d0f761dc31185afb7b5fbd1"><code>4cb89a5</code></a>
Migrated to native TOML configuration for Tox</li>
<li><a
href="https://github.com/agronholm/anyio/commit/6bebf18279924f7a5178ef3c9ac926768b9240f9"><code>6bebf18</code></a>
Made test_start_task_soon_cancel_immediately() less flaky</li>
<li><a
href="https://github.com/agronholm/anyio/commit/e8546bd1cdac8922d5d43efd6e0be334488f9244"><code>e8546bd</code></a>
Rebind instance method fixtures to the same instance as the test (<a
href="https://redirect.github.com/agronholm/anyio/issues/807">#807</a>)</li>
<li><a
href="https://github.com/agronholm/anyio/commit/57bcbc9c5674fd6fc40077c2ad3810f84d9399ae"><code>57bcbc9</code></a>
Updated macOS and Windows to test on Python 3.13 by default</li>
<li>Additional commits viewable in <a
href="https://github.com/agronholm/anyio/compare/4.6.0...4.6.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=anyio&package-manager=pip&previous-version=4.6.0&new-version=4.6.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
4 participants