Unverified Kaydet (Commit) a23d30f6 authored tarafından Barry Warsaw's avatar Barry Warsaw Kaydeden (comit) GitHub

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 bbbcf869)
Co-authored-by: 's avatarBarry Warsaw <barry@python.org>
üst 2b5937ec
...@@ -1291,7 +1291,7 @@ find and load modules. ...@@ -1291,7 +1291,7 @@ find and load modules.
Name of the place from which the module is loaded, e.g. "builtin" for Name of the place from which the module is loaded, e.g. "builtin" for
built-in modules and the filename for modules loaded from source. built-in modules and the filename for modules loaded from source.
Normally "origin" should be set, but it may be ``None`` (the default) Normally "origin" should be set, but it may be ``None`` (the default)
which indicates it is unspecified. which indicates it is unspecified (e.g. for namespace packages).
.. attribute:: submodule_search_locations .. attribute:: submodule_search_locations
......
...@@ -522,6 +522,18 @@ def _init_module_attrs(spec, module, *, override=False): ...@@ -522,6 +522,18 @@ def _init_module_attrs(spec, module, *, override=False):
loader = _NamespaceLoader.__new__(_NamespaceLoader) loader = _NamespaceLoader.__new__(_NamespaceLoader)
loader._path = spec.submodule_search_locations loader._path = spec.submodule_search_locations
spec.loader = loader
# While the docs say that module.__file__ is not set for
# built-in modules, and the code below will avoid setting it if
# spec.has_location is false, this is incorrect for namespace
# packages. Namespace packages have no location, but their
# __spec__.origin is None, and thus their module.__file__
# should also be None for consistency. While a bit of a hack,
# this is the best place to ensure this consistency.
#
# See # https://docs.python.org/3/library/importlib.html#importlib.abc.Loader.load_module
# and bpo-32305
module.__file__ = None
try: try:
module.__loader__ = loader module.__loader__ = loader
except AttributeError: except AttributeError:
......
...@@ -1276,9 +1276,9 @@ class PathFinder: ...@@ -1276,9 +1276,9 @@ class PathFinder:
elif spec.loader is None: elif spec.loader is None:
namespace_path = spec.submodule_search_locations namespace_path = spec.submodule_search_locations
if namespace_path: if namespace_path:
# We found at least one namespace path. Return a # We found at least one namespace path. Return a spec which
# spec which can create the namespace package. # can create the namespace package.
spec.origin = 'namespace' spec.origin = None
spec.submodule_search_locations = _NamespacePath(fullname, namespace_path, cls._get_spec) spec.submodule_search_locations = _NamespacePath(fullname, namespace_path, cls._get_spec)
return spec return spec
else: else:
......
...@@ -66,6 +66,11 @@ def _get_resource_reader( ...@@ -66,6 +66,11 @@ def _get_resource_reader(
return None return None
def _check_location(package):
if package.__spec__.origin is None or not package.__spec__.has_location:
raise FileNotFoundError(f'Package has no location {package!r}')
def open_binary(package: Package, resource: Resource) -> BinaryIO: def open_binary(package: Package, resource: Resource) -> BinaryIO:
"""Return a file-like object opened for binary reading of the resource.""" """Return a file-like object opened for binary reading of the resource."""
resource = _normalize_path(resource) resource = _normalize_path(resource)
...@@ -73,6 +78,7 @@ def open_binary(package: Package, resource: Resource) -> BinaryIO: ...@@ -73,6 +78,7 @@ def open_binary(package: Package, resource: Resource) -> BinaryIO:
reader = _get_resource_reader(package) reader = _get_resource_reader(package)
if reader is not None: if reader is not None:
return reader.open_resource(resource) return reader.open_resource(resource)
_check_location(package)
absolute_package_path = os.path.abspath(package.__spec__.origin) absolute_package_path = os.path.abspath(package.__spec__.origin)
package_path = os.path.dirname(absolute_package_path) package_path = os.path.dirname(absolute_package_path)
full_path = os.path.join(package_path, resource) full_path = os.path.join(package_path, resource)
...@@ -106,6 +112,7 @@ def open_text(package: Package, ...@@ -106,6 +112,7 @@ def open_text(package: Package,
reader = _get_resource_reader(package) reader = _get_resource_reader(package)
if reader is not None: if reader is not None:
return TextIOWrapper(reader.open_resource(resource), encoding, errors) return TextIOWrapper(reader.open_resource(resource), encoding, errors)
_check_location(package)
absolute_package_path = os.path.abspath(package.__spec__.origin) absolute_package_path = os.path.abspath(package.__spec__.origin)
package_path = os.path.dirname(absolute_package_path) package_path = os.path.dirname(absolute_package_path)
full_path = os.path.join(package_path, resource) full_path = os.path.join(package_path, resource)
...@@ -172,6 +179,8 @@ def path(package: Package, resource: Resource) -> Iterator[Path]: ...@@ -172,6 +179,8 @@ def path(package: Package, resource: Resource) -> Iterator[Path]:
return return
except FileNotFoundError: except FileNotFoundError:
pass pass
else:
_check_location(package)
# Fall-through for both the lack of resource_path() *and* if # Fall-through for both the lack of resource_path() *and* if
# resource_path() raises FileNotFoundError. # resource_path() raises FileNotFoundError.
package_directory = Path(package.__spec__.origin).parent package_directory = Path(package.__spec__.origin).parent
...@@ -232,9 +241,9 @@ def contents(package: Package) -> Iterator[str]: ...@@ -232,9 +241,9 @@ def contents(package: Package) -> Iterator[str]:
yield from reader.contents() yield from reader.contents()
return return
# Is the package a namespace package? By definition, namespace packages # Is the package a namespace package? By definition, namespace packages
# cannot have resources. # cannot have resources. We could use _check_location() and catch the
if (package.__spec__.origin == 'namespace' and # exception, but that's extra work, so just inline the check.
not package.__spec__.has_location): if package.__spec__.origin is None or not package.__spec__.has_location:
return [] return []
package_directory = Path(package.__spec__.origin).parent package_directory = Path(package.__spec__.origin).parent
yield from os.listdir(str(package_directory)) yield from os.listdir(str(package_directory))
......
...@@ -305,6 +305,7 @@ class ReloadTests: ...@@ -305,6 +305,7 @@ class ReloadTests:
expected = {'__name__': name, expected = {'__name__': name,
'__package__': name, '__package__': name,
'__doc__': None, '__doc__': None,
'__file__': None,
} }
os.mkdir(name) os.mkdir(name)
with open(bad_path, 'w') as init_file: with open(bad_path, 'w') as init_file:
...@@ -316,8 +317,9 @@ class ReloadTests: ...@@ -316,8 +317,9 @@ class ReloadTests:
spec = ns.pop('__spec__') spec = ns.pop('__spec__')
ns.pop('__builtins__', None) # An implementation detail. ns.pop('__builtins__', None) # An implementation detail.
self.assertEqual(spec.name, name) self.assertEqual(spec.name, name)
self.assertIs(spec.loader, None) self.assertIsNotNone(spec.loader)
self.assertIsNot(loader, None) self.assertIsNotNone(loader)
self.assertEqual(spec.loader, loader)
self.assertEqual(set(path), self.assertEqual(set(path),
set([os.path.dirname(bad_path)])) set([os.path.dirname(bad_path)]))
with self.assertRaises(AttributeError): with self.assertRaises(AttributeError):
......
...@@ -317,5 +317,21 @@ class ReloadTests(NamespacePackageTest): ...@@ -317,5 +317,21 @@ class ReloadTests(NamespacePackageTest):
self.assertEqual(foo.two.attr, 'portion2 foo two') self.assertEqual(foo.two.attr, 'portion2 foo two')
class LoaderTests(NamespacePackageTest):
paths = ['portion1']
def test_namespace_loader_consistency(self):
# bpo-32303
import foo
self.assertEqual(foo.__loader__, foo.__spec__.loader)
self.assertIsNotNone(foo.__loader__)
def test_namespace_origin_consistency(self):
# bpo-32305
import foo
self.assertIsNone(foo.__spec__.origin)
self.assertIsNone(foo.__file__)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
Make sure ``__spec__.loader`` matches ``__loader__`` for namespace packages.
For namespace packages, ensure that both ``__file__`` and
``__spec__.origin`` are set to None.
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment