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
8 changes: 8 additions & 0 deletions mypyc/doc/native_classes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ classes:
* ``LookupError``
* ``UserWarning``

A native class can't inherit from a base class with type ``Any``, such
as a class imported from a module that isn't type checked, even
indirectly through other base classes, since mypyc needs to know the
layout of all base classes. Use a
:ref:`non-native class <non-native-classes>` instead.

By default, a non-native class can't inherit a native class, and you
can't inherit from a native class outside the compilation unit that
defines the class. You can enable these through
Expand Down Expand Up @@ -231,6 +237,8 @@ efficient as pure native classes.
If a class definition uses an unsupported class decorator, *mypyc
compiles the class into a regular Python class* (non-native class).

.. _non-native-classes:

Defining non-native classes
---------------------------

Expand Down
6 changes: 4 additions & 2 deletions mypyc/irbuild/classdef.py
Original file line number Diff line number Diff line change
Expand Up @@ -599,14 +599,16 @@ def uses_non_ext_base_exprs(cdef: ClassDef) -> bool:
"""Should the base classes of a non-extension class be evaluated as written?

Resolved base types erase Generic[...] bases and type arguments, but they are
needed at runtime, e.g. to make the class generic.
needed at runtime, e.g. to make the class generic. Any bases are missing from
resolved base types, so Python must determine the bases and instance layout.
"""
info = cdef.info
if info.is_named_tuple or info.typeddict_type is not None:
# These have synthesized base classes that need special casing.
return False
return bool(
cdef.type_args
info.fallback_to_any
or cdef.type_args
or cdef.removed_base_type_exprs
or any(isinstance(expr, IndexExpr) for expr in cdef.base_type_exprs)
)
Expand Down
59 changes: 47 additions & 12 deletions mypyc/irbuild/prepare.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
OverloadedFuncDef,
RefExpr,
SymbolNode,
TypeAlias,
TypeInfo,
Var,
)
Expand Down Expand Up @@ -539,6 +540,48 @@ def _check_deletable_declarations(path: str, cdef: ClassDef, ir: ClassIR, errors
)


def report_needs_non_native_class(errors: Errors, msg: str, path: str, line: int) -> None:
errors.error(msg, path, line)
errors.note(
"Potential workaround: @mypy_extensions.mypyc_attr(native_class=False)", path, line
)
errors.note(
"https://mypyc.readthedocs.io/en/stable/native_classes.html#defining-non-native-classes",
path,
line,
)


def check_unknown_base_classes(path: str, cdef: ClassDef, errors: Errors, mapper: Mapper) -> None:
"""Reject native classes whose layout depends on an Any base."""
if not cdef.info.fallback_to_any:
return

base_description = "a base class with an unknown layout"
for base_expr in cdef.base_type_exprs:
if isinstance(base_expr, IndexExpr):
base_expr = base_expr.base
if not isinstance(base_expr, (NameExpr, MemberExpr)):
continue
base_node = base_expr.node
if isinstance(base_node, TypeAlias):
target = get_proper_type(base_node.target)
if isinstance(target, Instance):
base_node = target.type
if not isinstance(base_node, TypeInfo) or base_node.fallback_to_any:
base_description = f'"{base_expr.name}" (unknown base class layout)'
break
msg = f"Native classes cannot inherit from {base_description}"
if any(
base.fallback_to_any and base in mapper.type_to_ir and mapper.type_to_ir[base].is_ext_class
for base in cdef.info.mro[1:]
):
# A native base class already reported the error with the notes
errors.error(msg, path, cdef.line)
else:
report_needs_non_native_class(errors, msg, path, cdef.line)


def prepare_class_def(
path: str,
module_name: str,
Expand All @@ -556,6 +599,8 @@ def prepare_class_def(
ir = mapper.type_to_ir[cdef.info]
info = cdef.info

check_unknown_base_classes(path, cdef, errors, mapper)

attrs, attrs_lines = get_mypyc_attrs(cdef, path, errors)
if attrs.get("allow_interpreted_subclasses") is True:
ir.allow_interpreted_subclasses = True
Expand All @@ -580,18 +625,8 @@ def prepare_class_def(
# isn't in builtins, bad things will happen and we won't
# catch it here! But this should catch a lot of the most
# common pitfalls.
errors.error(
"Inheriting from most builtin types is unimplemented", path, cdef.line
)
errors.note(
"Potential workaround: @mypy_extensions.mypyc_attr(native_class=False)",
path,
cdef.line,
)
errors.note(
"https://mypyc.readthedocs.io/en/stable/native_classes.html#defining-non-native-classes",
path,
cdef.line,
report_needs_non_native_class(
errors, "Inheriting from most builtin types is unimplemented", path, cdef.line
)

free_list_len = attrs.get("free_list_len")
Expand Down
84 changes: 84 additions & 0 deletions mypyc/test-data/irbuild-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -1718,6 +1718,90 @@ class NonNativeClassContradiction(): # E: Class is marked as native_class=True
class BadUse(): # E: native_class must be used with True or False only
pass

[case testNativeClassUnknownBase]
from mypy_extensions import mypyc_attr
from typing import Any
from missing_module import Base # type: ignore

class C(Base): # E: Native classes cannot inherit from "Base" (unknown base class layout) \
# N: Potential workaround: @mypy_extensions.mypyc_attr(native_class=False) \
# N: https://mypyc.readthedocs.io/en/stable/native_classes.html#defining-non-native-classes
pass

class D(C): # E: Native classes cannot inherit from "C" (unknown base class layout)
pass

@mypyc_attr(native_class=False)
class NonNative(Base):
pass

class NativeChild(NonNative): # E: Native classes cannot inherit from "NonNative" (unknown base class layout) \
# N: Potential workaround: @mypy_extensions.mypyc_attr(native_class=False) \
# N: https://mypyc.readthedocs.io/en/stable/native_classes.html#defining-non-native-classes
pass

ExplicitAny: Any = str
class S(ExplicitAny): # E: Native classes cannot inherit from "ExplicitAny" (unknown base class layout) \
# N: Potential workaround: @mypy_extensions.mypyc_attr(native_class=False) \
# N: https://mypyc.readthedocs.io/en/stable/native_classes.html#defining-non-native-classes
pass

class Known:
pass

class KnownFirst(Known, Base): # E: Native classes cannot inherit from "Base" (unknown base class layout) \
# N: Potential workaround: @mypy_extensions.mypyc_attr(native_class=False) \
# N: https://mypyc.readthedocs.io/en/stable/native_classes.html#defining-non-native-classes
pass

class UnknownFirst(Base, Known): # E: Native classes cannot inherit from "Base" (unknown base class layout) \
# N: Potential workaround: @mypy_extensions.mypyc_attr(native_class=False) \
# N: https://mypyc.readthedocs.io/en/stable/native_classes.html#defining-non-native-classes
pass

class TwoUnknown(Base, ExplicitAny): # E: Native classes cannot inherit from "Base" (unknown base class layout) \
# N: Potential workaround: @mypy_extensions.mypyc_attr(native_class=False) \
# N: https://mypyc.readthedocs.io/en/stable/native_classes.html#defining-non-native-classes
pass

Alias = Known
class AliasFirst(Alias, Base): # E: Native classes cannot inherit from "Base" (unknown base class layout) \
# N: Potential workaround: @mypy_extensions.mypyc_attr(native_class=False) \
# N: https://mypyc.readthedocs.io/en/stable/native_classes.html#defining-non-native-classes
pass

class Indexed(Base[int]): # type: ignore # E: Native classes cannot inherit from "Base" (unknown base class layout) \
# N: Potential workaround: @mypy_extensions.mypyc_attr(native_class=False) \
# N: https://mypyc.readthedocs.io/en/stable/native_classes.html#defining-non-native-classes
pass

def make_base() -> Any: ...
class Dynamic(make_base()): # type: ignore # E: Native classes cannot inherit from a base class with an unknown layout \
# N: Potential workaround: @mypy_extensions.mypyc_attr(native_class=False) \
# N: https://mypyc.readthedocs.io/en/stable/native_classes.html#defining-non-native-classes
pass

[case testNativeClassIndirectUnknownBase]
# Any base class anywhere in the MRO makes the layout unknown
from stubbed import Indirect, IndirectAlias

class C(Indirect): # E: Native classes cannot inherit from "Indirect" (unknown base class layout) \
# N: Potential workaround: @mypy_extensions.mypyc_attr(native_class=False) \
# N: https://mypyc.readthedocs.io/en/stable/native_classes.html#defining-non-native-classes
pass

class D(IndirectAlias): # E: Native classes cannot inherit from "IndirectAlias" (unknown base class layout) \
# N: Potential workaround: @mypy_extensions.mypyc_attr(native_class=False) \
# N: https://mypyc.readthedocs.io/en/stable/native_classes.html#defining-non-native-classes
pass

[file stubbed.pyi]
from typing import Any

class AnyBase(Any): ...
class Indirect(AnyBase): ...
IndirectAlias = Indirect

[case testMypycAttrNativeClassMetaError]
from mypy_extensions import mypyc_attr

Expand Down
Loading
Loading