Skip to content

Commit a23d30f

Browse files
authored
bpo-32303 - Consistency fixes for namespace loaders (GH-5481) (#5503)
* Make sure ``__spec__.loader`` matches ``__loader__`` for namespace packages. * Make sure ``__spec__.origin` matches ``__file__`` for namespace packages. https://bugs.python.org/issue32303 https://bugs.python.org/issue32305 (cherry picked from commit bbbcf86) Co-authored-by: Barry Warsaw <[email protected]>
1 parent 2b5937e commit a23d30f

File tree

10 files changed

+1476
-1434
lines changed

10 files changed

+1476
-1434
lines changed

Doc/library/importlib.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -1291,7 +1291,7 @@ find and load modules.
12911291
Name of the place from which the module is loaded, e.g. "builtin" for
12921292
built-in modules and the filename for modules loaded from source.
12931293
Normally "origin" should be set, but it may be ``None`` (the default)
1294-
which indicates it is unspecified.
1294+
which indicates it is unspecified (e.g. for namespace packages).
12951295

12961296
.. attribute:: submodule_search_locations
12971297

Lib/importlib/_bootstrap.py

+12
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,18 @@ def _init_module_attrs(spec, module, *, override=False):
522522

523523
loader = _NamespaceLoader.__new__(_NamespaceLoader)
524524
loader._path = spec.submodule_search_locations
525+
spec.loader = loader
526+
# While the docs say that module.__file__ is not set for
527+
# built-in modules, and the code below will avoid setting it if
528+
# spec.has_location is false, this is incorrect for namespace
529+
# packages. Namespace packages have no location, but their
530+
# __spec__.origin is None, and thus their module.__file__
531+
# should also be None for consistency. While a bit of a hack,
532+
# this is the best place to ensure this consistency.
533+
#
534+
# See # https://docs.python.org/3/library/importlib.html#importlib.abc.Loader.load_module
535+
# and bpo-32305
536+
module.__file__ = None
525537
try:
526538
module.__loader__ = loader
527539
except AttributeError:

Lib/importlib/_bootstrap_external.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1276,9 +1276,9 @@ def find_spec(cls, fullname, path=None, target=None):
12761276
elif spec.loader is None:
12771277
namespace_path = spec.submodule_search_locations
12781278
if namespace_path:
1279-
# We found at least one namespace path. Return a
1280-
# spec which can create the namespace package.
1281-
spec.origin = 'namespace'
1279+
# We found at least one namespace path. Return a spec which
1280+
# can create the namespace package.
1281+
spec.origin = None
12821282
spec.submodule_search_locations = _NamespacePath(fullname, namespace_path, cls._get_spec)
12831283
return spec
12841284
else:

Lib/importlib/resources.py

+12-3
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,19 @@ def _get_resource_reader(
6666
return None
6767

6868

69+
def _check_location(package):
70+
if package.__spec__.origin is None or not package.__spec__.has_location:
71+
raise FileNotFoundError(f'Package has no location {package!r}')
72+
73+
6974
def open_binary(package: Package, resource: Resource) -> BinaryIO:
7075
"""Return a file-like object opened for binary reading of the resource."""
7176
resource = _normalize_path(resource)
7277
package = _get_package(package)
7378
reader = _get_resource_reader(package)
7479
if reader is not None:
7580
return reader.open_resource(resource)
81+
_check_location(package)
7682
absolute_package_path = os.path.abspath(package.__spec__.origin)
7783
package_path = os.path.dirname(absolute_package_path)
7884
full_path = os.path.join(package_path, resource)
@@ -106,6 +112,7 @@ def open_text(package: Package,
106112
reader = _get_resource_reader(package)
107113
if reader is not None:
108114
return TextIOWrapper(reader.open_resource(resource), encoding, errors)
115+
_check_location(package)
109116
absolute_package_path = os.path.abspath(package.__spec__.origin)
110117
package_path = os.path.dirname(absolute_package_path)
111118
full_path = os.path.join(package_path, resource)
@@ -172,6 +179,8 @@ def path(package: Package, resource: Resource) -> Iterator[Path]:
172179
return
173180
except FileNotFoundError:
174181
pass
182+
else:
183+
_check_location(package)
175184
# Fall-through for both the lack of resource_path() *and* if
176185
# resource_path() raises FileNotFoundError.
177186
package_directory = Path(package.__spec__.origin).parent
@@ -232,9 +241,9 @@ def contents(package: Package) -> Iterator[str]:
232241
yield from reader.contents()
233242
return
234243
# Is the package a namespace package? By definition, namespace packages
235-
# cannot have resources.
236-
if (package.__spec__.origin == 'namespace' and
237-
not package.__spec__.has_location):
244+
# cannot have resources. We could use _check_location() and catch the
245+
# exception, but that's extra work, so just inline the check.
246+
if package.__spec__.origin is None or not package.__spec__.has_location:
238247
return []
239248
package_directory = Path(package.__spec__.origin).parent
240249
yield from os.listdir(str(package_directory))

Lib/test/test_importlib/test_api.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ def test_reload_namespace_changed(self):
305305
expected = {'__name__': name,
306306
'__package__': name,
307307
'__doc__': None,
308+
'__file__': None,
308309
}
309310
os.mkdir(name)
310311
with open(bad_path, 'w') as init_file:
@@ -316,8 +317,9 @@ def test_reload_namespace_changed(self):
316317
spec = ns.pop('__spec__')
317318
ns.pop('__builtins__', None) # An implementation detail.
318319
self.assertEqual(spec.name, name)
319-
self.assertIs(spec.loader, None)
320-
self.assertIsNot(loader, None)
320+
self.assertIsNotNone(spec.loader)
321+
self.assertIsNotNone(loader)
322+
self.assertEqual(spec.loader, loader)
321323
self.assertEqual(set(path),
322324
set([os.path.dirname(bad_path)]))
323325
with self.assertRaises(AttributeError):

Lib/test/test_importlib/test_namespace_pkgs.py

+16
Original file line numberDiff line numberDiff line change
@@ -317,5 +317,21 @@ def test_dynamic_path(self):
317317
self.assertEqual(foo.two.attr, 'portion2 foo two')
318318

319319

320+
class LoaderTests(NamespacePackageTest):
321+
paths = ['portion1']
322+
323+
def test_namespace_loader_consistency(self):
324+
# bpo-32303
325+
import foo
326+
self.assertEqual(foo.__loader__, foo.__spec__.loader)
327+
self.assertIsNotNone(foo.__loader__)
328+
329+
def test_namespace_origin_consistency(self):
330+
# bpo-32305
331+
import foo
332+
self.assertIsNone(foo.__spec__.origin)
333+
self.assertIsNone(foo.__file__)
334+
335+
320336
if __name__ == "__main__":
321337
unittest.main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Make sure ``__spec__.loader`` matches ``__loader__`` for namespace packages.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
For namespace packages, ensure that both ``__file__`` and
2+
``__spec__.origin`` are set to None.

0 commit comments

Comments
 (0)