Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions mypyc/irbuild/prepare.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
Decorator,
Expression,
FuncDef,
IndexExpr,
MemberExpr,
MypyFile,
NameExpr,
Expand Down Expand Up @@ -118,6 +119,12 @@ def build_type_map(
module.path, module.fullname, cdef, errors, mapper, options
)

# Validate cross-class properties after all ClassIR flags are populated.
for module, cdef in classes:
with catch_errors(module.path, cdef.line):
if mapper.type_to_ir[cdef.info].is_ext_class:
validate_acyclic_class_bases(module.path, cdef, errors, mapper)

# Prepare implicit attribute accessors as needed if an attribute overrides a property.
for module, cdef in classes:
class_ir = mapper.type_to_ir[cdef.info]
Expand Down Expand Up @@ -345,6 +352,51 @@ def can_subclass_builtin(builtin_base: str) -> bool:
)


def get_removed_base_fullname(expr: Expression) -> str | None:
if isinstance(expr, IndexExpr):
expr = expr.base
if isinstance(expr, RefExpr):
return expr.fullname
return None


def find_non_acyclic_base(cdef: ClassDef, mapper: Mapper) -> str | None:
if cdef.type_args:
return "typing.Generic"

for expr in cdef.removed_base_type_exprs:
if fullname := get_removed_base_fullname(expr):
return fullname
return "a removed base class"

for base in cdef.info.mro[1:]:
if base.fullname == "builtins.object":
continue

base_ir = mapper.type_to_ir.get(base)
if base_ir is not None and base_ir.is_acyclic:
continue

return base.fullname

return None


def validate_acyclic_class_bases(
path: str, cdef: ClassDef, errors: Errors, mapper: Mapper
) -> None:
ir = mapper.type_to_ir[cdef.info]
if not ir.is_acyclic:
return

if fullname := find_non_acyclic_base(cdef, mapper):
errors.error(
f'"acyclic" can\'t be used in a class that inherits from non-acyclic type "{fullname}"',
path,
cdef.line,
)


def prepare_class_def(
path: str,
module_name: str,
Expand Down
41 changes: 41 additions & 0 deletions mypyc/test-data/irbuild-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -2138,6 +2138,47 @@ class NonNative:
class InterpSub:
pass

[case testAcyclicClassRequiresAcyclicBases]
from typing import Generic, TypeVar
from mypy_extensions import mypyc_attr, trait

T = TypeVar("T")

class NonAcyclicBase:
pass

@trait
class TraitBase:
pass

@mypyc_attr(native_class=False)
class NonNativeBase:
pass

@mypyc_attr(acyclic=True)
class AcyclicBase:
pass

@mypyc_attr(acyclic=True)
class GoodDerived(AcyclicBase):
pass

@mypyc_attr(acyclic=True)
class BadDerived(NonAcyclicBase): # E: "acyclic" can't be used in a class that inherits from non-acyclic type "__main__.NonAcyclicBase"
pass

@mypyc_attr(acyclic=True)
class BadGeneric(Generic[T]): # E: "acyclic" can't be used in a class that inherits from non-acyclic type "typing.Generic"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about testing traits and non-native classes as bases (fine to do this in a follow-up PR)?

pass

@mypyc_attr(acyclic=True)
class BadDerivedTrait(TraitBase): # E: "acyclic" can't be used in a class that inherits from non-acyclic type "__main__.TraitBase"
pass

@mypyc_attr(acyclic=True)
class BadDerivedNonNative(NonNativeBase): # E: "acyclic" can't be used in a class that inherits from non-acyclic type "__main__.NonNativeBase"
pass

[case testUnsupportedGetAttr]
from mypy_extensions import mypyc_attr

Expand Down
6 changes: 6 additions & 0 deletions mypyc/test-data/irbuild-python312.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[case testAcyclicGenericIsRejected_python3_12]
from mypy_extensions import mypyc_attr

@mypyc_attr(acyclic=True)
class BadGeneric[T]: # E: "acyclic" can't be used in a class that inherits from non-acyclic type "typing.Generic"
pass
3 changes: 3 additions & 0 deletions mypyc/test/test_irbuild.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@
"irbuild-match.test",
]

if sys.version_info >= (3, 12):
files.append("irbuild-python312.test")

if sys.version_info >= (3, 14):
files.append("irbuild-python314.test")

Expand Down
Loading