Skip to content

Commit

Permalink
Add Final to typing_extensions (#583)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ilevkivskyi authored Sep 13, 2018
1 parent d6631e8 commit c6c7dfd
Show file tree
Hide file tree
Showing 4 changed files with 359 additions and 5 deletions.
44 changes: 42 additions & 2 deletions typing_extensions/src_py2/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import subprocess
from unittest import TestCase, main, skipUnless

from typing_extensions import NoReturn, ClassVar
from typing_extensions import NoReturn, ClassVar, Final
from typing_extensions import ContextManager, Counter, Deque, DefaultDict
from typing_extensions import NewType, overload, Protocol, runtime
import typing
Expand Down Expand Up @@ -117,6 +117,46 @@ def test_no_isinstance(self):
issubclass(int, ClassVar)


class FinalTests(BaseTestCase):

def test_basics(self):
with self.assertRaises(TypeError):
Final[1]
with self.assertRaises(TypeError):
Final[int, str]
with self.assertRaises(TypeError):
Final[int][str]

def test_repr(self):
self.assertEqual(repr(Final), 'typing_extensions.Final')
cv = Final[int]
self.assertEqual(repr(cv), 'typing_extensions.Final[int]')
cv = Final[Employee]
self.assertEqual(repr(cv), 'typing_extensions.Final[%s.Employee]' % __name__)

def test_cannot_subclass(self):
with self.assertRaises(TypeError):
class C(type(Final)):
pass
with self.assertRaises(TypeError):
class C(type(Final[int])):
pass

def test_cannot_init(self):
with self.assertRaises(TypeError):
Final()
with self.assertRaises(TypeError):
type(Final)()
with self.assertRaises(TypeError):
type(Final[typing.Optional[int]])()

def test_no_isinstance(self):
with self.assertRaises(TypeError):
isinstance(1, Final[int])
with self.assertRaises(TypeError):
issubclass(int, Final)


class CollectionsAbcTests(BaseTestCase):

def test_isinstance_collections(self):
Expand Down Expand Up @@ -734,7 +774,7 @@ def test_typing_extensions_includes_standard(self):
self.assertIn('TYPE_CHECKING', a)

def test_typing_extensions_defers_when_possible(self):
exclude = {'overload', 'Text', 'TYPE_CHECKING'}
exclude = {'overload', 'Text', 'TYPE_CHECKING', 'Final'}
for item in typing_extensions.__all__:
if item not in exclude and hasattr(typing, item):
self.assertIs(
Expand Down
90 changes: 90 additions & 0 deletions typing_extensions/src_py2/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
__all__ = [
# Super-special typing primitives.
'ClassVar',
'Final',
'Protocol',
'Type',

Expand All @@ -25,6 +26,7 @@
'DefaultDict',

# One-off things.
'final',
'NewType',
'overload',
'runtime',
Expand Down Expand Up @@ -106,6 +108,94 @@ def _gorg(cls):
return cls


class FinalMeta(TypingMeta):
"""Metaclass for _Final"""

def __new__(cls, name, bases, namespace):
cls.assert_no_subclassing(bases)
self = super(FinalMeta, cls).__new__(cls, name, bases, namespace)
return self


class _Final(typing._FinalTypingBase):
"""A special typing construct to indicate that a name
cannot be re-assigned or overridden in a subclass.
For example:
MAX_SIZE: Final = 9000
MAX_SIZE += 1 # Error reported by type checker
class Connection:
TIMEOUT: Final[int] = 10
class FastConnector(Connection):
TIMEOUT = 1 # Error reported by type checker
There is no runtime checking of these properties.
"""

__metaclass__ = FinalMeta
__slots__ = ('__type__',)

def __init__(self, tp=None, **kwds):
self.__type__ = tp

def __getitem__(self, item):
cls = type(self)
if self.__type__ is None:
return cls(typing._type_check(item,
'{} accepts only single type.'.format(cls.__name__[1:])),
_root=True)
raise TypeError('{} cannot be further subscripted'
.format(cls.__name__[1:]))

def _eval_type(self, globalns, localns):
new_tp = typing._eval_type(self.__type__, globalns, localns)
if new_tp == self.__type__:
return self
return type(self)(new_tp, _root=True)

def __repr__(self):
r = super(_Final, self).__repr__()
if self.__type__ is not None:
r += '[{}]'.format(typing._type_repr(self.__type__))
return r

def __hash__(self):
return hash((type(self).__name__, self.__type__))

def __eq__(self, other):
if not isinstance(other, _Final):
return NotImplemented
if self.__type__ is not None:
return self.__type__ == other.__type__
return self is other

Final = _Final(_root=True)


def final(f):
"""This decorator can be used to indicate to type checkers that
the decorated method cannot be overridden, and decorated class
cannot be subclassed. For example:
class Base:
@final
def done(self) -> None:
...
class Sub(Base):
def done(self) -> None: # Error reported by type checker
...
@final
class Leaf:
...
class Other(Leaf): # Error reported by type checker
...
There is no runtime checking of these properties.
"""
return f


class _ProtocolMeta(GenericMeta):
"""Internal metaclass for Protocol.
Expand Down
55 changes: 52 additions & 3 deletions typing_extensions/src_py3/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from typing import Generic
from typing import get_type_hints
from typing import no_type_check
from typing_extensions import NoReturn, ClassVar, Type, NewType
from typing_extensions import NoReturn, ClassVar, Final, Type, NewType
try:
from typing_extensions import Protocol, runtime
except ImportError:
Expand Down Expand Up @@ -171,6 +171,47 @@ def test_no_isinstance(self):
issubclass(int, ClassVar)


class FinalTests(BaseTestCase):

def test_basics(self):
with self.assertRaises(TypeError):
Final[1]
with self.assertRaises(TypeError):
Final[int, str]
with self.assertRaises(TypeError):
Final[int][str]

def test_repr(self):
self.assertEqual(repr(Final), 'typing_extensions.Final')
cv = Final[int]
self.assertEqual(repr(cv), 'typing_extensions.Final[int]')
cv = Final[Employee]
self.assertEqual(repr(cv), 'typing_extensions.Final[%s.Employee]' % __name__)

@skipUnless(SUBCLASS_CHECK_FORBIDDEN, "Behavior added in typing 3.5.3")
def test_cannot_subclass(self):
with self.assertRaises(TypeError):
class C(type(Final)):
pass
with self.assertRaises(TypeError):
class C(type(Final[int])):
pass

def test_cannot_init(self):
with self.assertRaises(TypeError):
Final()
with self.assertRaises(TypeError):
type(Final)()
with self.assertRaises(TypeError):
type(Final[Optional[int]])()

def test_no_isinstance(self):
with self.assertRaises(TypeError):
isinstance(1, Final[int])
with self.assertRaises(TypeError):
issubclass(int, Final)


class OverloadTests(BaseTestCase):

def test_overload_fails(self):
Expand Down Expand Up @@ -262,6 +303,9 @@ class CSub(B):
class G(Generic[T]):
lst: ClassVar[List[T]] = []
class Loop:
attr: Final['Loop']
class NoneAndForward:
parent: 'NoneAndForward'
meaning: None
Expand Down Expand Up @@ -291,7 +335,7 @@ async def g_with(am: AsyncContextManager[int]):
# fake names for the sake of static analysis
ann_module = ann_module2 = ann_module3 = None
A = B = CSub = G = CoolEmployee = CoolEmployeeWithDefault = object
XMeth = XRepr = NoneAndForward = object
XMeth = XRepr = NoneAndForward = Loop = object

gth = get_type_hints

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

@skipUnless(PY36, 'Python 3.6 required')
def test_final_forward_ref(self):
self.assertEqual(gth(Loop, globals())['attr'], Final[Loop])
self.assertNotEqual(gth(Loop, globals())['attr'], Final[int])
self.assertNotEqual(gth(Loop, globals())['attr'], Final)

class CollectionsAbcTests(BaseTestCase):

Expand Down Expand Up @@ -1253,7 +1302,7 @@ def test_typing_extensions_includes_standard(self):
self.assertIn('runtime', a)

def test_typing_extensions_defers_when_possible(self):
exclude = {'overload', 'Text', 'TYPE_CHECKING'}
exclude = {'overload', 'Text', 'TYPE_CHECKING', 'Final'}
for item in typing_extensions.__all__:
if item not in exclude and hasattr(typing, item):
self.assertIs(
Expand Down
Loading

0 comments on commit c6c7dfd

Please sign in to comment.