gh-65961: Raise `DeprecationWarning` when `__package__` differs from `__spec__.parent` (#97879)

Also remove `importlib.util.set_package()` which was already slated for removal.

Co-authored-by: Eric Snow <ericsnowcurrently@gmail.com>
This commit is contained in:
Brett Cannon 2022-10-05 15:00:45 -07:00 committed by GitHub
parent 2016bc54a2
commit c206e53bb7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 45 additions and 102 deletions

View File

@ -1378,15 +1378,6 @@ an :term:`importer`.
.. deprecated:: 3.4 .. deprecated:: 3.4
The import machinery takes care of this automatically. The import machinery takes care of this automatically.
.. decorator:: set_package
A :term:`decorator` for :meth:`importlib.abc.Loader.load_module` to set the
:attr:`__package__` attribute on the returned module. If :attr:`__package__`
is set and has a value other than ``None`` it will not be changed.
.. deprecated:: 3.4
The import machinery takes care of this automatically.
.. function:: spec_from_loader(name, loader, *, origin=None, is_package=None) .. function:: spec_from_loader(name, loader, *, origin=None, is_package=None)
A factory function for creating a :class:`~importlib.machinery.ModuleSpec` A factory function for creating a :class:`~importlib.machinery.ModuleSpec`

View File

@ -358,7 +358,6 @@ of what happens during the loading portion of import::
sys.modules[spec.name] = module sys.modules[spec.name] = module
elif not hasattr(spec.loader, 'exec_module'): elif not hasattr(spec.loader, 'exec_module'):
module = spec.loader.load_module(spec.name) module = spec.loader.load_module(spec.name)
# Set __loader__ and __package__ if missing.
else: else:
sys.modules[spec.name] = module sys.modules[spec.name] = module
try: try:
@ -539,6 +538,10 @@ The import machinery fills in these attributes on each module object
during loading, based on the module's spec, before the loader executes during loading, based on the module's spec, before the loader executes
the module. the module.
It is **strongly** recommended that you rely on :attr:`__spec__` and
its attributes instead of any of the other individual attributes
listed below.
.. attribute:: __name__ .. attribute:: __name__
The ``__name__`` attribute must be set to the fully qualified name of The ``__name__`` attribute must be set to the fully qualified name of
@ -552,9 +555,12 @@ the module.
for introspection, but can be used for additional loader-specific for introspection, but can be used for additional loader-specific
functionality, for example getting data associated with a loader. functionality, for example getting data associated with a loader.
It is **strongly** recommended that you rely on :attr:`__spec__`
instead instead of this attribute.
.. attribute:: __package__ .. attribute:: __package__
The module's ``__package__`` attribute must be set. Its value must The module's ``__package__`` attribute may be set. Its value must
be a string, but it can be the same value as its ``__name__``. When be a string, but it can be the same value as its ``__name__``. When
the module is a package, its ``__package__`` value should be set to the module is a package, its ``__package__`` value should be set to
its ``__name__``. When the module is not a package, ``__package__`` its ``__name__``. When the module is not a package, ``__package__``
@ -562,14 +568,23 @@ the module.
submodules, to the parent package's name. See :pep:`366` for further submodules, to the parent package's name. See :pep:`366` for further
details. details.
This attribute is used instead of ``__name__`` to calculate explicit It is **strongly** recommended that you rely on :attr:`__spec__`
relative imports for main modules, as defined in :pep:`366`. It is instead instead of this attribute.
expected to have the same value as ``__spec__.parent``.
.. versionchanged:: 3.6 .. versionchanged:: 3.6
The value of ``__package__`` is expected to be the same as The value of ``__package__`` is expected to be the same as
``__spec__.parent``. ``__spec__.parent``.
.. versionchanged:: 3.10
:exc:`ImportWarning` is raised if import falls back to
``__package__`` instead of
:attr:`~importlib.machinery.ModuleSpec.parent`.
.. versionchanged:: 3.12
Raise :exc:`DeprecationWarning` instead of :exc:`ImportWarning`
when falling back to ``__package__``.
.. attribute:: __spec__ .. attribute:: __spec__
The ``__spec__`` attribute must be set to the module spec that was The ``__spec__`` attribute must be set to the module spec that was
@ -578,7 +593,7 @@ the module.
interpreter startup <programs>`. The one exception is ``__main__``, interpreter startup <programs>`. The one exception is ``__main__``,
where ``__spec__`` is :ref:`set to None in some cases <main_spec>`. where ``__spec__`` is :ref:`set to None in some cases <main_spec>`.
When ``__package__`` is not defined, ``__spec__.parent`` is used as When ``__spec__.parent`` is not set, ``__package__`` is used as
a fallback. a fallback.
.. versionadded:: 3.4 .. versionadded:: 3.4
@ -623,6 +638,9 @@ the module.
if a loader can load from a cached module but otherwise does not load if a loader can load from a cached module but otherwise does not load
from a file, that atypical scenario may be appropriate. from a file, that atypical scenario may be appropriate.
It is **strongly** recommended that you rely on :attr:`__spec__`
instead instead of ``__cached__``.
.. _package-path-rules: .. _package-path-rules:
module.__path__ module.__path__

View File

@ -215,6 +215,11 @@ Deprecated
may be removed in a future version of Python. Use the single-arg versions may be removed in a future version of Python. Use the single-arg versions
of these functions instead. (Contributed by Ofey Chan in :gh:`89874`.) of these functions instead. (Contributed by Ofey Chan in :gh:`89874`.)
* :exc:`DeprecationWarning` is now raised when ``__package__`` on a
module differs from ``__spec__.parent`` (previously it was
:exc:`ImportWarning`).
(Contributed by Brett Cannon in :gh:`65961`.)
Pending Removal in Python 3.13 Pending Removal in Python 3.13
------------------------------ ------------------------------
@ -275,6 +280,9 @@ Pending Removal in Python 3.14
* Creating :c:data:`immutable types <Py_TPFLAGS_IMMUTABLETYPE>` with mutable * Creating :c:data:`immutable types <Py_TPFLAGS_IMMUTABLETYPE>` with mutable
bases using the C API. bases using the C API.
* ``__package__`` will cease to be set or taken into consideration by
the import system (:gh:`97879`).
Pending Removal in Future Versions Pending Removal in Future Versions
---------------------------------- ----------------------------------
@ -432,6 +440,10 @@ Removed
* References to, and support for ``module_repr()`` has been eradicated. * References to, and support for ``module_repr()`` has been eradicated.
* ``importlib.util.set_package`` has been removed.
(Contributed by Brett Cannon in :gh:`65961`.)
Porting to Python 3.12 Porting to Python 3.12
====================== ======================

View File

@ -1228,7 +1228,7 @@ def _calc___package__(globals):
if spec is not None and package != spec.parent: if spec is not None and package != spec.parent:
_warnings.warn("__package__ != __spec__.parent " _warnings.warn("__package__ != __spec__.parent "
f"({package!r} != {spec.parent!r})", f"({package!r} != {spec.parent!r})",
ImportWarning, stacklevel=3) DeprecationWarning, stacklevel=3)
return package return package
elif spec is not None: elif spec is not None:
return spec.parent return spec.parent

View File

@ -141,26 +141,6 @@ def _module_to_load(name):
module.__initializing__ = False module.__initializing__ = False
def set_package(fxn):
"""Set __package__ on the returned module.
This function is deprecated.
"""
@functools.wraps(fxn)
def set_package_wrapper(*args, **kwargs):
warnings.warn('The import system now takes care of this automatically; '
'this decorator is slated for removal in Python 3.12',
DeprecationWarning, stacklevel=2)
module = fxn(*args, **kwargs)
if getattr(module, '__package__', None) is None:
module.__package__ = module.__name__
if not hasattr(module, '__path__'):
module.__package__ = module.__package__.rpartition('.')[0]
return module
return set_package_wrapper
def set_loader(fxn): def set_loader(fxn):
"""Set __loader__ on the returned module. """Set __loader__ on the returned module.

View File

@ -74,8 +74,8 @@ class Using__package__:
self.assertEqual(module.__name__, 'pkg') self.assertEqual(module.__name__, 'pkg')
def test_warn_when_package_and_spec_disagree(self): def test_warn_when_package_and_spec_disagree(self):
# Raise an ImportWarning if __package__ != __spec__.parent. # Raise a DeprecationWarning if __package__ != __spec__.parent.
with self.assertWarns(ImportWarning): with self.assertWarns(DeprecationWarning):
self.import_module({'__package__': 'pkg.fake', self.import_module({'__package__': 'pkg.fake',
'__spec__': FakeSpec('pkg.fakefake')}) '__spec__': FakeSpec('pkg.fakefake')})

View File

@ -252,69 +252,6 @@ class ModuleForLoaderTests:
) = util.test_both(ModuleForLoaderTests, util=importlib_util) ) = util.test_both(ModuleForLoaderTests, util=importlib_util)
class SetPackageTests:
"""Tests for importlib.util.set_package."""
def verify(self, module, expect):
"""Verify the module has the expected value for __package__ after
passing through set_package."""
fxn = lambda: module
wrapped = self.util.set_package(fxn)
with warnings.catch_warnings():
warnings.simplefilter('ignore', DeprecationWarning)
wrapped()
self.assertTrue(hasattr(module, '__package__'))
self.assertEqual(expect, module.__package__)
def test_top_level(self):
# __package__ should be set to the empty string if a top-level module.
# Implicitly tests when package is set to None.
module = types.ModuleType('module')
module.__package__ = None
self.verify(module, '')
def test_package(self):
# Test setting __package__ for a package.
module = types.ModuleType('pkg')
module.__path__ = ['<path>']
module.__package__ = None
self.verify(module, 'pkg')
def test_submodule(self):
# Test __package__ for a module in a package.
module = types.ModuleType('pkg.mod')
module.__package__ = None
self.verify(module, 'pkg')
def test_setting_if_missing(self):
# __package__ should be set if it is missing.
module = types.ModuleType('mod')
if hasattr(module, '__package__'):
delattr(module, '__package__')
self.verify(module, '')
def test_leaving_alone(self):
# If __package__ is set and not None then leave it alone.
for value in (True, False):
module = types.ModuleType('mod')
module.__package__ = value
self.verify(module, value)
def test_decorator_attrs(self):
def fxn(module): pass
with warnings.catch_warnings():
warnings.simplefilter('ignore', DeprecationWarning)
wrapped = self.util.set_package(fxn)
self.assertEqual(wrapped.__name__, fxn.__name__)
self.assertEqual(wrapped.__qualname__, fxn.__qualname__)
(Frozen_SetPackageTests,
Source_SetPackageTests
) = util.test_both(SetPackageTests, util=importlib_util)
class SetLoaderTests: class SetLoaderTests:
"""Tests importlib.util.set_loader().""" """Tests importlib.util.set_loader()."""

View File

@ -0,0 +1,5 @@
When ``__package__`` is different than ``__spec__.parent``, raise a
``DeprecationWarning`` instead of ``ImportWarning``.
Also remove ``importlib.util.set_package()`` which was scheduled for
removal.

View File

@ -1573,7 +1573,7 @@ resolve_name(PyThreadState *tstate, PyObject *name, PyObject *globals, int level
goto error; goto error;
} }
else if (equal == 0) { else if (equal == 0) {
if (PyErr_WarnEx(PyExc_ImportWarning, if (PyErr_WarnEx(PyExc_DeprecationWarning,
"__package__ != __spec__.parent", 1) < 0) { "__package__ != __spec__.parent", 1) < 0) {
goto error; goto error;
} }