Skip to content

Commit 9cfad2a

Browse files
Merge branch 'main' into typealiastype-writable-module
2 parents 21ae390 + f7dbe27 commit 9cfad2a

4 files changed

Lines changed: 222 additions & 70 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
- Make `typing_extensions.TypeAliasType`'s `__module__` attribute writable.
44
Backport of CPython PR
55
[#149172](https://github.com/python/cpython/pull/149172).
6+
- Fix setting of `__required_keys__` and `__optional_keys__` when inheriting
7+
keys with the same name.
68
- Fix incorrect behaviour on Python 3.9 and Python 3.10 that meant that
79
calling `isinstance` with `typing_extensions.Concatenate[...]` or
810
`typing_extensions.Unpack[...]` as the first argument could have a different

doc/index.rst

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ version from which features were backported; for example,
6565
``typing_extensions`` 3.10.0.0 was meant to reflect ``typing`` as of
6666
Python 3.10.0. During this period, no changelog was maintained.
6767

68+
In the documentation below, for each object added since version 4.0
69+
there is a note indicating the version in which it was added. Objects
70+
for which no such note is present have been present since before version 4.0.
71+
6872
Runtime use of types
6973
~~~~~~~~~~~~~~~~~~~~
7074

@@ -1071,18 +1075,17 @@ Capsule objects
10711075
Sentinel objects
10721076
~~~~~~~~~~~~~~~~
10731077

1074-
.. class:: Sentinel(name, repr=None)
1078+
.. class:: sentinel(name, /)
10751079

10761080
A type used to define sentinel values. The *name* argument should be the
10771081
name of the variable to which the return value shall be assigned.
10781082

1079-
If *repr* is provided, it will be used for the :meth:`~object.__repr__`
1080-
of the sentinel object. If not provided, ``"<name>"`` will be used.
1083+
Assigning attributes to a sentinel is deprecated.
10811084

10821085
Example::
10831086

1084-
>>> from typing_extensions import Sentinel, assert_type
1085-
>>> MISSING = Sentinel('MISSING')
1087+
>>> from typing_extensions import sentinel, assert_type
1088+
>>> MISSING = sentinel('MISSING')
10861089
>>> def func(arg: int | MISSING = MISSING) -> None:
10871090
... if arg is MISSING:
10881091
... assert_type(arg, MISSING)
@@ -1095,6 +1098,18 @@ Sentinel objects
10951098

10961099
See :pep:`661`
10971100

1101+
.. versionchanged:: 4.16.0
1102+
1103+
The implementation of this class has been updated to conform to
1104+
the accepted version of :pep:`661`.
1105+
1106+
Now supports pickle and will be reduced as a singleton.
1107+
Renamed from `Sentinel` to `sentinel`, `Sentinel` is deprecated.
1108+
Automatic `repr` string no longer has angle brackets.
1109+
`repr` parameter was deprecated.
1110+
`name` as a keyword is deprecated.
1111+
Subclassing and attribute assignment are deprecated.
1112+
10981113

10991114
Pure aliases
11001115
~~~~~~~~~~~~

src/test_typing_extensions.py

Lines changed: 90 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@
102102
reveal_type,
103103
runtime,
104104
runtime_checkable,
105+
sentinel,
105106
type_repr,
106107
)
107108

@@ -4642,6 +4643,47 @@ class ChildWithInlineAndOptional(Untotal, Inline):
46424643
class Wrong(*bases):
46434644
pass
46444645

4646+
def test_keys_inheritance_with_same_name(self):
4647+
class NotTotal(TypedDict, total=False):
4648+
a: int
4649+
4650+
class Total(NotTotal):
4651+
a: int
4652+
4653+
self.assertEqual(NotTotal.__required_keys__, frozenset())
4654+
self.assertEqual(NotTotal.__optional_keys__, frozenset(['a']))
4655+
self.assertEqual(Total.__required_keys__, frozenset(['a']))
4656+
self.assertEqual(Total.__optional_keys__, frozenset())
4657+
4658+
class Base(TypedDict):
4659+
a: NotRequired[int]
4660+
b: Required[int]
4661+
4662+
class Child(Base):
4663+
a: Required[int]
4664+
b: NotRequired[int]
4665+
4666+
self.assertEqual(Base.__required_keys__, frozenset(['b']))
4667+
self.assertEqual(Base.__optional_keys__, frozenset(['a']))
4668+
self.assertEqual(Child.__required_keys__, frozenset(['a']))
4669+
self.assertEqual(Child.__optional_keys__, frozenset(['b']))
4670+
4671+
def test_multiple_inheritance_with_same_key(self):
4672+
class Base1(TypedDict):
4673+
a: NotRequired[int]
4674+
4675+
class Base2(TypedDict):
4676+
a: Required[str]
4677+
4678+
class Child(Base1, Base2):
4679+
pass
4680+
4681+
# Last base wins
4682+
self.assertEqual(Child.__annotations__, {'a': Required[str]})
4683+
self.assertEqual(Child.__required_keys__, frozenset(['a']))
4684+
self.assertEqual(Child.__optional_keys__, frozenset())
4685+
4686+
46454687
def test_closed_values(self):
46464688
class Implicit(TypedDict): ...
46474689
class ExplicitTrue(TypedDict, closed=True): ...
@@ -9542,42 +9584,71 @@ def test_invalid_special_forms(self):
95429584

95439585

95449586
class TestSentinels(BaseTestCase):
9587+
SENTINEL = sentinel("TestSentinels.SENTINEL")
9588+
95459589
def test_sentinel_no_repr(self):
9546-
sentinel_no_repr = Sentinel('sentinel_no_repr')
9590+
sentinel_no_repr = sentinel('sentinel_no_repr')
95479591

9548-
self.assertEqual(sentinel_no_repr._name, 'sentinel_no_repr')
9549-
self.assertEqual(repr(sentinel_no_repr), '<sentinel_no_repr>')
9592+
self.assertEqual(sentinel_no_repr.__name__, 'sentinel_no_repr')
9593+
self.assertEqual(repr(sentinel_no_repr), 'sentinel_no_repr')
95509594

9551-
def test_sentinel_explicit_repr(self):
9552-
sentinel_explicit_repr = Sentinel('sentinel_explicit_repr', repr='explicit_repr')
9595+
def test_sentinel_deprecated_explicit_repr(self):
9596+
with self.assertWarnsRegex(DeprecationWarning, r"'repr' parameter is deprecated and will be removed"):
9597+
sentinel_explicit_repr = sentinel('sentinel_explicit_repr', repr='explicit_repr')
95539598

95549599
self.assertEqual(repr(sentinel_explicit_repr), 'explicit_repr')
95559600

95569601
@skipIf(sys.version_info < (3, 10), reason='New unions not available in 3.9')
95579602
def test_sentinel_type_expression_union(self):
9558-
sentinel = Sentinel('sentinel')
9603+
sentinel_type = sentinel('sentinel')
95599604

9560-
def func1(a: int | sentinel = sentinel): pass
9561-
def func2(a: sentinel | int = sentinel): pass
9605+
def func1(a: int | sentinel_type = sentinel_type): pass
9606+
def func2(a: sentinel_type | int = sentinel_type): pass
95629607

9563-
self.assertEqual(func1.__annotations__['a'], Union[int, sentinel])
9564-
self.assertEqual(func2.__annotations__['a'], Union[sentinel, int])
9608+
self.assertEqual(func1.__annotations__['a'], Union[int, sentinel_type])
9609+
self.assertEqual(func2.__annotations__['a'], Union[sentinel_type, int])
95659610

95669611
def test_sentinel_not_callable(self):
9567-
sentinel = Sentinel('sentinel')
9612+
sentinel_ = sentinel('sentinel')
95689613
with self.assertRaisesRegex(
95699614
TypeError,
9570-
"'Sentinel' object is not callable"
9615+
"'sentinel' object is not callable"
95719616
):
9617+
sentinel_()
9618+
9619+
def test_sentinel_copy_identity(self):
9620+
self.assertIs(self.SENTINEL, copy.copy(self.SENTINEL))
9621+
self.assertIs(self.SENTINEL, copy.deepcopy(self.SENTINEL))
9622+
9623+
anonymous_sentinel = sentinel("anonymous_sentinel")
9624+
self.assertIs(anonymous_sentinel, copy.copy(anonymous_sentinel))
9625+
self.assertIs(anonymous_sentinel, copy.deepcopy(anonymous_sentinel))
9626+
9627+
def test_sentinel_picklable_qualified(self):
9628+
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
9629+
self.assertIs(self.SENTINEL, pickle.loads(pickle.dumps(self.SENTINEL, protocol=proto)))
9630+
9631+
def test_sentinel_picklable_anonymous(self):
9632+
anonymous_sentinel = sentinel("anonymous_sentinel") # Anonymous sentinel can not be pickled
9633+
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
9634+
with self.assertRaisesRegex(
9635+
pickle.PicklingError,
9636+
r"attribute lookup anonymous_sentinel on \w+ failed|not found as \w+.anonymous_sentinel"
9637+
):
9638+
self.assertIs(anonymous_sentinel, pickle.loads(pickle.dumps(anonymous_sentinel, protocol=proto)))
9639+
9640+
def test_sentinel_deprecated(self):
9641+
with self.assertWarnsRegex(DeprecationWarning, r"Subclassing sentinel is deprecated"):
9642+
class SentinelSubclass(Sentinel):
9643+
pass
9644+
with self.assertRaisesRegex(TypeError, r"First parameter 'name' is required"):
95729645
sentinel()
95739646

9574-
def test_sentinel_not_picklable(self):
9575-
sentinel = Sentinel('sentinel')
9576-
with self.assertRaisesRegex(
9577-
TypeError,
9578-
"Cannot pickle 'Sentinel' object"
9579-
):
9580-
pickle.dumps(sentinel)
9647+
with self.assertWarnsRegex(DeprecationWarning, r"Passing 'name' as a keyword argument is deprecated"):
9648+
my_sentinel = Sentinel(name="my_sentinel")
9649+
with self.assertWarnsRegex(DeprecationWarning, r"Setting attribute 'foo' on sentinel objects is deprecated"):
9650+
my_sentinel.foo = "bar"
9651+
95819652

95829653
def load_tests(loader, tests, pattern):
95839654
import doctest

0 commit comments

Comments
 (0)