|
30 | 30 | )
|
31 | 31 |
|
32 | 32 | import pytest
|
| 33 | +from _pytest.pathlib import visit |
33 | 34 | from pytest import (
|
34 | 35 | Class,
|
35 | 36 | Collector,
|
@@ -625,68 +626,95 @@ def _patched_collect():
|
625 | 626 | collector.__original_collect = collector.collect
|
626 | 627 | collector.collect = _patched_collect
|
627 | 628 | elif type(collector) is Package:
|
| 629 | + if not collector.funcnamefilter(collector.name): |
| 630 | + return |
628 | 631 |
|
629 | 632 | def _patched_collect():
|
630 |
| - # When collector is a Package, collector.obj is the package's __init__.py. |
631 |
| - # Accessing the __init__.py to attach the fixture function may trigger |
632 |
| - # additional module imports or change the order of imports, which leads to |
633 |
| - # a number of problems. |
634 |
| - # see https://github.com/pytest-dev/pytest-asyncio/issues/729 |
635 |
| - # Moreover, Package.obj has been removed in pytest 8. |
636 |
| - # Therefore, pytest-asyncio creates a temporary Python module inside the |
637 |
| - # collected package. The sole purpose of that module is to house a fixture |
638 |
| - # function for the pacakge-scoped event loop fixture. Once the fixture |
639 |
| - # has been evaluated by pytest, the temporary module can be removed. |
640 |
| - with NamedTemporaryFile( |
641 |
| - dir=collector.path.parent, |
642 |
| - prefix="pytest_asyncio_virtual_module_", |
643 |
| - suffix=".py", |
644 |
| - ) as virtual_module_file: |
645 |
| - virtual_module = Module.from_parent( |
646 |
| - collector, path=Path(virtual_module_file.name) |
647 |
| - ) |
648 |
| - virtual_module_file.write( |
649 |
| - dedent( |
650 |
| - f"""\ |
651 |
| - import asyncio |
652 |
| - import pytest |
653 |
| - from pytest_asyncio.plugin import _temporary_event_loop_policy |
654 |
| - @pytest.fixture( |
655 |
| - scope="{collector_scope}", |
656 |
| - name="{collector.nodeid}::<event_loop>", |
657 |
| - ) |
658 |
| - def scoped_event_loop( |
659 |
| - *args, |
660 |
| - event_loop_policy, |
661 |
| - ): |
662 |
| - new_loop_policy = event_loop_policy |
663 |
| - with _temporary_event_loop_policy(new_loop_policy): |
664 |
| - loop = asyncio.new_event_loop() |
665 |
| - loop.__pytest_asyncio = True |
666 |
| - asyncio.set_event_loop(loop) |
667 |
| - yield loop |
668 |
| - loop.close() |
669 |
| - """ |
670 |
| - ).encode() |
671 |
| - ) |
672 |
| - virtual_module_file.flush() |
| 633 | + # pytest.Package collects all files and sub-packages. Pytest 8 changes |
| 634 | + # this logic to only collect a single directory. Sub-packages are then |
| 635 | + # collected by a separate Package collector. Therefore, this logic can be |
| 636 | + # dropped, once we move to pytest 8. |
| 637 | + collector_dir = Path(collector.path.parent) |
| 638 | + for direntry in visit(str(collector_dir), recurse=collector._recurse): |
| 639 | + if not direntry.name == "__init__.py": |
| 640 | + # No need to register a package-scoped fixture, if we aren't |
| 641 | + # collecting a (sub-)package |
| 642 | + continue |
| 643 | + pkgdir = Path(direntry.path).parent |
| 644 | + pkg_nodeid = str(pkgdir.relative_to(collector_dir)) |
| 645 | + if pkg_nodeid == ".": |
| 646 | + pkg_nodeid = "" |
673 | 647 | # Pytest's fixture matching algorithm compares a fixture's baseid with
|
674 | 648 | # an Item's nodeid to determine whether a fixture is available for a
|
675 |
| - # specific Item. Since Package.nodeid ends with __init__.py, the |
676 |
| - # fixture's baseid will also end with __init__.py, which prevents |
677 |
| - # the fixture from being matched to test items in the current package. |
678 |
| - # Since the fixture matching is purely based on string comparison, we |
679 |
| - # strip the __init__.py suffix from the Package's node ID and |
680 |
| - # tell the fixturemanager to collect the fixture with the modified |
681 |
| - # nodeid. This makes the fixture visible to all items in the package. |
| 649 | + # specific Item. Package.nodeid ends with __init__.py, so the |
| 650 | + # fixture's baseid will also end with __init__.py and prevents |
| 651 | + # the fixture from being matched to test items in the package. |
| 652 | + # Furthermore, Package also collects any sub-packages, which means |
| 653 | + # the ID of the scoped event loop for the package must change for |
| 654 | + # each sub-package. |
| 655 | + # As the fixture matching is purely based on string comparison, we |
| 656 | + # can assemble a path based on the root package path |
| 657 | + # (i.e. Package.path.parent) and the sub-package path |
| 658 | + # (i.e. Path(direntry.path).parent)). This makes the fixture visible |
| 659 | + # to all items in the package. |
682 | 660 | # see also https://github.com/pytest-dev/pytest/issues/11662#issuecomment-1879310072 # noqa
|
683 | 661 | # Possibly related to https://github.com/pytest-dev/pytest/issues/4085
|
684 |
| - fixturemanager = collector.config.pluginmanager.get_plugin("funcmanage") |
685 |
| - package_node_id = _removesuffix(collector.nodeid, "__init__.py") |
686 |
| - fixturemanager.parsefactories( |
687 |
| - virtual_module.obj, nodeid=package_node_id |
| 662 | + fixture_id = ( |
| 663 | + str(Path(pkg_nodeid).joinpath("__init__.py")) + "::<event_loop>" |
688 | 664 | )
|
689 |
| - yield virtual_module |
| 665 | + # When collector is a Package, collector.obj is the package's |
| 666 | + # __init__.py. Accessing the __init__.py to attach the fixture function |
| 667 | + # may trigger additional module imports or change the order of imports, |
| 668 | + # which leads to a number of problems. |
| 669 | + # see https://github.com/pytest-dev/pytest-asyncio/issues/729 |
| 670 | + # Moreover, Package.obj has been removed in pytest 8. |
| 671 | + # Therefore, pytest-asyncio creates a temporary Python module inside the |
| 672 | + # collected package. The sole purpose of that module is to house a |
| 673 | + # fixture function for the pacakge-scoped event loop fixture. Once the |
| 674 | + # fixture has been evaluated by pytest, the temporary module |
| 675 | + # can be removed. |
| 676 | + with NamedTemporaryFile( |
| 677 | + dir=pkgdir, |
| 678 | + prefix="pytest_asyncio_virtual_module_", |
| 679 | + suffix=".py", |
| 680 | + ) as virtual_module_file: |
| 681 | + virtual_module = Module.from_parent( |
| 682 | + collector, path=Path(virtual_module_file.name) |
| 683 | + ) |
| 684 | + virtual_module_file.write( |
| 685 | + dedent( |
| 686 | + f"""\ |
| 687 | + import asyncio |
| 688 | + import pytest |
| 689 | + from pytest_asyncio.plugin \ |
| 690 | + import _temporary_event_loop_policy |
| 691 | + @pytest.fixture( |
| 692 | + scope="{collector_scope}", |
| 693 | + name="{fixture_id}", |
| 694 | + ) |
| 695 | + def scoped_event_loop( |
| 696 | + *args, |
| 697 | + event_loop_policy, |
| 698 | + ): |
| 699 | + new_loop_policy = event_loop_policy |
| 700 | + with _temporary_event_loop_policy(new_loop_policy): |
| 701 | + loop = asyncio.new_event_loop() |
| 702 | + loop.__pytest_asyncio = True |
| 703 | + asyncio.set_event_loop(loop) |
| 704 | + yield loop |
| 705 | + loop.close() |
| 706 | + """ |
| 707 | + ).encode() |
| 708 | + ) |
| 709 | + virtual_module_file.flush() |
| 710 | + fixturemanager = collector.config.pluginmanager.get_plugin( |
| 711 | + "funcmanage" |
| 712 | + ) |
| 713 | + # Collect the fixtures in the virtual module with the node ID of |
| 714 | + # the current sub-package to ensure correct fixture matching. |
| 715 | + # see also https://github.com/pytest-dev/pytest/issues/11662#issuecomment-1879310072 # noqa |
| 716 | + fixturemanager.parsefactories(virtual_module.obj, nodeid=pkg_nodeid) |
| 717 | + yield virtual_module |
690 | 718 | yield from collector.__original_collect()
|
691 | 719 |
|
692 | 720 | collector.__original_collect = collector.collect
|
|
0 commit comments