Skip to content

Commit 9957360

Browse files
committed
sage.misc.package_dir.walk_packages: New, use in sage.misc.dev_tools
1 parent 915dc24 commit 9957360

File tree

2 files changed

+124
-2
lines changed

2 files changed

+124
-2
lines changed

src/sage/misc/dev_tools.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ def load_submodules(module=None, exclude_pattern=None):
169169
load sage.geometry.polyhedron.palp_database... succeeded
170170
load sage.geometry.polyhedron.ppl_lattice_polygon... succeeded
171171
"""
172-
import pkgutil
172+
from .package_dir import walk_packages
173173

174174
if module is None:
175175
import sage
@@ -181,7 +181,7 @@ def load_submodules(module=None, exclude_pattern=None):
181181
else:
182182
exclude = None
183183

184-
for importer, module_name, ispkg in pkgutil.walk_packages(module.__path__, module.__name__ + '.'):
184+
for importer, module_name, ispkg in walk_packages(module.__path__, module.__name__ + '.'):
185185
if ispkg or module_name in sys.modules:
186186
continue
187187

src/sage/misc/package_dir.py

+122
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import os
1616
import glob
17+
import sys
1718
from contextlib import contextmanager
1819

1920

@@ -211,3 +212,124 @@ def cython_namespace_package_support():
211212
yield
212213
finally:
213214
Cython.Utils.is_package_dir = Cython.Build.Cythonize.is_package_dir = Cython.Build.Dependencies.is_package_dir = orig_is_package_dir
215+
216+
217+
def walk_packages(path=None, prefix='', onerror=None):
218+
r"""
219+
Yield :class:`pkgutil.ModuleInfo` for all modules recursively on ``path``.
220+
221+
This version of the standard library function :func:`pkgutil.walk_packages`
222+
addresses https://github.com/python/cpython/issues/73444 by handling
223+
the implicit namespace packages in the package layout used by Sage;
224+
see :func:`is_package_or_sage_namespace_package_dir`.
225+
226+
INPUT:
227+
228+
- ``path`` -- a list of paths to look for modules in or
229+
``None`` (all accessible modules).
230+
231+
- ``prefix`` -- a string to output on the front of every module name
232+
on output.
233+
234+
- ``onerror`` -- a function which gets called with one argument (the
235+
name of the package which was being imported) if any exception
236+
occurs while trying to import a package. If ``None``, ignore
237+
:class:`ImportError` but propagate all other exceptions.
238+
239+
EXAMPLES::
240+
241+
sage: sorted(sage.misc.package_dir.walk_packages(sage.misc.__path__)) # a namespace package
242+
[..., ModuleInfo(module_finder=FileFinder('.../sage/misc'), name='package_dir', ispkg=False), ...]
243+
"""
244+
# Adapted from https://github.com/python/cpython/blob/3.11/Lib/pkgutil.py
245+
246+
def iter_modules(path=None, prefix=''):
247+
"""
248+
Yield :class:`ModuleInfo` for all submodules on ``path``.
249+
"""
250+
from pkgutil import get_importer, iter_importers, ModuleInfo
251+
252+
if path is None:
253+
importers = iter_importers()
254+
elif isinstance(path, str):
255+
raise ValueError("path must be None or list of paths to look for modules in")
256+
else:
257+
importers = map(get_importer, path)
258+
259+
yielded = {}
260+
for i in importers:
261+
for name, ispkg in iter_importer_modules(i, prefix):
262+
if name not in yielded:
263+
yielded[name] = 1
264+
yield ModuleInfo(i, name, ispkg)
265+
266+
def iter_importer_modules(importer, prefix=''):
267+
r"""
268+
Yield :class:`ModuleInfo` for all modules of ``importer``.
269+
"""
270+
from importlib.machinery import FileFinder
271+
272+
if isinstance(importer, FileFinder):
273+
if importer.path is None or not os.path.isdir(importer.path):
274+
return
275+
276+
yielded = {}
277+
import inspect
278+
try:
279+
filenames = os.listdir(importer.path)
280+
except OSError:
281+
# ignore unreadable directories like import does
282+
filenames = []
283+
filenames.sort() # handle packages before same-named modules
284+
285+
for fn in filenames:
286+
modname = inspect.getmodulename(fn)
287+
if modname and (modname in ['__init__', 'all']
288+
or modname.startswith('all__')
289+
or modname in yielded):
290+
continue
291+
292+
path = os.path.join(importer.path, fn)
293+
ispkg = False
294+
295+
if not modname and os.path.isdir(path) and '.' not in fn:
296+
modname = fn
297+
if not (ispkg := is_package_or_sage_namespace_package_dir(path)):
298+
continue
299+
300+
if modname and '.' not in modname:
301+
yielded[modname] = 1
302+
yield prefix + modname, ispkg
303+
304+
elif not hasattr(importer, 'iter_modules'):
305+
yield from []
306+
307+
else:
308+
yield from importer.iter_modules(prefix)
309+
310+
def seen(p, m={}):
311+
if p in m:
312+
return True
313+
m[p] = True
314+
315+
for info in iter_modules(path, prefix):
316+
yield info
317+
318+
if info.ispkg:
319+
try:
320+
__import__(info.name)
321+
except ImportError:
322+
if onerror is not None:
323+
onerror(info.name)
324+
except Exception:
325+
if onerror is not None:
326+
onerror(info.name)
327+
else:
328+
raise
329+
else:
330+
path = getattr(sys.modules[info.name], '__path__', None) or []
331+
332+
# don't traverse path items we've seen before
333+
path = [p for p in path if not seen(p)]
334+
335+
yield from walk_packages(path, info.name + '.', onerror)

0 commit comments

Comments
 (0)