Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Updated pkgutil + the associated test
  • Loading branch information
terryluan12 authored and youknowone committed Jan 4, 2026
commit b136a449fe470aa5a51d8122da5b59a4f2c9e4a1
34 changes: 15 additions & 19 deletions Lib/pkgutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
__all__ = [
'get_importer', 'iter_importers', 'get_loader', 'find_loader',
'walk_packages', 'iter_modules', 'get_data',
'ImpImporter', 'ImpLoader', 'read_code', 'extend_path',
'read_code', 'extend_path',
'ModuleInfo',
]

Expand All @@ -23,20 +23,6 @@
ModuleInfo.__doc__ = 'A namedtuple with minimal info about a module.'


def _get_spec(finder, name):
"""Return the finder-specific module spec."""
# Works with legacy finders.
try:
find_spec = finder.find_spec
except AttributeError:
loader = finder.find_module(name)
if loader is None:
return None
return importlib.util.spec_from_loader(name, loader)
else:
return find_spec(name)


def read_code(stream):
# This helper is needed in order for the PEP 302 emulation to
# correctly handle compiled files
Expand Down Expand Up @@ -184,6 +170,7 @@ def _iter_file_finder_modules(importer, prefix=''):
iter_importer_modules.register(
importlib.machinery.FileFinder, _iter_file_finder_modules)


try:
import zipimport
from zipimport import zipimporter
Expand Down Expand Up @@ -231,6 +218,7 @@ def get_importer(path_item):
The cache (or part of it) can be cleared manually if a
rescan of sys.path_hooks is necessary.
"""
path_item = os.fsdecode(path_item)
try:
importer = sys.path_importer_cache[path_item]
except KeyError:
Expand Down Expand Up @@ -282,6 +270,10 @@ def get_loader(module_or_name):
If the named module is not already imported, its containing package
(if any) is imported, in order to establish the package __path__.
"""
warnings._deprecated("pkgutil.get_loader",
f"{warnings._DEPRECATED_MSG}; "
"use importlib.util.find_spec() instead",
remove=(3, 14))
if module_or_name in sys.modules:
module_or_name = sys.modules[module_or_name]
if module_or_name is None:
Expand All @@ -306,6 +298,10 @@ def find_loader(fullname):
importlib.util.find_spec that converts most failures to ImportError
and only returns the loader rather than the full spec
"""
warnings._deprecated("pkgutil.find_loader",
f"{warnings._DEPRECATED_MSG}; "
"use importlib.util.find_spec() instead",
remove=(3, 14))
if fullname.startswith('.'):
msg = "Relative module name {!r} not supported".format(fullname)
raise ImportError(msg)
Expand All @@ -328,10 +324,10 @@ def extend_path(path, name):
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)

This will add to the package's __path__ all subdirectories of
directories on sys.path named after the package. This is useful
if one wants to distribute different parts of a single logical
package as multiple directories.
For each directory on sys.path that has a subdirectory that
matches the package name, add the subdirectory to the package's
__path__. This is useful if one wants to distribute different
parts of a single logical package as multiple directories.

It also looks for *.pkg files beginning where * matches the name
argument. This feature is similar to *.pth files (see site.py),
Expand Down
179 changes: 159 additions & 20 deletions Lib/test/test_pkgutil.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from pathlib import Path
from test.support.import_helper import unload, CleanImport
from test.support.warnings_helper import check_warnings
from test.support.warnings_helper import check_warnings, ignore_warnings
import unittest
import sys
import importlib
Expand All @@ -11,6 +12,10 @@
import shutil
import zipfile

from test.support.import_helper import DirsOnSysPath
from test.support.os_helper import FakePath
from test.test_importlib.util import uncache

# Note: pkgutil.walk_packages is currently tested in test_runpy. This is
# a hack to get a major issue resolved for 3.3b2. Longer term, it should
# be moved back here, perhaps by factoring out the helper code for
Expand Down Expand Up @@ -91,6 +96,45 @@ def test_getdata_zipfile(self):

del sys.modules[pkg]

def test_issue44061_iter_modules(self):
#see: issue44061
zip = 'test_getdata_zipfile.zip'
pkg = 'test_getdata_zipfile'

# Include a LF and a CRLF, to test that binary data is read back
RESOURCE_DATA = b'Hello, world!\nSecond line\r\nThird line'

# Make a package with some resources
zip_file = os.path.join(self.dirname, zip)
z = zipfile.ZipFile(zip_file, 'w')

# Empty init.py
z.writestr(pkg + '/__init__.py', "")
# Resource files, res.txt
z.writestr(pkg + '/res.txt', RESOURCE_DATA)
z.close()

# Check we can read the resources
sys.path.insert(0, zip_file)
try:
res = pkgutil.get_data(pkg, 'res.txt')
self.assertEqual(res, RESOURCE_DATA)

# make sure iter_modules accepts Path objects
names = []
for moduleinfo in pkgutil.iter_modules([FakePath(zip_file)]):
self.assertIsInstance(moduleinfo, pkgutil.ModuleInfo)
names.append(moduleinfo.name)
self.assertEqual(names, [pkg])
finally:
del sys.path[0]
sys.modules.pop(pkg, None)

# assert path must be None or list of paths
expected_msg = "path must be None or list of paths to look for modules in"
with self.assertRaisesRegex(ValueError, expected_msg):
list(pkgutil.iter_modules("invalid_path"))

def test_unreadable_dir_on_syspath(self):
# issue7367 - walk_packages failed if unreadable dir on sys.path
package_name = "unreadable_package"
Expand Down Expand Up @@ -187,8 +231,7 @@ def test_walk_packages_raises_on_string_or_bytes_input(self):
with self.assertRaises((TypeError, ValueError)):
list(pkgutil.walk_packages(bytes_input))

# TODO: RUSTPYTHON
@unittest.expectedFailure
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_name_resolution(self):
import logging
import logging.handlers
Expand Down Expand Up @@ -280,6 +323,38 @@ def test_name_resolution(self):
with self.assertRaises(exc):
pkgutil.resolve_name(s)

def test_name_resolution_import_rebinding(self):
# The same data is also used for testing import in test_import and
# mock.patch in test_unittest.
path = os.path.join(os.path.dirname(__file__), 'test_import', 'data')
with uncache('package3', 'package3.submodule'), DirsOnSysPath(path):
self.assertEqual(pkgutil.resolve_name('package3.submodule.attr'), 'submodule')
with uncache('package3', 'package3.submodule'), DirsOnSysPath(path):
self.assertEqual(pkgutil.resolve_name('package3.submodule:attr'), 'submodule')
with uncache('package3', 'package3.submodule'), DirsOnSysPath(path):
self.assertEqual(pkgutil.resolve_name('package3:submodule.attr'), 'rebound')
self.assertEqual(pkgutil.resolve_name('package3.submodule.attr'), 'submodule')
self.assertEqual(pkgutil.resolve_name('package3:submodule.attr'), 'rebound')
with uncache('package3', 'package3.submodule'), DirsOnSysPath(path):
self.assertEqual(pkgutil.resolve_name('package3:submodule.attr'), 'rebound')
self.assertEqual(pkgutil.resolve_name('package3.submodule:attr'), 'submodule')
self.assertEqual(pkgutil.resolve_name('package3:submodule.attr'), 'rebound')

def test_name_resolution_import_rebinding2(self):
path = os.path.join(os.path.dirname(__file__), 'test_import', 'data')
with uncache('package4', 'package4.submodule'), DirsOnSysPath(path):
self.assertEqual(pkgutil.resolve_name('package4.submodule.attr'), 'submodule')
with uncache('package4', 'package4.submodule'), DirsOnSysPath(path):
self.assertEqual(pkgutil.resolve_name('package4.submodule:attr'), 'submodule')
with uncache('package4', 'package4.submodule'), DirsOnSysPath(path):
self.assertEqual(pkgutil.resolve_name('package4:submodule.attr'), 'origin')
self.assertEqual(pkgutil.resolve_name('package4.submodule.attr'), 'submodule')
self.assertEqual(pkgutil.resolve_name('package4:submodule.attr'), 'submodule')
with uncache('package4', 'package4.submodule'), DirsOnSysPath(path):
self.assertEqual(pkgutil.resolve_name('package4:submodule.attr'), 'origin')
self.assertEqual(pkgutil.resolve_name('package4.submodule:attr'), 'submodule')
self.assertEqual(pkgutil.resolve_name('package4:submodule.attr'), 'submodule')


class PkgutilPEP302Tests(unittest.TestCase):

Expand Down Expand Up @@ -391,7 +466,7 @@ def test_iter_importers(self):
importers = list(iter_importers(fullname))
expected_importer = get_importer(pathitem)
for finder in importers:
spec = pkgutil._get_spec(finder, fullname)
spec = finder.find_spec(fullname)
loader = spec.loader
try:
loader = loader.loader
Expand All @@ -403,7 +478,7 @@ def test_iter_importers(self):
self.assertEqual(finder, expected_importer)
self.assertIsInstance(loader,
importlib.machinery.SourceFileLoader)
self.assertIsNone(pkgutil._get_spec(finder, pkgname))
self.assertIsNone(finder.find_spec(pkgname))

with self.assertRaises(ImportError):
list(iter_importers('invalid.module'))
Expand Down Expand Up @@ -448,7 +523,43 @@ def test_mixed_namespace(self):
del sys.modules['foo.bar']
del sys.modules['foo.baz']

# XXX: test .pkg files

def test_extend_path_argument_types(self):
pkgname = 'foo'
dirname_0 = self.create_init(pkgname)

# If the input path is not a list it is returned unchanged
self.assertEqual('notalist', pkgutil.extend_path('notalist', 'foo'))
self.assertEqual(('not', 'a', 'list'), pkgutil.extend_path(('not', 'a', 'list'), 'foo'))
self.assertEqual(123, pkgutil.extend_path(123, 'foo'))
self.assertEqual(None, pkgutil.extend_path(None, 'foo'))

# Cleanup
shutil.rmtree(dirname_0)
del sys.path[0]


def test_extend_path_pkg_files(self):
pkgname = 'foo'
dirname_0 = self.create_init(pkgname)

with open(os.path.join(dirname_0, 'bar.pkg'), 'w') as pkg_file:
pkg_file.write('\n'.join([
'baz',
'/foo/bar/baz',
'',
'#comment'
]))

extended_paths = pkgutil.extend_path(sys.path, 'bar')

self.assertEqual(extended_paths[:-2], sys.path)
self.assertEqual(extended_paths[-2], 'baz')
self.assertEqual(extended_paths[-1], '/foo/bar/baz')

# Cleanup
shutil.rmtree(dirname_0)
del sys.path[0]


class NestedNamespacePackageTest(unittest.TestCase):
Expand Down Expand Up @@ -491,36 +602,50 @@ def test_nested(self):
self.assertEqual(c, 1)
self.assertEqual(d, 2)


class ImportlibMigrationTests(unittest.TestCase):
# With full PEP 302 support in the standard import machinery, the
# PEP 302 emulation in this module is in the process of being
# deprecated in favour of importlib proper

@unittest.skipIf(__name__ == '__main__', 'not compatible with __main__')
@ignore_warnings(category=DeprecationWarning)
def test_get_loader_handles_missing_loader_attribute(self):
global __loader__
this_loader = __loader__
del __loader__
try:
with check_warnings() as w:
self.assertIsNotNone(pkgutil.get_loader(__name__))
self.assertEqual(len(w.warnings), 0)
self.assertIsNotNone(pkgutil.get_loader(__name__))
finally:
__loader__ = this_loader

@ignore_warnings(category=DeprecationWarning)
def test_get_loader_handles_missing_spec_attribute(self):
name = 'spam'
mod = type(sys)(name)
del mod.__spec__
with CleanImport(name):
sys.modules[name] = mod
loader = pkgutil.get_loader(name)
try:
sys.modules[name] = mod
loader = pkgutil.get_loader(name)
finally:
sys.modules.pop(name, None)
self.assertIsNone(loader)

@ignore_warnings(category=DeprecationWarning)
def test_get_loader_handles_spec_attribute_none(self):
name = 'spam'
mod = type(sys)(name)
mod.__spec__ = None
with CleanImport(name):
sys.modules[name] = mod
loader = pkgutil.get_loader(name)
try:
sys.modules[name] = mod
loader = pkgutil.get_loader(name)
finally:
sys.modules.pop(name, None)
self.assertIsNone(loader)

@ignore_warnings(category=DeprecationWarning)
def test_get_loader_None_in_sys_modules(self):
name = 'totally bogus'
sys.modules[name] = None
Expand All @@ -530,24 +655,38 @@ def test_get_loader_None_in_sys_modules(self):
del sys.modules[name]
self.assertIsNone(loader)

def test_get_loader_is_deprecated(self):
with check_warnings(
(r".*\bpkgutil.get_loader\b.*", DeprecationWarning),
):
res = pkgutil.get_loader("sys")
self.assertIsNotNone(res)

def test_find_loader_is_deprecated(self):
with check_warnings(
(r".*\bpkgutil.find_loader\b.*", DeprecationWarning),
):
res = pkgutil.find_loader("sys")
self.assertIsNotNone(res)

@ignore_warnings(category=DeprecationWarning)
def test_find_loader_missing_module(self):
name = 'totally bogus'
loader = pkgutil.find_loader(name)
self.assertIsNone(loader)

def test_find_loader_avoids_emulation(self):
with check_warnings() as w:
self.assertIsNotNone(pkgutil.find_loader("sys"))
self.assertIsNotNone(pkgutil.find_loader("os"))
self.assertIsNotNone(pkgutil.find_loader("test.support"))
self.assertEqual(len(w.warnings), 0)

def test_get_importer_avoids_emulation(self):
# We use an illegal path so *none* of the path hooks should fire
with check_warnings() as w:
self.assertIsNone(pkgutil.get_importer("*??"))
self.assertEqual(len(w.warnings), 0)

def test_issue44061(self):
try:
pkgutil.get_importer(Path("/home"))
except AttributeError:
self.fail("Unexpected AttributeError when calling get_importer")

def test_iter_importers_avoids_emulation(self):
with check_warnings() as w:
for importer in pkgutil.iter_importers(): pass
Expand Down