Skip to content

Commit a103f25

Browse files
committed
Merge branch 'abstract-property'
Implement @abstractproperty. Closes #263.
2 parents 891543d + 623e921 commit a103f25

File tree

7 files changed

+277
-11
lines changed

7 files changed

+277
-11
lines changed

mypy/checker.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1898,6 +1898,20 @@ def visit_decorator(self, e: Decorator) -> Type:
18981898
sig = set_callable_name(sig, e.func)
18991899
e.var.type = sig
19001900
e.var.is_ready = True
1901+
if e.func.is_property:
1902+
self.check_incompatible_property_override(e)
1903+
1904+
def check_incompatible_property_override(self, e: Decorator) -> None:
1905+
if not e.var.is_settable_property:
1906+
name = e.func.name()
1907+
for base in e.func.info.mro[1:]:
1908+
base_attr = base.names.get(name)
1909+
if not base_attr:
1910+
continue
1911+
if (isinstance(base_attr.node, OverloadedFuncDef) and
1912+
base_attr.node.is_property and
1913+
base_attr.node.items[0].var.is_settable_property):
1914+
self.fail(messages.READ_ONLY_PROPERTY_OVERRIDES_READ_WRITE, e)
19011915

19021916
def visit_with_stmt(self, s: WithStmt) -> Type:
19031917
echk = self.expr_checker

mypy/messages.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@
6464
CANNOT_ASSIGN_TO_TYPE = 'Cannot assign to a type'
6565
INCONSISTENT_ABSTRACT_OVERLOAD = \
6666
'Overloaded method has both abstract and non-abstract variants'
67+
READ_ONLY_PROPERTY_OVERRIDES_READ_WRITE = \
68+
'Read-only property cannot override read-write property'
6769
INSTANCE_LAYOUT_CONFLICT = 'Instance layout conflict in multiple inheritance'
6870
FORMAT_REQUIRES_MAPPING = 'Format requires a mapping'
6971
GENERIC_TYPE_NOT_VALID_AS_EXPRESSION = \
@@ -732,7 +734,7 @@ def cannot_instantiate_abstract_class(self, class_name: str,
732734
context: Context) -> None:
733735
attrs = format_string_list("'%s'" % a for a in abstract_attributes[:5])
734736
self.fail("Cannot instantiate abstract class '%s' with abstract "
735-
"method%s %s" % (class_name, plural_s(abstract_attributes),
737+
"attribute%s %s" % (class_name, plural_s(abstract_attributes),
736738
attrs),
737739
context)
738740

mypy/semanal.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,8 @@ def analyze_property_with_multi_part_definition(self, defn: OverloadedFuncDef) -
424424
if node.name == 'setter':
425425
# The first item represents the entire property.
426426
defn.items[0].var.is_settable_property = True
427+
# Get abstractness from the original definition.
428+
item.func.is_abstract = items[0].func.is_abstract
427429
else:
428430
self.fail("Decorated property not supported", item)
429431
item.func.accept(self)
@@ -1498,10 +1500,13 @@ def visit_decorator(self, dec: Decorator) -> None:
14981500
dec.func.is_class = True
14991501
dec.var.is_classmethod = True
15001502
self.check_decorated_function_is_method('classmethod', dec)
1501-
elif refers_to_fullname(d, 'builtins.property'):
1503+
elif (refers_to_fullname(d, 'builtins.property') or
1504+
refers_to_fullname(d, 'abc.abstractproperty')):
15021505
removed.append(i)
15031506
dec.func.is_property = True
15041507
dec.var.is_property = True
1508+
if refers_to_fullname(d, 'abc.abstractproperty'):
1509+
dec.func.is_abstract = True
15051510
self.check_decorated_function_is_method('property', dec)
15061511
if len(dec.func.arguments) > 1:
15071512
self.fail('Too many arguments', dec.func)
@@ -2353,10 +2358,17 @@ def visit_decorator(self, dec: Decorator) -> None:
23532358
"""
23542359
super().visit_decorator(dec)
23552360
if dec.var.is_property:
2361+
# Decorators are expected to have a callable type (it's a little odd).
23562362
if dec.func.type is None:
2357-
dec.var.type = AnyType()
2363+
dec.var.type = CallableType(
2364+
[AnyType()],
2365+
[ARG_POS],
2366+
[None],
2367+
AnyType(),
2368+
self.builtin_type('function'),
2369+
name=dec.var.name())
23582370
elif isinstance(dec.func.type, CallableType):
2359-
dec.var.type = dec.func.type.ret_type
2371+
dec.var.type = dec.func.type
23602372
return
23612373
decorator_preserves_type = True
23622374
for expr in dec.decorators:

mypy/test/data/check-abstract.test

Lines changed: 214 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ class B(metaclass=ABCMeta):
142142
@abstractmethod
143143
def f(self): pass
144144
A() # OK
145-
B() # E: Cannot instantiate abstract class 'B' with abstract method 'f'
145+
B() # E: Cannot instantiate abstract class 'B' with abstract attribute 'f'
146146
[out]
147147

148148
[case testInstantiatingClassWithInheritedAbstractMethod]
@@ -154,7 +154,7 @@ class A(metaclass=ABCMeta):
154154
@abstractmethod
155155
def g(self): pass
156156
class B(A): pass
157-
B()# E: Cannot instantiate abstract class 'B' with abstract methods 'f' and 'g'
157+
B()# E: Cannot instantiate abstract class 'B' with abstract attributes 'f' and 'g'
158158
[out]
159159

160160

@@ -376,8 +376,8 @@ class D(A, B):
376376
class E(A, B):
377377
def f(self) -> None: pass
378378
def g(self) -> None: pass
379-
C() # E: Cannot instantiate abstract class 'C' with abstract method 'g'
380-
D() # E: Cannot instantiate abstract class 'D' with abstract method 'f'
379+
C() # E: Cannot instantiate abstract class 'C' with abstract attribute 'g'
380+
D() # E: Cannot instantiate abstract class 'D' with abstract attribute 'f'
381381
E()
382382

383383
[case testInconsistentMro]
@@ -405,7 +405,7 @@ class B(A):
405405
def f(self, x: int) -> int: pass
406406
@overload
407407
def f(self, x: str) -> str: pass
408-
A() # E: Cannot instantiate abstract class 'A' with abstract method 'f'
408+
A() # E: Cannot instantiate abstract class 'A' with abstract attribute 'f'
409409
B()
410410
B().f(1)
411411
a = B() # type: A
@@ -430,7 +430,7 @@ class B(A):
430430
def f(self, x: int) -> int: pass
431431
@overload
432432
def f(self, x: str) -> str: pass
433-
A() # E: Cannot instantiate abstract class 'A' with abstract method 'f'
433+
A() # E: Cannot instantiate abstract class 'A' with abstract attribute 'f'
434434
B()
435435
B().f(1)
436436
a = B() # type: A
@@ -521,3 +521,211 @@ class B:
521521
class C:
522522
def __add__(self, other: int) -> B: pass
523523
[out]
524+
525+
526+
-- Abstract properties
527+
-- -------------------
528+
529+
530+
[case testReadOnlyAbstractProperty]
531+
from abc import abstractproperty, ABCMeta
532+
class A(metaclass=ABCMeta):
533+
@abstractproperty
534+
def x(self) -> int: pass
535+
def f(a: A) -> None:
536+
a.x() # E: "int" not callable
537+
a.x = 1 # E: Property "x" defined in "A" is read-only
538+
[out]
539+
main: note: In function "f":
540+
541+
[case testReadOnlyAbstractPropertyForwardRef]
542+
from abc import abstractproperty, ABCMeta
543+
def f(a: A) -> None:
544+
a.x() # E: "int" not callable
545+
a.x = 1 # E: Property "x" defined in "A" is read-only
546+
class A(metaclass=ABCMeta):
547+
@abstractproperty
548+
def x(self) -> int: pass
549+
[out]
550+
main: note: In function "f":
551+
552+
[case testReadWriteAbstractProperty]
553+
from abc import abstractproperty, ABCMeta
554+
def f(a: A) -> None:
555+
a.x.y # E: "int" has no attribute "y"
556+
a.x = 1
557+
class A(metaclass=ABCMeta):
558+
@abstractproperty
559+
def x(self) -> int: pass
560+
@x.setter
561+
def x(self, x: int) -> None: pass
562+
[out]
563+
main: note: In function "f":
564+
565+
[case testInstantiateClassWithReadOnlyAbstractProperty]
566+
from abc import abstractproperty, ABCMeta
567+
class A(metaclass=ABCMeta):
568+
@abstractproperty
569+
def x(self) -> int: pass
570+
class B(A): pass
571+
b = B() # E: Cannot instantiate abstract class 'B' with abstract attribute 'x'
572+
573+
[case testInstantiateClassWithReadWriteAbstractProperty]
574+
from abc import abstractproperty, ABCMeta
575+
class A(metaclass=ABCMeta):
576+
@abstractproperty
577+
def x(self) -> int: pass
578+
@x.setter
579+
def x(self, x: int) -> None: pass
580+
class B(A): pass
581+
b = B() # E: Cannot instantiate abstract class 'B' with abstract attribute 'x'
582+
583+
[case testImplementAbstractPropertyViaProperty]
584+
from abc import abstractproperty, ABCMeta
585+
class A(metaclass=ABCMeta):
586+
@abstractproperty
587+
def x(self) -> int: pass
588+
class B(A):
589+
@property
590+
def x(self) -> int: pass
591+
b = B()
592+
b.x() # E: "int" not callable
593+
[builtins fixtures/property.py]
594+
595+
[case testImplementReradWriteAbstractPropertyViaProperty]
596+
from abc import abstractproperty, ABCMeta
597+
class A(metaclass=ABCMeta):
598+
@abstractproperty
599+
def x(self) -> int: pass
600+
@x.setter
601+
def x(self, v: int) -> None: pass
602+
class B(A):
603+
@property
604+
def x(self) -> int: pass
605+
@x.setter
606+
def x(self, v: int) -> None: pass
607+
b = B()
608+
b.x.y # E: "int" has no attribute "y"
609+
[builtins fixtures/property.py]
610+
611+
[case testImplementAbstractPropertyViaPropertyInvalidType]
612+
from abc import abstractproperty, ABCMeta
613+
class A(metaclass=ABCMeta):
614+
@abstractproperty
615+
def x(self) -> int: pass
616+
class B(A):
617+
@property
618+
def x(self) -> str: pass # E
619+
b = B()
620+
b.x() # E
621+
[builtins fixtures/property.py]
622+
[out]
623+
main: note: In class "B":
624+
main:7: error: Return type of "x" incompatible with supertype "A"
625+
main: note: At top level:
626+
main:9: error: "str" not callable
627+
628+
[case testCantImplementAbstractPropertyViaInstanceVariable]
629+
from abc import abstractproperty, ABCMeta
630+
class A(metaclass=ABCMeta):
631+
@abstractproperty
632+
def x(self) -> int: pass
633+
class B(A):
634+
def __init__(self) -> None:
635+
self.x = 1 # E
636+
b = B() # E
637+
b.x.y # E
638+
[builtins fixtures/property.py]
639+
[out]
640+
main: note: In member "__init__" of class "B":
641+
main:7: error: Property "x" defined in "B" is read-only
642+
main: note: At top level:
643+
main:8: error: Cannot instantiate abstract class 'B' with abstract attribute 'x'
644+
main:9: error: "int" has no attribute "y"
645+
646+
[case testSuperWithAbstractProperty]
647+
from abc import abstractproperty, ABCMeta
648+
class A(metaclass=ABCMeta):
649+
@abstractproperty
650+
def x(self) -> int: pass
651+
class B(A):
652+
@property
653+
def x(self) -> int:
654+
return super().x.y # E: "int" has no attribute "y"
655+
[builtins fixtures/property.py]
656+
[out]
657+
main: note: In member "x" of class "B":
658+
659+
[case testSuperWithReadWriteAbstractProperty]
660+
from abc import abstractproperty, ABCMeta
661+
class A(metaclass=ABCMeta):
662+
@abstractproperty
663+
def x(self) -> int: pass
664+
@x.setter
665+
def x(self, v: int) -> None: pass
666+
class B(A):
667+
@property
668+
def x(self) -> int:
669+
return super().x.y # E
670+
@x.setter
671+
def x(self, v: int) -> None:
672+
super().x = '' # E
673+
[builtins fixtures/property.py]
674+
[out]
675+
main: note: In member "x" of class "B":
676+
main:10: error: "int" has no attribute "y"
677+
main: note: In function "x":
678+
main:13: error: Invalid assignment target
679+
680+
[case testOnlyImplementGetterOfReadWriteAbstractProperty]
681+
from abc import abstractproperty, ABCMeta
682+
class A(metaclass=ABCMeta):
683+
@abstractproperty
684+
def x(self) -> int: pass
685+
@x.setter
686+
def x(self, v: int) -> None: pass
687+
class B(A):
688+
@property # E
689+
def x(self) -> int: pass
690+
b = B()
691+
b.x.y # E
692+
[builtins fixtures/property.py]
693+
[out]
694+
main: note: In class "B":
695+
main:8: error: Read-only property cannot override read-write property
696+
main: note: At top level:
697+
main:11: error: "int" has no attribute "y"
698+
699+
[case testDynamicallyTypedReadOnlyAbstractProperty]
700+
from abc import abstractproperty, ABCMeta
701+
class A(metaclass=ABCMeta):
702+
@abstractproperty
703+
def x(self): pass
704+
def f(a: A) -> None:
705+
a.x.y
706+
a.x = 1 # E: Property "x" defined in "A" is read-only
707+
[out]
708+
main: note: In function "f":
709+
710+
[case testDynamicallyTypedReadOnlyAbstractPropertyForwardRef]
711+
from abc import abstractproperty, ABCMeta
712+
def f(a: A) -> None:
713+
a.x.y
714+
a.x = 1 # E: Property "x" defined in "A" is read-only
715+
class A(metaclass=ABCMeta):
716+
@abstractproperty
717+
def x(self): pass
718+
[out]
719+
main: note: In function "f":
720+
721+
[case testDynamicallyTypedReadWriteAbstractProperty]
722+
from abc import abstractproperty, ABCMeta
723+
def f(a: A) -> None:
724+
a.x.y
725+
a.x = 1
726+
class A(metaclass=ABCMeta):
727+
@abstractproperty
728+
def x(self): pass
729+
@x.setter
730+
def x(self, x): pass
731+
[out]

mypy/test/data/lib-stub/abc.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
class ABCMeta: pass
22
abstractmethod = object()
3+
abstractproperty = object()

mypy/test/data/python2eval.test

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,3 +379,19 @@ _program.py:2: error: No overload variant of "f" matches argument types [builtin
379379
def f(x): # type: (str) -> None
380380
pass
381381
f(bytearray('foo'))
382+
383+
[case testAbstractProperty_python2]
384+
from abc import abstractproperty, ABCMeta
385+
class A:
386+
__metaclass__ = ABCMeta
387+
@abstractproperty
388+
def x(self): # type: () -> int
389+
pass
390+
class B(A):
391+
@property
392+
def x(self): # type: () -> int
393+
return 3
394+
b = B()
395+
print b.x + 1
396+
[out]
397+
4

mypy/test/data/pythoneval.test

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -943,7 +943,6 @@ print(r"""\"""$""")
943943
[out]
944944
'
945945
\"""$
946-
--' (hack to fix syntax highlighting)
947946

948947
[case testSubclassBothGenericAndNonGenericABC]
949948
from typing import Generic, TypeVar
@@ -1103,3 +1102,17 @@ T = TypeVar('T')
11031102
def sorted(x: Iterable[T], *, key: Callable[[T], object] = None) -> None: ...
11041103
a = None # type: List[Dict[str, str]]
11051104
sorted(a, key=lambda y: y[''])
1105+
1106+
[case testAbstractProperty]
1107+
from abc import abstractproperty, ABCMeta
1108+
class A(metaclass=ABCMeta):
1109+
@abstractproperty
1110+
def x(self) -> int: pass
1111+
class B(A):
1112+
@property
1113+
def x(self) -> int:
1114+
return 3
1115+
b = B()
1116+
print(b.x + 1)
1117+
[out]
1118+
4

0 commit comments

Comments
 (0)