Skip to content

Commit

Permalink
Merge branch 'make-ducktype-internal'
Browse files Browse the repository at this point in the history
Closes #596.
  • Loading branch information
JukkaL committed Mar 9, 2015
2 parents 28a9fcd + f09f6e6 commit e70a516
Show file tree
Hide file tree
Showing 25 changed files with 72 additions and 132 deletions.
39 changes: 10 additions & 29 deletions docs/source/duck_type_compatibility.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,20 @@ Duck type compatibility

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

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

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

* ``int`` is duck type compatible with ``float``
* ``float`` is duck type compatible with ``complex``.
Mypy support for Python 2 is still work in progress.

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

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

.. note::

Note that in Python 2 a ``str`` object with non-ASCII characters is
Expand All @@ -50,7 +35,3 @@ Also, in Python 2 ``str`` would be duck type compatible with ``unicode``.
silently pass type checking. In Python 3 ``str`` and ``bytes`` are
separate, unrelated types and this kind of error is easy to
detect. This a good reason for preferring Python 3 over Python 2!

.. note::

Mypy support for Python 2 is still work in progress.
6 changes: 2 additions & 4 deletions lib-python/3.2/tempfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@
from typing import (
Any as _Any, Callable as _Callable, Iterator as _Iterator,
Undefined as _Undefined, List as _List, Tuple as _Tuple, Dict as _Dict,
Iterable as _Iterable, IO as _IO, ducktype as _ducktype,
Traceback as _Traceback
Iterable as _Iterable, IO as _IO, Traceback as _Traceback, cast as _cast,
)

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


@_ducktype(_IO[_Any])
class _TemporaryFileWrapper:
"""Temporary file wrapper
Expand Down Expand Up @@ -457,7 +455,7 @@ def NamedTemporaryFile(mode: str = 'w+b', buffering: int = -1,
file = _io.open(fd, mode, buffering=buffering,
newline=newline, encoding=encoding)

return _TemporaryFileWrapper(file, name, delete)
return _cast(_IO[_Any], _TemporaryFileWrapper(file, name, delete))

if _os.name != 'posix' or _sys.platform == 'cygwin':
# On non-POSIX and Cygwin systems, assume that we cannot unlink a file
Expand Down
3 changes: 1 addition & 2 deletions lib-python/3.2/test/test_shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from test.support import check_warnings, captured_stdout

from typing import (
Any, Callable, Tuple, List, Sequence, BinaryIO, Traceback, IO, Union, ducktype, cast
Any, Callable, Tuple, List, Sequence, BinaryIO, Traceback, IO, Union, cast
)

import bz2
Expand Down Expand Up @@ -851,7 +851,6 @@ class TestCopyFile(unittest.TestCase):

_delete = False

@ducktype(IO[str])
class Faux(object):
_entered = False
_exited_with = None # type: tuple
Expand Down
6 changes: 1 addition & 5 deletions lib-typing/2.7/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
_Protocol, Sized, Iterable, Iterator, Sequence, Union, Optional,
AbstractSet, Mapping, BinaryIO, TextIO, SupportsInt, SupportsFloat,
SupportsAbs, Reversible, Undefined, AnyStr, annotations, builtinclass,
cast, disjointclass, ducktype, forwardref, overload, TypeVar
cast, disjointclass, forwardref, overload, TypeVar
)


Expand Down Expand Up @@ -433,10 +433,6 @@ class A: pass
self.assertIs(builtinclass(int), int)
self.assertIs(builtinclass(A), A)

def test_ducktype(self):
class A: pass
self.assertIs(ducktype(str)(A), A)

def test_disjointclass(self):
class A: pass
self.assertIs(disjointclass(str)(A), A)
Expand Down
10 changes: 0 additions & 10 deletions lib-typing/2.7/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,6 @@ def builtinclass(cls):
return cls


def ducktype(type):
"""Return a duck type declaration decorator.
The decorator only affects type checking.
"""
def decorator(cls):
return cls
return decorator


def disjointclass(type):
"""Return a disjoint class declaration decorator.
Expand Down
6 changes: 1 addition & 5 deletions lib-typing/3.2/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
_Protocol, Sized, Iterable, Iterator, Sequence, Union, Optional,
AbstractSet, Mapping, BinaryIO, TextIO, SupportsInt, SupportsFloat,
SupportsAbs, SupportsRound, Reversible, Undefined, AnyStr, builtinclass,
cast, disjointclass, ducktype, forwardref, overload, TypeVar
cast, disjointclass, forwardref, overload, TypeVar
)


Expand Down Expand Up @@ -438,10 +438,6 @@ class A: pass
self.assertIs(builtinclass(int), int)
self.assertIs(builtinclass(A), A)

def test_ducktype(self):
class A: pass
self.assertIs(ducktype(str)(A), A)

def test_disjointclass(self):
class A: pass
self.assertIs(disjointclass(str)(A), A)
Expand Down
10 changes: 0 additions & 10 deletions lib-typing/3.2/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,6 @@ def builtinclass(cls):
return cls


def ducktype(type):
"""Return a duck type declaration decorator.
The decorator only affects type checking.
"""
def decorator(cls):
return cls
return decorator


def disjointclass(type):
"""Return a disjoint class declaration decorator.
Expand Down
4 changes: 2 additions & 2 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,8 +609,8 @@ def matches_signature_erased(self, arg_types: List[Type], is_var_arg: bool,
# Fixed function arguments.
func_fixed = callee.max_fixed_args()
for i in range(min(len(arg_types), func_fixed)):
# Use is_more_precise rather than is_subtype because it ignores ducktype
# declarations. This is important since ducktype declarations are ignored
# Use is_more_precise rather than is_subtype because it ignores _promote
# declarations. This is important since _promote declarations are ignored
# when performing runtime type checking.
if not is_compatible_overload_arg(arg_types[i], callee.arg_types[i]):
return False
Expand Down
8 changes: 4 additions & 4 deletions mypy/join.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,10 @@ def join_instances(t: Instance, s: Instance) -> Type:
def join_instances_via_supertype(t: Instance, s: Instance) -> Type:
# Give preference to joins via duck typing relationship, so that
# join(int, float) == float, for example.
if t.type.ducktype and is_subtype(t.type.ducktype, s):
return join_types(t.type.ducktype, s)
elif s.type.ducktype and is_subtype(s.type.ducktype, t):
return join_types(t, s.type.ducktype)
if t.type._promote and is_subtype(t.type._promote, s):
return join_types(t.type._promote, s)
elif s.type._promote and is_subtype(s.type._promote, t):
return join_types(t, s.type._promote)
res = s
mapped = map_instance_to_supertype(t, t.type.bases[0].type)
join = join_instances(mapped, res)
Expand Down
10 changes: 5 additions & 5 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1364,16 +1364,16 @@ def accept(self, visitor: NodeVisitor[T]) -> T:
return visitor.visit_namedtuple_expr(self)


class DucktypeExpr(Node):
"""Ducktype class decorator expression ducktype(...)."""
class PromoteExpr(Node):
"""Ducktype class decorator expression _promote(...)."""

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

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

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


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

# Duck type compatibility (ducktype decorator)
ducktype = None # type: mypy.types.Type
# Duck type compatibility (_promote decorator)
_promote = None # type: mypy.types.Type

# Representation of a Tuple[...] base class, if the class has any
# (e.g., for named tuples). If this is not None, the actual Type
Expand Down
18 changes: 9 additions & 9 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
SliceExpr, CastExpr, TypeApplication, Context, SymbolTable,
SymbolTableNode, TVAR, UNBOUND_TVAR, ListComprehension, GeneratorExpr,
FuncExpr, MDEF, FuncBase, Decorator, SetExpr, UndefinedExpr, TypeVarExpr,
StrExpr, PrintStmt, ConditionalExpr, DucktypeExpr, DisjointclassExpr,
StrExpr, PrintStmt, ConditionalExpr, PromoteExpr, DisjointclassExpr,
ComparisonExpr, StarExpr, ARG_POS, ARG_NAMED, MroError, type_aliases,
YieldFromStmt, YieldFromExpr, NamedTupleExpr, NonlocalDecl,
SetComprehension, DictionaryComprehension, TYPE_ALIAS, TypeAliasExpr
Expand Down Expand Up @@ -409,8 +409,8 @@ def setup_ducktyping(self, defn: ClassDef) -> None:
for decorator in defn.decorators:
if isinstance(decorator, CallExpr):
analyzed = decorator.analyzed
if isinstance(analyzed, DucktypeExpr):
defn.info.ducktype = analyzed.type
if isinstance(analyzed, PromoteExpr):
defn.info._promote = analyzed.type
elif isinstance(analyzed, DisjointclassExpr):
node = analyzed.cls.node
if isinstance(node, TypeInfo):
Expand Down Expand Up @@ -1436,17 +1436,17 @@ def visit_call_expr(self, expr: CallExpr) -> None:
expr.analyzed = UndefinedExpr(type)
expr.analyzed.line = expr.line
expr.analyzed.accept(self)
elif refers_to_fullname(expr.callee, 'typing.ducktype'):
# Special form ducktype(...).
if not self.check_fixed_args(expr, 1, 'ducktype'):
elif refers_to_fullname(expr.callee, 'typing._promote'):
# Special form _promote(...).
if not self.check_fixed_args(expr, 1, '_promote'):
return
# Translate first argument to an unanalyzed type.
try:
target = expr_to_unanalyzed_type(expr.args[0])
except TypeTranslationError:
self.fail('Argument 1 to ducktype is not a type', expr)
self.fail('Argument 1 to _promote is not a type', expr)
return
expr.analyzed = DucktypeExpr(target)
expr.analyzed = PromoteExpr(target)
expr.analyzed.line = expr.line
expr.analyzed.accept(self)
elif refers_to_fullname(expr.callee, 'typing.disjointclass'):
Expand Down Expand Up @@ -1607,7 +1607,7 @@ def visit_conditional_expr(self, expr: ConditionalExpr) -> None:
expr.cond.accept(self)
expr.else_expr.accept(self)

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

def visit_disjointclass_expr(self, expr: DisjointclassExpr) -> None:
Expand Down
8 changes: 4 additions & 4 deletions mypy/strconv.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,8 @@ def visit_class_def(self, o):
a.insert(1, ('Decorators', o.decorators))
if o.is_builtinclass:
a.insert(1, 'Builtinclass')
if o.info and o.info.ducktype:
a.insert(1, 'Ducktype({})'.format(o.info.ducktype))
if o.info and o.info._promote:
a.insert(1, 'Promote({})'.format(o.info._promote))
if o.info and o.info.disjoint_classes:
a.insert(1, ('Disjointclasses', [info.fullname() for
info in o.info.disjoint_classes]))
Expand Down Expand Up @@ -411,8 +411,8 @@ def visit_namedtuple_expr(self, o):
o.info.name(),
o.info.tuple_type)

def visit_ducktype_expr(self, o):
return 'DucktypeExpr:{}({})'.format(o.line, o.type)
def visit__promote_expr(self, o):
return 'PromoteExpr:{}({})'.format(o.line, o.type)

def visit_disjointclass_expr(self, o):
return 'DisjointclassExpr:{}({})'.format(o.line, o.cls.fullname)
Expand Down
2 changes: 1 addition & 1 deletion mypy/subtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def visit_erased_type(self, left: ErasedType) -> bool:
def visit_instance(self, left: Instance) -> bool:
right = self.right
if isinstance(right, Instance):
if left.type.ducktype and is_subtype(left.type.ducktype,
if left.type._promote and is_subtype(left.type._promote,
self.right):
return True
rname = right.type.fullname()
Expand Down
12 changes: 6 additions & 6 deletions mypy/test/data/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -827,26 +827,26 @@ class B(S, int): pass # E: Instance layout conflict in multiple inheritance
main: In class "B":


-- Ducktype decorators
-- _promote decorators
-- -------------------


[case testSimpleDucktypeDecorator]
from typing import ducktype, Undefined
from typing import _promote, Undefined
class A: pass
@ducktype(A)
@_promote(A)
class B: pass
a = Undefined(A)
b = Undefined(B)
b = a # E: Incompatible types in assignment (expression has type "A", variable has type "B")
a = b

[case testDucktypeTransitivityDecorator]
from typing import ducktype, Undefined
from typing import _promote, Undefined
class A: pass
@ducktype(A)
@_promote(A)
class B: pass
@ducktype(B)
@_promote(B)
class C: pass
a = Undefined(A)
c = Undefined(C)
Expand Down
10 changes: 5 additions & 5 deletions mypy/test/data/check-inference.test
Original file line number Diff line number Diff line change
Expand Up @@ -975,9 +975,9 @@ a.x.y # E: "object" has no attribute "y"


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

[case testListWithDucktypeCompatibilityAndTransitivity]
from typing import List, Undefined, ducktype
from typing import List, Undefined, _promote
class A: pass
@ducktype(A)
@_promote(A)
class B: pass
@ducktype(B)
@_promote(B)
class C: pass
a = Undefined(List[A])
x1 = [A(), C()]
Expand Down
4 changes: 2 additions & 2 deletions mypy/test/data/check-overloading.test
Original file line number Diff line number Diff line change
Expand Up @@ -478,13 +478,13 @@ main: In class "C":
main, line 17: Signature of "f" incompatible with supertype "A"

[case testOverloadingAndDucktypeCompatibility]
from typing import overload, ducktype, builtinclass
from typing import overload, _promote, builtinclass

@builtinclass
class A: pass

@builtinclass
@ducktype(A)
@_promote(A)
class B: pass

@overload
Expand Down
Loading

0 comments on commit e70a516

Please sign in to comment.