Skip to content

Commit 98e3faf

Browse files
authored
[mypyc] Support for annotating classes to be native or not (native_class=True/False) (#18802)
1 parent 16f134e commit 98e3faf

File tree

5 files changed

+143
-9
lines changed

5 files changed

+143
-9
lines changed

mypyc/irbuild/prepare.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def build_type_map(
8888
is_abstract=cdef.info.is_abstract,
8989
is_final_class=cdef.info.is_final,
9090
)
91-
class_ir.is_ext_class = is_extension_class(cdef)
91+
class_ir.is_ext_class = is_extension_class(module.path, cdef, errors)
9292
if class_ir.is_ext_class:
9393
class_ir.deletable = cdef.info.deletable_attributes.copy()
9494
# If global optimizations are disabled, turn of tracking of class children

mypyc/irbuild/util.py

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
)
3030
from mypy.semanal import refers_to_fullname
3131
from mypy.types import FINAL_DECORATOR_NAMES
32+
from mypyc.errors import Errors
3233

3334
DATACLASS_DECORATORS = {"dataclasses.dataclass", "attr.s", "attr.attrs"}
3435

@@ -125,15 +126,68 @@ def get_mypyc_attrs(stmt: ClassDef | Decorator) -> dict[str, Any]:
125126
return attrs
126127

127128

128-
def is_extension_class(cdef: ClassDef) -> bool:
129-
if any(
130-
not is_trait_decorator(d)
131-
and not is_dataclass_decorator(d)
132-
and not get_mypyc_attr_call(d)
133-
and not is_final_decorator(d)
134-
for d in cdef.decorators
135-
):
129+
def is_extension_class(path: str, cdef: ClassDef, errors: Errors) -> bool:
130+
# Check for @mypyc_attr(native_class=True/False) decorator.
131+
explicit_native_class = get_explicit_native_class(path, cdef, errors)
132+
133+
# Classes with native_class=False are explicitly marked as non extension.
134+
if explicit_native_class is False:
136135
return False
136+
137+
implicit_extension_class = is_implicit_extension_class(cdef)
138+
139+
# Classes with native_class=True should be extension classes, but they might
140+
# not be able to be due to other reasons. Print an error in that case.
141+
if explicit_native_class is True and not implicit_extension_class:
142+
errors.error(
143+
"Class is marked as native_class=True but it can't be a native class", path, cdef.line
144+
)
145+
146+
return implicit_extension_class
147+
148+
149+
def get_explicit_native_class(path: str, cdef: ClassDef, errors: Errors) -> bool | None:
150+
"""Return value of @mypyc_attr(native_class=True/False) decorator.
151+
152+
Look for a @mypyc_attr decorator with native_class=True/False and return
153+
the value assigned or None if it doesn't exist. Other values are an error.
154+
"""
155+
156+
for d in cdef.decorators:
157+
mypyc_attr_call = get_mypyc_attr_call(d)
158+
if not mypyc_attr_call:
159+
continue
160+
161+
for i, name in enumerate(mypyc_attr_call.arg_names):
162+
if name != "native_class":
163+
continue
164+
165+
arg = mypyc_attr_call.args[i]
166+
if not isinstance(arg, NameExpr):
167+
errors.error("native_class must be used with True or False only", path, cdef.line)
168+
return None
169+
170+
if arg.name == "False":
171+
return False
172+
elif arg.name == "True":
173+
return True
174+
else:
175+
errors.error("native_class must be used with True or False only", path, cdef.line)
176+
return None
177+
return None
178+
179+
180+
def is_implicit_extension_class(cdef: ClassDef) -> bool:
181+
for d in cdef.decorators:
182+
# Classes that have any decorator other than supported decorators, are not extension classes
183+
if (
184+
not is_trait_decorator(d)
185+
and not is_dataclass_decorator(d)
186+
and not get_mypyc_attr_call(d)
187+
and not is_final_decorator(d)
188+
):
189+
return False
190+
137191
if cdef.info.typeddict_type:
138192
return False
139193
if cdef.info.is_named_tuple:

mypyc/test-data/fixtures/ir.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,8 +352,10 @@ def next(i: Iterator[_T]) -> _T: pass
352352
def next(i: Iterator[_T], default: _T) -> _T: pass
353353
def hash(o: object) -> int: ...
354354
def globals() -> Dict[str, Any]: ...
355+
def hasattr(obj: object, name: str) -> bool: ...
355356
def getattr(obj: object, name: str, default: Any = None) -> Any: ...
356357
def setattr(obj: object, name: str, value: Any) -> None: ...
358+
def delattr(obj: object, name: str) -> None: ...
357359
def enumerate(x: Iterable[_T]) -> Iterator[Tuple[int, _T]]: ...
358360
@overload
359361
def zip(x: Iterable[_T], y: Iterable[_S]) -> Iterator[Tuple[_T, _S]]: ...

mypyc/test-data/irbuild-classes.test

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1345,3 +1345,28 @@ class SomeEnum(Enum):
13451345

13461346
ALIAS = Literal[SomeEnum.AVALUE]
13471347
ALIAS2 = Union[Literal[SomeEnum.AVALUE], None]
1348+
1349+
[case testMypycAttrNativeClassErrors]
1350+
from mypy_extensions import mypyc_attr
1351+
1352+
@mypyc_attr(native_class=False)
1353+
class AnnontatedNonExtensionClass:
1354+
pass
1355+
1356+
@mypyc_attr(native_class=False)
1357+
class DerivedExplicitNonNativeClass(AnnontatedNonExtensionClass):
1358+
pass
1359+
1360+
1361+
def decorator(cls):
1362+
return cls
1363+
1364+
@mypyc_attr(native_class=True)
1365+
@decorator
1366+
class NonNativeClassContradiction(): # E: Class is marked as native_class=True but it can't be a native class
1367+
pass
1368+
1369+
1370+
@mypyc_attr(native_class="yes")
1371+
class BadUse(): # E: native_class must be used with True or False only
1372+
pass

mypyc/test-data/run-classes.test

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2829,3 +2829,56 @@ Traceback (most recent call last):
28292829
File "native.py", line 5, in __del__
28302830
raise Exception("e2")
28312831
Exception: e2
2832+
2833+
[case testMypycAttrNativeClass]
2834+
from mypy_extensions import mypyc_attr
2835+
from testutil import assertRaises
2836+
2837+
@mypyc_attr(native_class=False)
2838+
class AnnontatedNonExtensionClass:
2839+
pass
2840+
2841+
class DerivedClass(AnnontatedNonExtensionClass):
2842+
pass
2843+
2844+
class ImplicitExtensionClass():
2845+
pass
2846+
2847+
@mypyc_attr(native_class=True)
2848+
class AnnotatedExtensionClass():
2849+
pass
2850+
2851+
def test_function():
2852+
setattr(AnnontatedNonExtensionClass, 'attr_class', 5)
2853+
assert(hasattr(AnnontatedNonExtensionClass, 'attr_class') == True)
2854+
assert(getattr(AnnontatedNonExtensionClass, 'attr_class') == 5)
2855+
delattr(AnnontatedNonExtensionClass, 'attr_class')
2856+
assert(hasattr(AnnontatedNonExtensionClass, 'attr_class') == False)
2857+
2858+
inst = AnnontatedNonExtensionClass()
2859+
setattr(inst, 'attr_instance', 6)
2860+
assert(hasattr(inst, 'attr_instance') == True)
2861+
assert(getattr(inst, 'attr_instance') == 6)
2862+
delattr(inst, 'attr_instance')
2863+
assert(hasattr(inst, 'attr_instance') == False)
2864+
2865+
setattr(DerivedClass, 'attr_class', 5)
2866+
assert(hasattr(DerivedClass, 'attr_class') == True)
2867+
assert(getattr(DerivedClass, 'attr_class') == 5)
2868+
delattr(DerivedClass, 'attr_class')
2869+
assert(hasattr(DerivedClass, 'attr_class') == False)
2870+
2871+
derived_inst = DerivedClass()
2872+
setattr(derived_inst, 'attr_instance', 6)
2873+
assert(hasattr(derived_inst, 'attr_instance') == True)
2874+
assert(getattr(derived_inst, 'attr_instance') == 6)
2875+
delattr(derived_inst, 'attr_instance')
2876+
assert(hasattr(derived_inst, 'attr_instance') == False)
2877+
2878+
ext_inst = ImplicitExtensionClass()
2879+
with assertRaises(AttributeError):
2880+
setattr(ext_inst, 'attr_instance', 6)
2881+
2882+
explicit_ext_inst = AnnotatedExtensionClass()
2883+
with assertRaises(AttributeError):
2884+
setattr(explicit_ext_inst, 'attr_instance', 6)

0 commit comments

Comments
 (0)