Skip to content

Commit b649af9

Browse files
authored
Add Final to typing_extensions (#583)
This is a runtime counterpart of an experimental feature added to mypy in python/mypy#5522 This implementation just mimics the behaviour of `ClassVar` on all Python/`typing` versions, which is probably the most reasonable thing to do.
1 parent cba1c98 commit b649af9

File tree

4 files changed

+359
-5
lines changed

4 files changed

+359
-5
lines changed

typing_extensions/src_py2/test_typing_extensions.py

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import subprocess
88
from unittest import TestCase, main, skipUnless
99

10-
from typing_extensions import NoReturn, ClassVar
10+
from typing_extensions import NoReturn, ClassVar, Final
1111
from typing_extensions import ContextManager, Counter, Deque, DefaultDict
1212
from typing_extensions import NewType, overload, Protocol, runtime
1313
import typing
@@ -117,6 +117,46 @@ def test_no_isinstance(self):
117117
issubclass(int, ClassVar)
118118

119119

120+
class FinalTests(BaseTestCase):
121+
122+
def test_basics(self):
123+
with self.assertRaises(TypeError):
124+
Final[1]
125+
with self.assertRaises(TypeError):
126+
Final[int, str]
127+
with self.assertRaises(TypeError):
128+
Final[int][str]
129+
130+
def test_repr(self):
131+
self.assertEqual(repr(Final), 'typing_extensions.Final')
132+
cv = Final[int]
133+
self.assertEqual(repr(cv), 'typing_extensions.Final[int]')
134+
cv = Final[Employee]
135+
self.assertEqual(repr(cv), 'typing_extensions.Final[%s.Employee]' % __name__)
136+
137+
def test_cannot_subclass(self):
138+
with self.assertRaises(TypeError):
139+
class C(type(Final)):
140+
pass
141+
with self.assertRaises(TypeError):
142+
class C(type(Final[int])):
143+
pass
144+
145+
def test_cannot_init(self):
146+
with self.assertRaises(TypeError):
147+
Final()
148+
with self.assertRaises(TypeError):
149+
type(Final)()
150+
with self.assertRaises(TypeError):
151+
type(Final[typing.Optional[int]])()
152+
153+
def test_no_isinstance(self):
154+
with self.assertRaises(TypeError):
155+
isinstance(1, Final[int])
156+
with self.assertRaises(TypeError):
157+
issubclass(int, Final)
158+
159+
120160
class CollectionsAbcTests(BaseTestCase):
121161

122162
def test_isinstance_collections(self):
@@ -734,7 +774,7 @@ def test_typing_extensions_includes_standard(self):
734774
self.assertIn('TYPE_CHECKING', a)
735775

736776
def test_typing_extensions_defers_when_possible(self):
737-
exclude = {'overload', 'Text', 'TYPE_CHECKING'}
777+
exclude = {'overload', 'Text', 'TYPE_CHECKING', 'Final'}
738778
for item in typing_extensions.__all__:
739779
if item not in exclude and hasattr(typing, item):
740780
self.assertIs(

typing_extensions/src_py2/typing_extensions.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
__all__ = [
1616
# Super-special typing primitives.
1717
'ClassVar',
18+
'Final',
1819
'Protocol',
1920
'Type',
2021

@@ -25,6 +26,7 @@
2526
'DefaultDict',
2627

2728
# One-off things.
29+
'final',
2830
'NewType',
2931
'overload',
3032
'runtime',
@@ -106,6 +108,94 @@ def _gorg(cls):
106108
return cls
107109

108110

111+
class FinalMeta(TypingMeta):
112+
"""Metaclass for _Final"""
113+
114+
def __new__(cls, name, bases, namespace):
115+
cls.assert_no_subclassing(bases)
116+
self = super(FinalMeta, cls).__new__(cls, name, bases, namespace)
117+
return self
118+
119+
120+
class _Final(typing._FinalTypingBase):
121+
"""A special typing construct to indicate that a name
122+
cannot be re-assigned or overridden in a subclass.
123+
For example:
124+
125+
MAX_SIZE: Final = 9000
126+
MAX_SIZE += 1 # Error reported by type checker
127+
128+
class Connection:
129+
TIMEOUT: Final[int] = 10
130+
class FastConnector(Connection):
131+
TIMEOUT = 1 # Error reported by type checker
132+
133+
There is no runtime checking of these properties.
134+
"""
135+
136+
__metaclass__ = FinalMeta
137+
__slots__ = ('__type__',)
138+
139+
def __init__(self, tp=None, **kwds):
140+
self.__type__ = tp
141+
142+
def __getitem__(self, item):
143+
cls = type(self)
144+
if self.__type__ is None:
145+
return cls(typing._type_check(item,
146+
'{} accepts only single type.'.format(cls.__name__[1:])),
147+
_root=True)
148+
raise TypeError('{} cannot be further subscripted'
149+
.format(cls.__name__[1:]))
150+
151+
def _eval_type(self, globalns, localns):
152+
new_tp = typing._eval_type(self.__type__, globalns, localns)
153+
if new_tp == self.__type__:
154+
return self
155+
return type(self)(new_tp, _root=True)
156+
157+
def __repr__(self):
158+
r = super(_Final, self).__repr__()
159+
if self.__type__ is not None:
160+
r += '[{}]'.format(typing._type_repr(self.__type__))
161+
return r
162+
163+
def __hash__(self):
164+
return hash((type(self).__name__, self.__type__))
165+
166+
def __eq__(self, other):
167+
if not isinstance(other, _Final):
168+
return NotImplemented
169+
if self.__type__ is not None:
170+
return self.__type__ == other.__type__
171+
return self is other
172+
173+
Final = _Final(_root=True)
174+
175+
176+
def final(f):
177+
"""This decorator can be used to indicate to type checkers that
178+
the decorated method cannot be overridden, and decorated class
179+
cannot be subclassed. For example:
180+
181+
class Base:
182+
@final
183+
def done(self) -> None:
184+
...
185+
class Sub(Base):
186+
def done(self) -> None: # Error reported by type checker
187+
...
188+
@final
189+
class Leaf:
190+
...
191+
class Other(Leaf): # Error reported by type checker
192+
...
193+
194+
There is no runtime checking of these properties.
195+
"""
196+
return f
197+
198+
109199
class _ProtocolMeta(GenericMeta):
110200
"""Internal metaclass for Protocol.
111201

typing_extensions/src_py3/test_typing_extensions.py

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from typing import Generic
1313
from typing import get_type_hints
1414
from typing import no_type_check
15-
from typing_extensions import NoReturn, ClassVar, Type, NewType
15+
from typing_extensions import NoReturn, ClassVar, Final, Type, NewType
1616
try:
1717
from typing_extensions import Protocol, runtime
1818
except ImportError:
@@ -171,6 +171,47 @@ def test_no_isinstance(self):
171171
issubclass(int, ClassVar)
172172

173173

174+
class FinalTests(BaseTestCase):
175+
176+
def test_basics(self):
177+
with self.assertRaises(TypeError):
178+
Final[1]
179+
with self.assertRaises(TypeError):
180+
Final[int, str]
181+
with self.assertRaises(TypeError):
182+
Final[int][str]
183+
184+
def test_repr(self):
185+
self.assertEqual(repr(Final), 'typing_extensions.Final')
186+
cv = Final[int]
187+
self.assertEqual(repr(cv), 'typing_extensions.Final[int]')
188+
cv = Final[Employee]
189+
self.assertEqual(repr(cv), 'typing_extensions.Final[%s.Employee]' % __name__)
190+
191+
@skipUnless(SUBCLASS_CHECK_FORBIDDEN, "Behavior added in typing 3.5.3")
192+
def test_cannot_subclass(self):
193+
with self.assertRaises(TypeError):
194+
class C(type(Final)):
195+
pass
196+
with self.assertRaises(TypeError):
197+
class C(type(Final[int])):
198+
pass
199+
200+
def test_cannot_init(self):
201+
with self.assertRaises(TypeError):
202+
Final()
203+
with self.assertRaises(TypeError):
204+
type(Final)()
205+
with self.assertRaises(TypeError):
206+
type(Final[Optional[int]])()
207+
208+
def test_no_isinstance(self):
209+
with self.assertRaises(TypeError):
210+
isinstance(1, Final[int])
211+
with self.assertRaises(TypeError):
212+
issubclass(int, Final)
213+
214+
174215
class OverloadTests(BaseTestCase):
175216

176217
def test_overload_fails(self):
@@ -262,6 +303,9 @@ class CSub(B):
262303
class G(Generic[T]):
263304
lst: ClassVar[List[T]] = []
264305
306+
class Loop:
307+
attr: Final['Loop']
308+
265309
class NoneAndForward:
266310
parent: 'NoneAndForward'
267311
meaning: None
@@ -291,7 +335,7 @@ async def g_with(am: AsyncContextManager[int]):
291335
# fake names for the sake of static analysis
292336
ann_module = ann_module2 = ann_module3 = None
293337
A = B = CSub = G = CoolEmployee = CoolEmployeeWithDefault = object
294-
XMeth = XRepr = NoneAndForward = object
338+
XMeth = XRepr = NoneAndForward = Loop = object
295339

296340
gth = get_type_hints
297341

@@ -346,6 +390,11 @@ def test_get_type_hints_ClassVar(self):
346390
'x': ClassVar[Optional[B]]})
347391
self.assertEqual(gth(G), {'lst': ClassVar[List[T]]})
348392

393+
@skipUnless(PY36, 'Python 3.6 required')
394+
def test_final_forward_ref(self):
395+
self.assertEqual(gth(Loop, globals())['attr'], Final[Loop])
396+
self.assertNotEqual(gth(Loop, globals())['attr'], Final[int])
397+
self.assertNotEqual(gth(Loop, globals())['attr'], Final)
349398

350399
class CollectionsAbcTests(BaseTestCase):
351400

@@ -1253,7 +1302,7 @@ def test_typing_extensions_includes_standard(self):
12531302
self.assertIn('runtime', a)
12541303

12551304
def test_typing_extensions_defers_when_possible(self):
1256-
exclude = {'overload', 'Text', 'TYPE_CHECKING'}
1305+
exclude = {'overload', 'Text', 'TYPE_CHECKING', 'Final'}
12571306
for item in typing_extensions.__all__:
12581307
if item not in exclude and hasattr(typing, item):
12591308
self.assertIs(

0 commit comments

Comments
 (0)