1414from typing import List
1515from typing import Optional
1616from typing import Sequence
17- from typing import Set
1817from typing import Tuple
1918from typing import Type
2019from typing import TYPE_CHECKING
4645if TYPE_CHECKING :
4746 from typing_extensions import Literal
4847
48+ from _pytest .python import Package
49+
4950
5051def pytest_addoption (parser : Parser ) -> None :
5152 parser .addini (
@@ -571,6 +572,17 @@ def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
571572 return False
572573 return True
573574
575+ def _collectpackage (self , fspath : Path ) -> Optional ["Package" ]:
576+ from _pytest .python import Package
577+
578+ ihook = self .gethookproxy (fspath )
579+ if not self .isinitpath (fspath ):
580+ if ihook .pytest_ignore_collect (collection_path = fspath , config = self .config ):
581+ return None
582+
583+ pkg : Package = Package .from_parent (self , path = fspath )
584+ return pkg
585+
574586 def _collectfile (
575587 self , fspath : Path , handle_dupes : bool = True
576588 ) -> Sequence [nodes .Collector ]:
@@ -679,8 +691,6 @@ def perform_collect( # noqa: F811
679691 return items
680692
681693 def collect (self ) -> Iterator [Union [nodes .Item , nodes .Collector ]]:
682- from _pytest .python import Package
683-
684694 # Keep track of any collected nodes in here, so we don't duplicate fixtures.
685695 node_cache1 : Dict [Path , Sequence [nodes .Collector ]] = {}
686696 node_cache2 : Dict [Tuple [Type [nodes .Collector ], Path ], nodes .Collector ] = {}
@@ -690,63 +700,57 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:
690700 matchnodes_cache : Dict [Tuple [Type [nodes .Collector ], str ], CollectReport ] = {}
691701
692702 # Directories of pkgs with dunder-init files.
693- pkg_roots : Dict [Path , Package ] = {}
703+ pkg_roots : Dict [Path , "Package" ] = {}
704+
705+ pm = self .config .pluginmanager
694706
695707 for argpath , names in self ._initial_parts :
696708 self .trace ("processing argument" , (argpath , names ))
697709 self .trace .root .indent += 1
698710
699711 # Start with a Session root, and delve to argpath item (dir or file)
700712 # and stack all Packages found on the way.
701- # No point in finding packages when collecting doctests.
702- if not self .config .getoption ("doctestmodules" , False ):
703- pm = self .config .pluginmanager
704- for parent in (argpath , * argpath .parents ):
705- if not pm ._is_in_confcutdir (argpath ):
706- break
707-
708- if parent .is_dir ():
709- pkginit = parent / "__init__.py"
710- if pkginit .is_file () and pkginit not in node_cache1 :
711- col = self ._collectfile (pkginit , handle_dupes = False )
712- if col :
713- if isinstance (col [0 ], Package ):
714- pkg_roots [parent ] = col [0 ]
715- node_cache1 [col [0 ].path ] = [col [0 ]]
713+ for parent in (argpath , * argpath .parents ):
714+ if not pm ._is_in_confcutdir (argpath ):
715+ break
716+
717+ if parent .is_dir ():
718+ pkginit = parent / "__init__.py"
719+ if pkginit .is_file () and parent not in node_cache1 :
720+ pkg = self ._collectpackage (parent )
721+ if pkg is not None :
722+ pkg_roots [parent ] = pkg
723+ node_cache1 [pkg .path ] = [pkg ]
716724
717725 # If it's a directory argument, recurse and look for any Subpackages.
718726 # Let the Package collector deal with subnodes, don't collect here.
719727 if argpath .is_dir ():
720728 assert not names , f"invalid arg { (argpath , names )!r} "
721729
722- seen_dirs : Set [Path ] = set ()
723- for direntry in visit (argpath , self ._recurse ):
724- if not direntry .is_file ():
725- continue
730+ if argpath in pkg_roots :
731+ yield pkg_roots [argpath ]
726732
733+ for direntry in visit (argpath , self ._recurse ):
727734 path = Path (direntry .path )
728- dirpath = path .parent
729-
730- if dirpath not in seen_dirs :
731- # Collect packages first.
732- seen_dirs .add (dirpath )
733- pkginit = dirpath / "__init__.py"
734- if pkginit .exists ():
735- for x in self ._collectfile (pkginit ):
735+ if direntry .is_dir () and self ._recurse (direntry ):
736+ pkginit = path / "__init__.py"
737+ if pkginit .is_file ():
738+ pkg = self ._collectpackage (path )
739+ if pkg is not None :
740+ yield pkg
741+ pkg_roots [path ] = pkg
742+
743+ elif direntry .is_file ():
744+ if path .parent in pkg_roots :
745+ # Package handles this file.
746+ continue
747+ for x in self ._collectfile (path ):
748+ key2 = (type (x ), x .path )
749+ if key2 in node_cache2 :
750+ yield node_cache2 [key2 ]
751+ else :
752+ node_cache2 [key2 ] = x
736753 yield x
737- if isinstance (x , Package ):
738- pkg_roots [dirpath ] = x
739- if dirpath in pkg_roots :
740- # Do not collect packages here.
741- continue
742-
743- for x in self ._collectfile (path ):
744- key2 = (type (x ), x .path )
745- if key2 in node_cache2 :
746- yield node_cache2 [key2 ]
747- else :
748- node_cache2 [key2 ] = x
749- yield x
750754 else :
751755 assert argpath .is_file ()
752756
@@ -805,21 +809,6 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:
805809 self ._notfound .append ((report_arg , col ))
806810 continue
807811
808- # If __init__.py was the only file requested, then the matched
809- # node will be the corresponding Package (by default), and the
810- # first yielded item will be the __init__ Module itself, so
811- # just use that. If this special case isn't taken, then all the
812- # files in the package will be yielded.
813- if argpath .name == "__init__.py" and isinstance (matching [0 ], Package ):
814- try :
815- yield next (iter (matching [0 ].collect ()))
816- except StopIteration :
817- # The package collects nothing with only an __init__.py
818- # file in it, which gets ignored by the default
819- # "python_files" option.
820- pass
821- continue
822-
823812 yield from matching
824813
825814 self .trace .root .indent -= 1
0 commit comments