1- from pkgutil import iter_modules
1+ from __future__ import annotations
2+
23import sys
4+ from pkgutil import iter_modules
5+ from typing import TYPE_CHECKING
36
7+ if TYPE_CHECKING :
8+ from collections .abc import Callable , Generator , Iterable
9+ from importlib .abc import Loader
10+ from types import ModuleType
11+ from typing_extensions import TypeIs
12+ from importscan .types import IgnoreModule , ModuleInfo , StrOrBytesPath
413
5- def scan (package , ignore = None , handle_error = None ):
14+
15+ def scan (
16+ package : ModuleType ,
17+ ignore : Iterable [IgnoreModule ] | IgnoreModule | None = None ,
18+ handle_error : Callable [[str , Exception ], object ] | None = None ,
19+ ) -> None :
620 """Scan a package by importing it.
721
822 A framework can provide registration decorators: a decorator that
@@ -99,22 +113,39 @@ def handle_error(name, e):
99113 is_ignored = is_ignored ,
100114 handle_error = handle_error ,
101115 ):
102- try :
103- loader = importer .find_spec (modname ).loader
104- except AttributeError :
105- # zipimport.zipimporter doesn't have find_spec
106- loader = importer .find_module (modname )
116+ # FIXME: Add support for MetaPathFinder? But how would that work?
117+ # What path do we pass in to get the correct result?
118+ # We probably would need to remember the value of path we passed
119+ # into iter_modules for submodules/subpackages. There's also
120+ # the additional issue that not all finders will implement the
121+ # non-standard iter_modules method, but without it there's
122+ # no way to list all of the modules. Also since walk_packages
123+ # already imports all of the packages, why are we importing
124+ # them again here? Shouldn't we only import modules here?
125+ # Also why do we do only use `import_module` here, but not
126+ # in `walk_packages`? Doesn't that mean that the additional
127+ # check in `import_module` doesn't do anything for packages?
128+ loader = importer .find_spec (modname ).loader # type: ignore
129+ assert loader is not None
130+
107131 try :
108132 import_module (modname , loader , handle_error )
109133 finally :
110- if hasattr (loader , "file" ) and hasattr (loader .file , "close" ):
111- loader .file .close ()
112-
113-
114- def import_module (modname , loader , handle_error ):
134+ if hasattr (loader , "file" ) and hasattr (
135+ loader .file , # pyright: ignore[reportAttributeAccessIssue]
136+ "close" ,
137+ ):
138+ loader .file .close () # pyright: ignore[reportAttributeAccessIssue]
139+
140+
141+ def import_module (
142+ modname : str ,
143+ loader : Loader ,
144+ handle_error : Callable [[str , Exception ], object ] | None ,
145+ ) -> None :
115146 get_filename = getattr (loader , "get_filename" , None )
116147 if get_filename is None :
117- get_filename = loader ._get_filename
148+ get_filename = loader ._get_filename # type: ignore[attr-defined]
118149 try :
119150 fn = get_filename (modname )
120151 except TypeError :
@@ -135,10 +166,13 @@ def import_module(modname, loader, handle_error):
135166 raise
136167
137168
138- def get_is_ignored (package , ignore ):
169+ def get_is_ignored (
170+ package : ModuleType , ignore : Iterable [IgnoreModule ] | IgnoreModule | None
171+ ) -> Callable [[str ], bool ]:
172+
139173 pkg_name = package .__name__
140174
141- def is_nonstr_iter (v ) :
175+ def is_nonstr_iter (v : object ) -> TypeIs [ Iterable [ IgnoreModule ]] :
142176 if isinstance (v , str ): # pragma: no cover
143177 return False
144178 return hasattr (v , "__iter__" )
@@ -157,23 +191,28 @@ def is_nonstr_iter(v):
157191 # functions, e.g. re.compile('pattern').search
158192 callable_ignores = [ign for ign in ignore if callable (ign )]
159193
160- def is_ignored (fullname ) :
194+ def is_ignored (fullname : str ) -> bool :
161195 for ign in rel_ignores :
162196 if fullname .startswith (pkg_name + ign ):
163197 return True
164198 for ign in abs_ignores :
165199 # non-leading-dotted name absolute object name
166200 if fullname .startswith (ign ):
167201 return True
168- for ign in callable_ignores :
169- if ign (fullname ):
202+ for ign_fn in callable_ignores :
203+ if ign_fn (fullname ):
170204 return True
171205 return False
172206
173207 return is_ignored
174208
175209
176- def walk_packages (path = None , prefix = "" , is_ignored = None , handle_error = None ):
210+ def walk_packages (
211+ path : Iterable [StrOrBytesPath ] | None = None ,
212+ prefix : str = "" ,
213+ is_ignored : Callable [[str ], bool ] | None = None ,
214+ handle_error : Callable [[str , Exception ], object ] | None = None ,
215+ ) -> Generator [ModuleInfo ]:
177216 """Yields (module_finder, name, ispkg) for all modules recursively
178217 on path, or, if path is ``None``, all accessible modules.
179218
@@ -205,10 +244,11 @@ def walk_packages(path=None, prefix="", is_ignored=None, handle_error=None):
205244
206245 """
207246
208- def seen (p , m = {}) :
247+ def seen (p : str , m : set [ str ] = set ()) -> bool :
209248 if p in m : # pragma: no cover
210249 return True
211- m [p ] = True
250+ m .add (p )
251+ return False
212252
213253 # iter_modules is nonrecursive
214254 for module_finder , name , ispkg in iter_modules (path , prefix ):
@@ -235,6 +275,6 @@ def seen(p, m={}):
235275 path = getattr (sys .modules [name ], "__path__" , None ) or []
236276
237277 # don't traverse path items we've seen before
238- path = [p for p in path if not seen (p )]
278+ path = [p for p in path if not seen (p )] # pyright: ignore
239279
240280 yield from walk_packages (path , name + "." , is_ignored , handle_error )
0 commit comments