|
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