Skip to content

Commit e70a516

Browse files
committed
Merge branch 'make-ducktype-internal'
Closes #596.
2 parents 28a9fcd + f09f6e6 commit e70a516

25 files changed

+72
-132
lines changed

docs/source/duck_type_compatibility.rst

Lines changed: 10 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,20 @@ Duck type compatibility
33

44
In Python, certain types are compatible even though they aren't subclasses of
55
each other. For example, ``int`` objects are valid whenever ``float`` objects
6-
are expected. Mypy supports this idiom via *duck type compatibility*. You can
7-
specify a type to be a valid substitute for another type using the ``ducktype``
8-
class decorator:
6+
are expected. Mypy supports this idiom via *duck type compatibility*. As of
7+
now, this is only supported for a small set of built-in types:
98

10-
.. code-block:: python
11-
12-
from typing import ducktype
13-
14-
@ducktype(str)
15-
class MyString:
16-
def __init__(self, ...): ...
17-
...
18-
19-
Now mypy considers a ``MyString`` instance to be valid whenever a
20-
``str`` object is expected, independent of whether ``MyString``
21-
actually is a perfect substitute for strings. You can think of this as
22-
a class-level cast as opposed to a value-level cast. This is a powerful
23-
feature but you can easily abuse it and make it easy to write programs
24-
that pass type checking but will crash and burn when run!
9+
* ``int`` is duck type compatible with ``float`` and ``complex``.
10+
* ``float`` is duck type compatible with ``complex``.
11+
* In Python 2, ``str`` is duck type compatible with ``unicode``.
2512

26-
The most common case where ``ducktype`` is useful is for certain
27-
well-known standard library classes:
13+
.. note::
2814

29-
* ``int`` is duck type compatible with ``float``
30-
* ``float`` is duck type compatible with ``complex``.
15+
Mypy support for Python 2 is still work in progress.
3116

32-
Thus code like this is nice and clean and also behaves as expected:
17+
For example, mypy considers an ``int`` object to be valid whenever a
18+
``float`` object is expected. Thus code like this is nice and clean
19+
and also behaves as expected:
3320

3421
.. code-block:: python
3522
@@ -39,8 +26,6 @@ Thus code like this is nice and clean and also behaves as expected:
3926
n = 90 # Inferred type 'int'
4027
print(degrees_to_radians(n)) # Okay!
4128
42-
Also, in Python 2 ``str`` would be duck type compatible with ``unicode``.
43-
4429
.. note::
4530

4631
Note that in Python 2 a ``str`` object with non-ASCII characters is
@@ -50,7 +35,3 @@ Also, in Python 2 ``str`` would be duck type compatible with ``unicode``.
5035
silently pass type checking. In Python 3 ``str`` and ``bytes`` are
5136
separate, unrelated types and this kind of error is easy to
5237
detect. This a good reason for preferring Python 3 over Python 2!
53-
54-
.. note::
55-
56-
Mypy support for Python 2 is still work in progress.

lib-python/3.2/tempfile.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@
3939
from typing import (
4040
Any as _Any, Callable as _Callable, Iterator as _Iterator,
4141
Undefined as _Undefined, List as _List, Tuple as _Tuple, Dict as _Dict,
42-
Iterable as _Iterable, IO as _IO, ducktype as _ducktype,
43-
Traceback as _Traceback
42+
Iterable as _Iterable, IO as _IO, Traceback as _Traceback, cast as _cast,
4443
)
4544

4645
try:
@@ -353,7 +352,6 @@ def mktemp(suffix: str = "", prefix: str = template, dir: str = None) -> str:
353352
raise IOError(_errno.EEXIST, "No usable temporary filename found")
354353

355354

356-
@_ducktype(_IO[_Any])
357355
class _TemporaryFileWrapper:
358356
"""Temporary file wrapper
359357
@@ -457,7 +455,7 @@ def NamedTemporaryFile(mode: str = 'w+b', buffering: int = -1,
457455
file = _io.open(fd, mode, buffering=buffering,
458456
newline=newline, encoding=encoding)
459457

460-
return _TemporaryFileWrapper(file, name, delete)
458+
return _cast(_IO[_Any], _TemporaryFileWrapper(file, name, delete))
461459

462460
if _os.name != 'posix' or _sys.platform == 'cygwin':
463461
# On non-POSIX and Cygwin systems, assume that we cannot unlink a file

lib-python/3.2/test/test_shutil.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from test.support import check_warnings, captured_stdout
2525

2626
from typing import (
27-
Any, Callable, Tuple, List, Sequence, BinaryIO, Traceback, IO, Union, ducktype, cast
27+
Any, Callable, Tuple, List, Sequence, BinaryIO, Traceback, IO, Union, cast
2828
)
2929

3030
import bz2
@@ -851,7 +851,6 @@ class TestCopyFile(unittest.TestCase):
851851

852852
_delete = False
853853

854-
@ducktype(IO[str])
855854
class Faux(object):
856855
_entered = False
857856
_exited_with = None # type: tuple

lib-typing/2.7/test_typing.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
_Protocol, Sized, Iterable, Iterator, Sequence, Union, Optional,
88
AbstractSet, Mapping, BinaryIO, TextIO, SupportsInt, SupportsFloat,
99
SupportsAbs, Reversible, Undefined, AnyStr, annotations, builtinclass,
10-
cast, disjointclass, ducktype, forwardref, overload, TypeVar
10+
cast, disjointclass, forwardref, overload, TypeVar
1111
)
1212

1313

@@ -433,10 +433,6 @@ class A: pass
433433
self.assertIs(builtinclass(int), int)
434434
self.assertIs(builtinclass(A), A)
435435

436-
def test_ducktype(self):
437-
class A: pass
438-
self.assertIs(ducktype(str)(A), A)
439-
440436
def test_disjointclass(self):
441437
class A: pass
442438
self.assertIs(disjointclass(str)(A), A)

lib-typing/2.7/typing.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,6 @@ def builtinclass(cls):
4949
return cls
5050

5151

52-
def ducktype(type):
53-
"""Return a duck type declaration decorator.
54-
55-
The decorator only affects type checking.
56-
"""
57-
def decorator(cls):
58-
return cls
59-
return decorator
60-
61-
6252
def disjointclass(type):
6353
"""Return a disjoint class declaration decorator.
6454

lib-typing/3.2/test_typing.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
_Protocol, Sized, Iterable, Iterator, Sequence, Union, Optional,
77
AbstractSet, Mapping, BinaryIO, TextIO, SupportsInt, SupportsFloat,
88
SupportsAbs, SupportsRound, Reversible, Undefined, AnyStr, builtinclass,
9-
cast, disjointclass, ducktype, forwardref, overload, TypeVar
9+
cast, disjointclass, forwardref, overload, TypeVar
1010
)
1111

1212

@@ -438,10 +438,6 @@ class A: pass
438438
self.assertIs(builtinclass(int), int)
439439
self.assertIs(builtinclass(A), A)
440440

441-
def test_ducktype(self):
442-
class A: pass
443-
self.assertIs(ducktype(str)(A), A)
444-
445441
def test_disjointclass(self):
446442
class A: pass
447443
self.assertIs(disjointclass(str)(A), A)

lib-typing/3.2/typing.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,6 @@ def builtinclass(cls):
4848
return cls
4949

5050

51-
def ducktype(type):
52-
"""Return a duck type declaration decorator.
53-
54-
The decorator only affects type checking.
55-
"""
56-
def decorator(cls):
57-
return cls
58-
return decorator
59-
60-
6151
def disjointclass(type):
6252
"""Return a disjoint class declaration decorator.
6353

mypy/checkexpr.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -609,8 +609,8 @@ def matches_signature_erased(self, arg_types: List[Type], is_var_arg: bool,
609609
# Fixed function arguments.
610610
func_fixed = callee.max_fixed_args()
611611
for i in range(min(len(arg_types), func_fixed)):
612-
# Use is_more_precise rather than is_subtype because it ignores ducktype
613-
# declarations. This is important since ducktype declarations are ignored
612+
# Use is_more_precise rather than is_subtype because it ignores _promote
613+
# declarations. This is important since _promote declarations are ignored
614614
# when performing runtime type checking.
615615
if not is_compatible_overload_arg(arg_types[i], callee.arg_types[i]):
616616
return False

mypy/join.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -201,10 +201,10 @@ def join_instances(t: Instance, s: Instance) -> Type:
201201
def join_instances_via_supertype(t: Instance, s: Instance) -> Type:
202202
# Give preference to joins via duck typing relationship, so that
203203
# join(int, float) == float, for example.
204-
if t.type.ducktype and is_subtype(t.type.ducktype, s):
205-
return join_types(t.type.ducktype, s)
206-
elif s.type.ducktype and is_subtype(s.type.ducktype, t):
207-
return join_types(t, s.type.ducktype)
204+
if t.type._promote and is_subtype(t.type._promote, s):
205+
return join_types(t.type._promote, s)
206+
elif s.type._promote and is_subtype(s.type._promote, t):
207+
return join_types(t, s.type._promote)
208208
res = s
209209
mapped = map_instance_to_supertype(t, t.type.bases[0].type)
210210
join = join_instances(mapped, res)

mypy/nodes.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,16 +1364,16 @@ def accept(self, visitor: NodeVisitor[T]) -> T:
13641364
return visitor.visit_namedtuple_expr(self)
13651365

13661366

1367-
class DucktypeExpr(Node):
1368-
"""Ducktype class decorator expression ducktype(...)."""
1367+
class PromoteExpr(Node):
1368+
"""Ducktype class decorator expression _promote(...)."""
13691369

13701370
type = Undefined('mypy.types.Type')
13711371

13721372
def __init__(self, type: 'mypy.types.Type') -> None:
13731373
self.type = type
13741374

13751375
def accept(self, visitor: NodeVisitor[T]) -> T:
1376-
return visitor.visit_ducktype_expr(self)
1376+
return visitor.visit__promote_expr(self)
13771377

13781378

13791379
class DisjointclassExpr(Node):
@@ -1445,8 +1445,8 @@ class TypeInfo(SymbolNode):
14451445
# Direct base classes.
14461446
bases = Undefined(List['mypy.types.Instance'])
14471447

1448-
# Duck type compatibility (ducktype decorator)
1449-
ducktype = None # type: mypy.types.Type
1448+
# Duck type compatibility (_promote decorator)
1449+
_promote = None # type: mypy.types.Type
14501450

14511451
# Representation of a Tuple[...] base class, if the class has any
14521452
# (e.g., for named tuples). If this is not None, the actual Type

mypy/semanal.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
SliceExpr, CastExpr, TypeApplication, Context, SymbolTable,
5656
SymbolTableNode, TVAR, UNBOUND_TVAR, ListComprehension, GeneratorExpr,
5757
FuncExpr, MDEF, FuncBase, Decorator, SetExpr, UndefinedExpr, TypeVarExpr,
58-
StrExpr, PrintStmt, ConditionalExpr, DucktypeExpr, DisjointclassExpr,
58+
StrExpr, PrintStmt, ConditionalExpr, PromoteExpr, DisjointclassExpr,
5959
ComparisonExpr, StarExpr, ARG_POS, ARG_NAMED, MroError, type_aliases,
6060
YieldFromStmt, YieldFromExpr, NamedTupleExpr, NonlocalDecl,
6161
SetComprehension, DictionaryComprehension, TYPE_ALIAS, TypeAliasExpr
@@ -409,8 +409,8 @@ def setup_ducktyping(self, defn: ClassDef) -> None:
409409
for decorator in defn.decorators:
410410
if isinstance(decorator, CallExpr):
411411
analyzed = decorator.analyzed
412-
if isinstance(analyzed, DucktypeExpr):
413-
defn.info.ducktype = analyzed.type
412+
if isinstance(analyzed, PromoteExpr):
413+
defn.info._promote = analyzed.type
414414
elif isinstance(analyzed, DisjointclassExpr):
415415
node = analyzed.cls.node
416416
if isinstance(node, TypeInfo):
@@ -1436,17 +1436,17 @@ def visit_call_expr(self, expr: CallExpr) -> None:
14361436
expr.analyzed = UndefinedExpr(type)
14371437
expr.analyzed.line = expr.line
14381438
expr.analyzed.accept(self)
1439-
elif refers_to_fullname(expr.callee, 'typing.ducktype'):
1440-
# Special form ducktype(...).
1441-
if not self.check_fixed_args(expr, 1, 'ducktype'):
1439+
elif refers_to_fullname(expr.callee, 'typing._promote'):
1440+
# Special form _promote(...).
1441+
if not self.check_fixed_args(expr, 1, '_promote'):
14421442
return
14431443
# Translate first argument to an unanalyzed type.
14441444
try:
14451445
target = expr_to_unanalyzed_type(expr.args[0])
14461446
except TypeTranslationError:
1447-
self.fail('Argument 1 to ducktype is not a type', expr)
1447+
self.fail('Argument 1 to _promote is not a type', expr)
14481448
return
1449-
expr.analyzed = DucktypeExpr(target)
1449+
expr.analyzed = PromoteExpr(target)
14501450
expr.analyzed.line = expr.line
14511451
expr.analyzed.accept(self)
14521452
elif refers_to_fullname(expr.callee, 'typing.disjointclass'):
@@ -1607,7 +1607,7 @@ def visit_conditional_expr(self, expr: ConditionalExpr) -> None:
16071607
expr.cond.accept(self)
16081608
expr.else_expr.accept(self)
16091609

1610-
def visit_ducktype_expr(self, expr: DucktypeExpr) -> None:
1610+
def visit__promote_expr(self, expr: PromoteExpr) -> None:
16111611
expr.type = self.anal_type(expr.type)
16121612

16131613
def visit_disjointclass_expr(self, expr: DisjointclassExpr) -> None:

mypy/strconv.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,8 @@ def visit_class_def(self, o):
136136
a.insert(1, ('Decorators', o.decorators))
137137
if o.is_builtinclass:
138138
a.insert(1, 'Builtinclass')
139-
if o.info and o.info.ducktype:
140-
a.insert(1, 'Ducktype({})'.format(o.info.ducktype))
139+
if o.info and o.info._promote:
140+
a.insert(1, 'Promote({})'.format(o.info._promote))
141141
if o.info and o.info.disjoint_classes:
142142
a.insert(1, ('Disjointclasses', [info.fullname() for
143143
info in o.info.disjoint_classes]))
@@ -411,8 +411,8 @@ def visit_namedtuple_expr(self, o):
411411
o.info.name(),
412412
o.info.tuple_type)
413413

414-
def visit_ducktype_expr(self, o):
415-
return 'DucktypeExpr:{}({})'.format(o.line, o.type)
414+
def visit__promote_expr(self, o):
415+
return 'PromoteExpr:{}({})'.format(o.line, o.type)
416416

417417
def visit_disjointclass_expr(self, o):
418418
return 'DisjointclassExpr:{}({})'.format(o.line, o.cls.fullname)

mypy/subtypes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def visit_erased_type(self, left: ErasedType) -> bool:
7272
def visit_instance(self, left: Instance) -> bool:
7373
right = self.right
7474
if isinstance(right, Instance):
75-
if left.type.ducktype and is_subtype(left.type.ducktype,
75+
if left.type._promote and is_subtype(left.type._promote,
7676
self.right):
7777
return True
7878
rname = right.type.fullname()

mypy/test/data/check-classes.test

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -827,26 +827,26 @@ class B(S, int): pass # E: Instance layout conflict in multiple inheritance
827827
main: In class "B":
828828

829829

830-
-- Ducktype decorators
830+
-- _promote decorators
831831
-- -------------------
832832

833833

834834
[case testSimpleDucktypeDecorator]
835-
from typing import ducktype, Undefined
835+
from typing import _promote, Undefined
836836
class A: pass
837-
@ducktype(A)
837+
@_promote(A)
838838
class B: pass
839839
a = Undefined(A)
840840
b = Undefined(B)
841841
b = a # E: Incompatible types in assignment (expression has type "A", variable has type "B")
842842
a = b
843843

844844
[case testDucktypeTransitivityDecorator]
845-
from typing import ducktype, Undefined
845+
from typing import _promote, Undefined
846846
class A: pass
847-
@ducktype(A)
847+
@_promote(A)
848848
class B: pass
849-
@ducktype(B)
849+
@_promote(B)
850850
class C: pass
851851
a = Undefined(A)
852852
c = Undefined(C)

mypy/test/data/check-inference.test

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -975,9 +975,9 @@ a.x.y # E: "object" has no attribute "y"
975975

976976

977977
[case testListWithDucktypeCompatibility]
978-
from typing import List, Undefined, ducktype
978+
from typing import List, Undefined, _promote
979979
class A: pass
980-
@ducktype(A)
980+
@_promote(A)
981981
class B: pass
982982
a = Undefined(List[A])
983983
x1 = [A(), B()]
@@ -989,11 +989,11 @@ a = x3 # E: Incompatible types in assignment (expression has type List[B], varia
989989
[builtins fixtures/list.py]
990990

991991
[case testListWithDucktypeCompatibilityAndTransitivity]
992-
from typing import List, Undefined, ducktype
992+
from typing import List, Undefined, _promote
993993
class A: pass
994-
@ducktype(A)
994+
@_promote(A)
995995
class B: pass
996-
@ducktype(B)
996+
@_promote(B)
997997
class C: pass
998998
a = Undefined(List[A])
999999
x1 = [A(), C()]

mypy/test/data/check-overloading.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -478,13 +478,13 @@ main: In class "C":
478478
main, line 17: Signature of "f" incompatible with supertype "A"
479479

480480
[case testOverloadingAndDucktypeCompatibility]
481-
from typing import overload, ducktype, builtinclass
481+
from typing import overload, _promote, builtinclass
482482

483483
@builtinclass
484484
class A: pass
485485

486486
@builtinclass
487-
@ducktype(A)
487+
@_promote(A)
488488
class B: pass
489489

490490
@overload

0 commit comments

Comments
 (0)