1717from typing import Optional
1818from typing import overload
1919from typing import Sequence
20- from typing import Set
2120from typing import Tuple
2221from typing import Type
22+ from typing import TYPE_CHECKING
2323from typing import Union
2424
2525import _pytest ._code
4343from _pytest .runner import SetupState
4444
4545
46+ if TYPE_CHECKING :
47+ from _pytest .python import Package
48+
49+
4650def pytest_addoption (parser : Parser ) -> None :
4751 parser .addini (
4852 "norecursedirs" ,
@@ -572,6 +576,17 @@ def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
572576 return False
573577 return True
574578
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+
575590 def _collectfile (
576591 self , fspath : Path , handle_dupes : bool = True
577592 ) -> Sequence [nodes .Collector ]:
@@ -680,8 +695,6 @@ def perform_collect( # noqa: F811
680695 return items
681696
682697 def collect (self ) -> Iterator [Union [nodes .Item , nodes .Collector ]]:
683- from _pytest .python import Package
684-
685698 # Keep track of any collected nodes in here, so we don't duplicate fixtures.
686699 node_cache1 : Dict [Path , Sequence [nodes .Collector ]] = {}
687700 node_cache2 : Dict [Tuple [Type [nodes .Collector ], Path ], nodes .Collector ] = {}
@@ -691,63 +704,57 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:
691704 matchnodes_cache : Dict [Tuple [Type [nodes .Collector ], str ], CollectReport ] = {}
692705
693706 # 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
695710
696711 for argpath , names in self ._initial_parts :
697712 self .trace ("processing argument" , (argpath , names ))
698713 self .trace .root .indent += 1
699714
700715 # Start with a Session root, and delve to argpath item (dir or file)
701716 # 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 ]
717728
718729 # If it's a directory argument, recurse and look for any Subpackages.
719730 # Let the Package collector deal with subnodes, don't collect here.
720731 if argpath .is_dir ():
721732 assert not names , f"invalid arg { (argpath , names )!r} "
722733
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 ]
727736
737+ for direntry in visit (argpath , self ._recurse ):
728738 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
737757 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
751758 else :
752759 assert argpath .is_file ()
753760
@@ -806,21 +813,6 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:
806813 self ._notfound .append ((report_arg , col ))
807814 continue
808815
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-
824816 yield from matching
825817
826818 self .trace .root .indent -= 1
0 commit comments