]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-118803: Make `ByteString` deprecations louder; remove `ByteString` from `typing...
authorAlex Waygood <Alex.Waygood@Gmail.com>
Thu, 18 Sep 2025 18:58:16 +0000 (19:58 +0100)
committerGitHub <noreply@github.com>
Thu, 18 Sep 2025 18:58:16 +0000 (18:58 +0000)
Doc/whatsnew/3.15.rst
Lib/_collections_abc.py
Lib/test/libregrtest/refleak.py
Lib/test/test_collections.py
Lib/test/test_typing.py
Lib/typing.py
Misc/NEWS.d/next/Library/2025-09-18-14-21-57.gh-issue-118803.2JPbto.rst [new file with mode: 0644]

index d0d7f6dce142ed97fffeb2849bd516a4d9152707..424e23ab3542457097653e433e8a8ec560a19932 100644 (file)
@@ -289,6 +289,25 @@ New modules
 Improved modules
 ================
 
+collections.abc
+---------------
+
+* :class:`collections.abc.ByteString` has been removed from
+  ``collections.abc.__all__``. :class:`!collections.abc.ByteString` has been
+  deprecated since Python 3.12, and is scheduled for removal in Python 3.17.
+
+* The following statements now cause ``DeprecationWarning``\ s to be emitted at
+  runtime:
+
+  * ``from collections.abc import ByteString``
+  * ``import collections.abc; collections.abc.ByteString``.
+
+  ``DeprecationWarning``\ s were already emitted if
+  :class:`collections.abc.ByteString` was subclassed or used as the second
+  argument to :func:`isinstance` or :func:`issubclass`, but warnings were not
+  previously emitted if it was merely imported or accessed from the
+  :mod:`!collections.abc` module.
+
 dbm
 ---
 
@@ -671,6 +690,21 @@ typing
   as it was incorrectly infered in runtime before.
   (Contributed by Nikita Sobolev in :gh:`137191`.)
 
+* :class:`typing.ByteString` has been removed from ``typing.__all__``.
+  :class:`!typing.ByteString` has been deprecated since Python 3.9, and is
+  scheduled for removal in Python 3.17.
+
+* The following statements now cause ``DeprecationWarning``\ s to be emitted at
+  runtime:
+
+  * ``from typing import ByteString``
+  * ``import typing; typing.ByteString``.
+
+  ``DeprecationWarning``\ s were already emitted if :class:`typing.ByteString`
+  was subclassed or used as the second argument to :func:`isinstance` or
+  :func:`issubclass`, but warnings were not previously emitted if it was merely
+  imported or accessed from the :mod:`!typing` module.
+
 
 unicodedata
 -----------
index 241d40d57409aeb6deb604a2d798f8d347381e92..60b471317ce97ca015db498090c1ab3120d15bb3 100644 (file)
@@ -49,7 +49,7 @@ __all__ = ["Awaitable", "Coroutine",
            "Mapping", "MutableMapping",
            "MappingView", "KeysView", "ItemsView", "ValuesView",
            "Sequence", "MutableSequence",
-           "ByteString", "Buffer",
+           "Buffer",
            ]
 
 # This module has been renamed from collections.abc to _collections_abc to
@@ -1165,3 +1165,13 @@ class MutableSequence(Sequence):
 
 MutableSequence.register(list)
 MutableSequence.register(bytearray)
+
+_deprecated_ByteString = globals().pop("ByteString")
+
+def __getattr__(attr):
+    if attr == "ByteString":
+        import warnings
+        warnings._deprecated("collections.abc.ByteString", remove=(3, 17))
+        globals()["ByteString"] = _deprecated_ByteString
+        return _deprecated_ByteString
+    raise AttributeError(f"module 'collections.abc' has no attribute {attr!r}")
index 5c78515506df59a12f031cb87f926ff427d5ee5d..e7da17e500ead96dda434e450fe49a7750f517c0 100644 (file)
@@ -93,6 +93,13 @@ def runtest_refleak(test_name, test_func,
         for obj in abc.__subclasses__() + [abc]:
             abcs[obj] = _get_dump(obj)[0]
 
+    # `ByteString` is not included in `collections.abc.__all__`
+    with warnings.catch_warnings(action='ignore', category=DeprecationWarning):
+        ByteString = collections.abc.ByteString
+    # Mypy doesn't even think `ByteString` is a class, hence the `type: ignore`
+    for obj in ByteString.__subclasses__() + [ByteString]:  # type: ignore[attr-defined]
+        abcs[obj] = _get_dump(obj)[0]
+
     # bpo-31217: Integer pool to get a single integer object for the same
     # value. The pool is used to prevent false alarm when checking for memory
     # block leaks. Fill the pool with values in -1000..1000 which are the most
@@ -254,6 +261,8 @@ def dash_R_cleanup(fs, ps, pic, zdc, abcs, linecache_data):
 
     # Clear ABC registries, restoring previously saved ABC registries.
     abs_classes = [getattr(collections.abc, a) for a in collections.abc.__all__]
+    with warnings.catch_warnings(action='ignore', category=DeprecationWarning):
+        abs_classes.append(collections.abc.ByteString)
     abs_classes = filter(isabstract, abs_classes)
     for abc in abs_classes:
         for obj in abc.__subclasses__() + [abc]:
index 3dac736e0189b1c26f10d49b928a4c3e57af08b5..76995c52b1a3c2aaa7a894bbb096da42a4007c8d 100644 (file)
@@ -12,6 +12,7 @@ from itertools import product, chain, combinations
 import string
 import sys
 from test import support
+from test.support.import_helper import import_fresh_module
 import types
 import unittest
 
@@ -26,7 +27,7 @@ from collections.abc import Sized, Container, Callable, Collection
 from collections.abc import Set, MutableSet
 from collections.abc import Mapping, MutableMapping, KeysView, ItemsView, ValuesView
 from collections.abc import Sequence, MutableSequence
-from collections.abc import ByteString, Buffer
+from collections.abc import Buffer
 
 
 class TestUserObjects(unittest.TestCase):
@@ -1935,6 +1936,14 @@ class TestCollectionABCs(ABCTestCase):
                             nativeseq, seqseq, (letter, start, stop))
 
     def test_ByteString(self):
+        previous_sys_modules = sys.modules.copy()
+        self.addCleanup(sys.modules.update, previous_sys_modules)
+
+        for module in "collections", "_collections_abc", "collections.abc":
+            sys.modules.pop(module, None)
+
+        with self.assertWarns(DeprecationWarning):
+            from collections.abc import ByteString
         for sample in [bytes, bytearray]:
             with self.assertWarns(DeprecationWarning):
                 self.assertIsInstance(sample(), ByteString)
@@ -1956,6 +1965,14 @@ class TestCollectionABCs(ABCTestCase):
             # No metaclass conflict
             class Z(ByteString, Awaitable): pass
 
+    def test_ByteString_attribute_access(self):
+        collections_abc = import_fresh_module(
+            "collections.abc",
+            fresh=("collections", "_collections_abc")
+        )
+        with self.assertWarns(DeprecationWarning):
+            collections_abc.ByteString
+
     def test_Buffer(self):
         for sample in [bytes, bytearray, memoryview]:
             self.assertIsInstance(sample(b"x"), Buffer)
index 6ea1f2a35d615d76517e516ae47eb3f4cb312737..1c8b2978aa3f09431c647d715597727bd083cd65 100644 (file)
@@ -13,6 +13,7 @@ import os
 import pickle
 import re
 import sys
+import warnings
 from unittest import TestCase, main, skip
 from unittest.mock import patch
 from copy import copy, deepcopy
@@ -7500,14 +7501,23 @@ class CollectionsAbcTests(BaseTestCase):
         self.assertNotIsInstance((), typing.MutableSequence)
 
     def test_bytestring(self):
+        previous_typing_module = sys.modules.pop("typing", None)
+        self.addCleanup(sys.modules.__setitem__, "typing", previous_typing_module)
+
+        with self.assertWarns(DeprecationWarning):
+            from typing import ByteString
+        with self.assertWarns(DeprecationWarning):
+            self.assertIsInstance(b'', ByteString)
         with self.assertWarns(DeprecationWarning):
-            self.assertIsInstance(b'', typing.ByteString)
+            self.assertIsInstance(bytearray(b''), ByteString)
         with self.assertWarns(DeprecationWarning):
-            self.assertIsInstance(bytearray(b''), typing.ByteString)
+            self.assertIsSubclass(bytes, ByteString)
         with self.assertWarns(DeprecationWarning):
-            class Foo(typing.ByteString): ...
+            self.assertIsSubclass(bytearray, ByteString)
         with self.assertWarns(DeprecationWarning):
-            class Bar(typing.ByteString, typing.Awaitable): ...
+            class Foo(ByteString): ...
+        with self.assertWarns(DeprecationWarning):
+            class Bar(ByteString, typing.Awaitable): ...
 
     def test_list(self):
         self.assertIsSubclass(list, typing.List)
@@ -10455,6 +10465,10 @@ SpecialAttrsT = typing.TypeVar('SpecialAttrsT', int, float, complex)
 class SpecialAttrsTests(BaseTestCase):
 
     def test_special_attrs(self):
+        with warnings.catch_warnings(
+            action='ignore', category=DeprecationWarning
+        ):
+            typing_ByteString = typing.ByteString
         cls_to_check = {
             # ABC classes
             typing.AbstractSet: 'AbstractSet',
@@ -10463,7 +10477,7 @@ class SpecialAttrsTests(BaseTestCase):
             typing.AsyncIterable: 'AsyncIterable',
             typing.AsyncIterator: 'AsyncIterator',
             typing.Awaitable: 'Awaitable',
-            typing.ByteString: 'ByteString',
+            typing_ByteString: 'ByteString',
             typing.Callable: 'Callable',
             typing.ChainMap: 'ChainMap',
             typing.Collection: 'Collection',
@@ -10816,7 +10830,8 @@ class AllTests(BaseTestCase):
                 # there's a few types and metaclasses that aren't exported
                 not k.endswith(('Meta', '_contra', '_co')) and
                 not k.upper() == k and
-                # but export all things that have __module__ == 'typing'
+                k not in {"ByteString"} and
+                # but export all other things that have __module__ == 'typing'
                 getattr(v, '__module__', None) == typing.__name__
             )
         }
index a1bf2c9cb097470b29d530fd6f0407579d0f3a6c..df84e2c8764d9ca698fae1719df74450aec29f0f 100644 (file)
@@ -65,7 +65,6 @@ __all__ = [
 
     # ABCs (from collections.abc).
     'AbstractSet',  # collections.abc.Set.
-    'ByteString',
     'Container',
     'ContextManager',
     'Hashable',
@@ -1603,21 +1602,6 @@ class _SpecialGenericAlias(_NotIterable, _BaseGenericAlias, _root=True):
         return Union[left, self]
 
 
-class _DeprecatedGenericAlias(_SpecialGenericAlias, _root=True):
-    def __init__(
-        self, origin, nparams, *, removal_version, inst=True, name=None
-    ):
-        super().__init__(origin, nparams, inst=inst, name=name)
-        self._removal_version = removal_version
-
-    def __instancecheck__(self, inst):
-        import warnings
-        warnings._deprecated(
-            f"{self.__module__}.{self._name}", remove=self._removal_version
-        )
-        return super().__instancecheck__(inst)
-
-
 class _CallableGenericAlias(_NotIterable, _GenericAlias, _root=True):
     def __repr__(self):
         assert self._name == 'Callable'
@@ -2805,9 +2789,6 @@ Mapping = _alias(collections.abc.Mapping, 2)
 MutableMapping = _alias(collections.abc.MutableMapping, 2)
 Sequence = _alias(collections.abc.Sequence, 1)
 MutableSequence = _alias(collections.abc.MutableSequence, 1)
-ByteString = _DeprecatedGenericAlias(
-    collections.abc.ByteString, 0, removal_version=(3, 17)  # Not generic.
-)
 # Tuple accepts variable number of parameters.
 Tuple = _TupleType(tuple, -1, inst=False, name='Tuple')
 Tuple.__doc__ = \
@@ -3799,6 +3780,48 @@ def __getattr__(attr):
         )
         warnings.warn(depr_message, category=DeprecationWarning, stacklevel=2)
         obj = _collect_type_parameters
+    elif attr == "ByteString":
+        import warnings
+
+        warnings._deprecated(
+            "typing.ByteString",
+            message=(
+                "{name!r} and 'collections.abc.ByteString' are deprecated "
+                "and slated for removal in Python {remove}"
+            ),
+            remove=(3, 17)
+        )
+
+        class _DeprecatedGenericAlias(_SpecialGenericAlias, _root=True):
+            def __init__(
+                self, origin, nparams, *, removal_version, inst=True, name=None
+            ):
+                super().__init__(origin, nparams, inst=inst, name=name)
+                self._removal_version = removal_version
+
+            def __instancecheck__(self, inst):
+                import warnings
+                warnings._deprecated(
+                    f"{self.__module__}.{self._name}", remove=self._removal_version
+                )
+                return super().__instancecheck__(inst)
+
+            def __subclasscheck__(self, cls):
+                import warnings
+                warnings._deprecated(
+                    f"{self.__module__}.{self._name}", remove=self._removal_version
+                )
+                return super().__subclasscheck__(cls)
+
+        with warnings.catch_warnings(
+            action="ignore", category=DeprecationWarning
+        ):
+            # Not generic
+            ByteString = globals()["ByteString"] = _DeprecatedGenericAlias(
+                collections.abc.ByteString, 0, removal_version=(3, 17)
+            )
+
+        return ByteString
     else:
         raise AttributeError(f"module {__name__!r} has no attribute {attr!r}")
     globals()[attr] = obj
diff --git a/Misc/NEWS.d/next/Library/2025-09-18-14-21-57.gh-issue-118803.2JPbto.rst b/Misc/NEWS.d/next/Library/2025-09-18-14-21-57.gh-issue-118803.2JPbto.rst
new file mode 100644 (file)
index 0000000..a70fd0f
--- /dev/null
@@ -0,0 +1,15 @@
+:class:`collections.abc.ByteString` has been removed from
+``collections.abc.__all__``, and :class:`typing.ByteString` has been removed
+from ``typing.__all__``. The former has been deprecated since Python 3.12,
+and the latter has been deprecated since Python 3.9. Both classes are
+scheduled for removal in Python 3.17.
+
+Additionally, the following statements now cause ``DeprecationWarning``\ s to
+be emitted at runtime: ``from collections.abc import ByteString``, ``from
+typing import ByteString``, ``import collections.abc;
+collections.abc.ByteString`` and ``import typing; typing.ByteString``. Both
+classes already caused ``DeprecationWarning``\ s to be emitted if they were
+subclassed or used as the second argument to ``isinstance()`` or
+``issubclass()``, but they did not previously lead to
+``DeprecationWarning``\ s if they were merely imported or accessed from their
+respective modules.