17
17
from typing import Optional
18
18
from typing import overload
19
19
from typing import Sequence
20
- from typing import Set
21
20
from typing import Tuple
22
21
from typing import Type
22
+ from typing import TYPE_CHECKING
23
23
from typing import Union
24
24
25
25
import _pytest ._code
43
43
from _pytest .runner import SetupState
44
44
45
45
46
+ if TYPE_CHECKING :
47
+ from _pytest .python import Package
48
+
49
+
46
50
def pytest_addoption (parser : Parser ) -> None :
47
51
parser .addini (
48
52
"norecursedirs" ,
@@ -572,6 +576,17 @@ def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
572
576
return False
573
577
return True
574
578
579
+ def _collectpackage (self , fspath : Path ) -> Optional ["Package" ]:
580
+ from _pytest .python import Package
581
+
582
+ ihook = self .gethookproxy (fspath )
583
+ if not self .isinitpath (fspath ):
584
+ if ihook .pytest_ignore_collect (collection_path = fspath , config = self .config ):
585
+ return None
586
+
587
+ pkg : Package = Package .from_parent (self , path = fspath )
588
+ return pkg
589
+
575
590
def _collectfile (
576
591
self , fspath : Path , handle_dupes : bool = True
577
592
) -> Sequence [nodes .Collector ]:
@@ -680,8 +695,6 @@ def perform_collect( # noqa: F811
680
695
return items
681
696
682
697
def collect (self ) -> Iterator [Union [nodes .Item , nodes .Collector ]]:
683
- from _pytest .python import Package
684
-
685
698
# Keep track of any collected nodes in here, so we don't duplicate fixtures.
686
699
node_cache1 : Dict [Path , Sequence [nodes .Collector ]] = {}
687
700
node_cache2 : Dict [Tuple [Type [nodes .Collector ], Path ], nodes .Collector ] = {}
@@ -691,63 +704,57 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:
691
704
matchnodes_cache : Dict [Tuple [Type [nodes .Collector ], str ], CollectReport ] = {}
692
705
693
706
# Directories of pkgs with dunder-init files.
694
- pkg_roots : Dict [Path , Package ] = {}
707
+ pkg_roots : Dict [Path , "Package" ] = {}
708
+
709
+ pm = self .config .pluginmanager
695
710
696
711
for argpath , names in self ._initial_parts :
697
712
self .trace ("processing argument" , (argpath , names ))
698
713
self .trace .root .indent += 1
699
714
700
715
# Start with a Session root, and delve to argpath item (dir or file)
701
716
# and stack all Packages found on the way.
702
- # No point in finding packages when collecting doctests.
703
- if not self .config .getoption ("doctestmodules" , False ):
704
- pm = self .config .pluginmanager
705
- for parent in (argpath , * argpath .parents ):
706
- if not pm ._is_in_confcutdir (argpath ):
707
- break
708
-
709
- if parent .is_dir ():
710
- pkginit = parent / "__init__.py"
711
- if pkginit .is_file () and pkginit not in node_cache1 :
712
- col = self ._collectfile (pkginit , handle_dupes = False )
713
- if col :
714
- if isinstance (col [0 ], Package ):
715
- pkg_roots [parent ] = col [0 ]
716
- node_cache1 [col [0 ].path ] = [col [0 ]]
717
+ for parent in (argpath , * argpath .parents ):
718
+ if not pm ._is_in_confcutdir (argpath ):
719
+ break
720
+
721
+ if parent .is_dir ():
722
+ pkginit = parent / "__init__.py"
723
+ if pkginit .is_file () and parent not in node_cache1 :
724
+ pkg = self ._collectpackage (parent )
725
+ if pkg is not None :
726
+ pkg_roots [parent ] = pkg
727
+ node_cache1 [pkg .path ] = [pkg ]
717
728
718
729
# If it's a directory argument, recurse and look for any Subpackages.
719
730
# Let the Package collector deal with subnodes, don't collect here.
720
731
if argpath .is_dir ():
721
732
assert not names , f"invalid arg { (argpath , names )!r} "
722
733
723
- seen_dirs : Set [Path ] = set ()
724
- for direntry in visit (argpath , self ._recurse ):
725
- if not direntry .is_file ():
726
- continue
734
+ if argpath in pkg_roots :
735
+ yield pkg_roots [argpath ]
727
736
737
+ for direntry in visit (argpath , self ._recurse ):
728
738
path = Path (direntry .path )
729
- dirpath = path .parent
730
-
731
- if dirpath not in seen_dirs :
732
- # Collect packages first.
733
- seen_dirs .add (dirpath )
734
- pkginit = dirpath / "__init__.py"
735
- if pkginit .exists ():
736
- for x in self ._collectfile (pkginit ):
739
+ if direntry .is_dir () and self ._recurse (direntry ):
740
+ pkginit = path / "__init__.py"
741
+ if pkginit .is_file ():
742
+ pkg = self ._collectpackage (path )
743
+ if pkg is not None :
744
+ yield pkg
745
+ pkg_roots [path ] = pkg
746
+
747
+ elif direntry .is_file ():
748
+ if path .parent in pkg_roots :
749
+ # Package handles this file.
750
+ continue
751
+ for x in self ._collectfile (path ):
752
+ key2 = (type (x ), x .path )
753
+ if key2 in node_cache2 :
754
+ yield node_cache2 [key2 ]
755
+ else :
756
+ node_cache2 [key2 ] = x
737
757
yield x
738
- if isinstance (x , Package ):
739
- pkg_roots [dirpath ] = x
740
- if dirpath in pkg_roots :
741
- # Do not collect packages here.
742
- continue
743
-
744
- for x in self ._collectfile (path ):
745
- key2 = (type (x ), x .path )
746
- if key2 in node_cache2 :
747
- yield node_cache2 [key2 ]
748
- else :
749
- node_cache2 [key2 ] = x
750
- yield x
751
758
else :
752
759
assert argpath .is_file ()
753
760
@@ -806,21 +813,6 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:
806
813
self ._notfound .append ((report_arg , col ))
807
814
continue
808
815
809
- # If __init__.py was the only file requested, then the matched
810
- # node will be the corresponding Package (by default), and the
811
- # first yielded item will be the __init__ Module itself, so
812
- # just use that. If this special case isn't taken, then all the
813
- # files in the package will be yielded.
814
- if argpath .name == "__init__.py" and isinstance (matching [0 ], Package ):
815
- try :
816
- yield next (iter (matching [0 ].collect ()))
817
- except StopIteration :
818
- # The package collects nothing with only an __init__.py
819
- # file in it, which gets ignored by the default
820
- # "python_files" option.
821
- pass
822
- continue
823
-
824
816
yield from matching
825
817
826
818
self .trace .root .indent -= 1
0 commit comments