From 07dca16eae0f51622971ed248824338e642c8d04 Mon Sep 17 00:00:00 2001 From: KotlinIsland <65446343+kotlinisland@users.noreply.github.com> Date: Wed, 18 Dec 2024 01:01:58 +1000 Subject: [PATCH] abstract and read only --- CHANGELOG.md | 3 + docs/source/based_features.rst | 49 +++++++++++++ mypy/checker.py | 8 +++ mypy/checkmember.py | 2 + mypy/message_registry.py | 1 + mypy/messages.py | 24 +++++-- mypy/metastore.py | 2 +- mypy/nodes.py | 10 +-- mypy/plugin.py | 8 +-- mypy/report.py | 2 +- mypy/semanal.py | 83 +++++++++++++++++++--- mypy/semanal_classprop.py | 32 +++++++-- mypy/semanal_shared.py | 4 +- mypy/stubutil.py | 2 +- mypy/test/data.py | 2 +- mypy/type_visitor.py | 6 +- mypy/typeanal.py | 11 ++- mypy/types.py | 4 +- mypy/visitor.py | 6 +- mypyc/analysis/dataflow.py | 2 +- mypyc/ir/ops.py | 10 +-- mypyc/ir/rtypes.py | 4 +- mypyc/irbuild/builder.py | 2 +- mypyc/irbuild/classdef.py | 2 +- mypyc/irbuild/nonlocalcontrol.py | 4 +- mypyc/test/testutil.py | 2 +- test-data/unit/check-based-abstract.test | 26 +++++++ test-data/unit/check-based-modifiers.test | 28 ++++++++ test-data/unit/check-based-read-only.test | 19 +++++ test-data/unit/check-based-union-join.test | 6 +- test-data/unit/check-classes.test | 2 +- test-data/unit/check-incremental.test | 2 +- test-data/unit/fixtures/typing-full.pyi | 10 +-- test-data/unit/lib-stub/basedtyping.pyi | 8 ++- test-data/unit/semanal-classvar.test | 2 +- 35 files changed, 318 insertions(+), 70 deletions(-) create mode 100644 test-data/unit/check-based-abstract.test create mode 100644 test-data/unit/check-based-modifiers.test create mode 100644 test-data/unit/check-based-read-only.test diff --git a/CHANGELOG.md b/CHANGELOG.md index 68d1bae2a..11e179c25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Basedmypy Changelog ## [Unreleased] +### Added +- `Abstract` and `abstract` modifiers +- `ReadOnly` attributes ## [2.9.0] ### Added diff --git a/docs/source/based_features.rst b/docs/source/based_features.rst index ffc6c1a00..570c9706d 100644 --- a/docs/source/based_features.rst +++ b/docs/source/based_features.rst @@ -158,6 +158,55 @@ represent a set of choices that the ``TypeVar`` can be replaced with: type C = B[object] # mypy doesn't report the error here +Abstract Classes +---------------- + +abstract classes are more strict: + +.. code-block:: python + + class A: # error: abstract class not denoted as abstract + @abstractmethod + def f(self): ... + +and more flexable: + +.. code-block:: python + + from basedtyping import abstract + + @abstract + class A: + @abstract + def f(self): ... + + +and there are abstract attributes: + +.. code-block:: python + + from basedtyping import abstract, Abstract + + @abstract + class A: + a: Abstract[int] + + +Read-only attributes +-------------------- + +simply: + +.. code-block:: python + + from typing import ReadOnly + + class A: + a: ReadOnly[int] + + A().a = 1 # error: A.a is read-only + + Reinvented type guards ---------------------- diff --git a/mypy/checker.py b/mypy/checker.py index 0fdb34335..9d36a4c52 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3604,6 +3604,14 @@ def check_assignment( self.infer_variable_type(inferred, lvalue, rvalue_type, rvalue) self.check_assignment_to_slots(lvalue) + if ( + isinstance(lvalue, NameExpr) + and isinstance(lvalue.node, Var) + and lvalue.node.is_read_only + and not self.get_final_context() + ): + self.msg.read_only(lvalue.node.name, rvalue) + # (type, operator) tuples for augmented assignments supported with partial types partial_type_augmented_ops: Final = {("builtins.list", "+"), ("builtins.set", "|")} diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 29b9961ec..c7d673b77 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -807,6 +807,8 @@ def analyze_var( if mx.is_lvalue and var.is_property and not var.is_settable_property: # TODO allow setting attributes in subclass (although it is probably an error) mx.msg.read_only_property(name, itype.type, mx.context) + if mx.is_lvalue and var.is_read_only: + mx.msg.read_only(name, mx.context, itype.type) if mx.is_lvalue and var.is_classvar: mx.msg.cant_assign_to_classvar(name, mx.context) t = freshen_all_functions_type_vars(typ) diff --git a/mypy/message_registry.py b/mypy/message_registry.py index 608a3783e..6bf4ec031 100644 --- a/mypy/message_registry.py +++ b/mypy/message_registry.py @@ -266,6 +266,7 @@ def with_additional_msg(self, info: str) -> ErrorMessage: CLASS_VAR_WITH_TYPEVARS: Final = "ClassVar cannot contain type variables" CLASS_VAR_WITH_GENERIC_SELF: Final = "ClassVar cannot contain Self type in generic classes" CLASS_VAR_OUTSIDE_OF_CLASS: Final = "ClassVar can only be used for assignments in class body" +ABSTRACT_OUTSIDE_OF_CLASS: Final = "`Abstract` can only be used for assignments in a class body" # Protocol RUNTIME_PROTOCOL_EXPECTED: Final = ErrorMessage( diff --git a/mypy/messages.py b/mypy/messages.py index 319b6ca89..27cb41df1 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -1569,12 +1569,13 @@ def incompatible_conditional_function_def( def cannot_instantiate_abstract_class( self, class_name: str, abstract_attributes: dict[str, bool], context: Context ) -> None: - attrs = format_string_list([f'"{a}"' for a in abstract_attributes]) + if abstract_attributes: + attrs = format_string_list([f'"{a}"' for a in abstract_attributes]) + rest = f" with abstract attribute{plural_s(abstract_attributes)} {attrs}" + else: + rest = "" self.fail( - f'Cannot instantiate abstract class "{class_name}" with abstract ' - f"attribute{plural_s(abstract_attributes)} {attrs}", - context, - code=codes.ABSTRACT, + f'Cannot instantiate abstract class "{class_name}"{rest}', context, code=codes.ABSTRACT ) attrs_with_none = [ f'"{a}"' @@ -1655,7 +1656,18 @@ def final_without_value(self, ctx: Context) -> None: self.fail("Final name must be initialized with a value", ctx) def read_only_property(self, name: str, type: TypeInfo, context: Context) -> None: - self.fail(f'Property "{name}" defined in "{type.name}" is read-only', context) + self.fail( + f'Property "{name}" defined in "{type.name}" is read-only', + context, + code=ErrorCode("read-only", "", ""), + ) + + def read_only(self, name: str, context: Context, type: TypeInfo | None = None) -> None: + if type is None: + prefix = f'Name "{name}"' + else: + prefix = f'Attribute "{name}" defined in "{type.name}"' + self.fail(f"{prefix} is read only", context, code=ErrorCode("read-only", "", "")) def incompatible_typevar_value( self, diff --git a/mypy/metastore.py b/mypy/metastore.py index ece397360..d3f1bfdd5 100644 --- a/mypy/metastore.py +++ b/mypy/metastore.py @@ -22,7 +22,7 @@ import sqlite3 -class MetadataStore: +class MetadataStore: # type: ignore[abstract] """Generic interface for metadata storage.""" @abstractmethod diff --git a/mypy/nodes.py b/mypy/nodes.py index 8990f7780..c0f0a9de3 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -247,7 +247,7 @@ class FakeExpression(Expression): @trait -class SymbolNode(Node): +class SymbolNode(Node): # type: ignore[abstract] """Nodes that can be stored in a symbol table.""" __slots__ = () @@ -505,7 +505,7 @@ def accept(self, visitor: StatementVisitor[T]) -> T: FUNCBASE_FLAGS: Final = ["is_property", "is_class", "is_static", "is_final", "is_type_check_only"] -class FuncBase(Node): +class FuncBase(Node): # type: ignore[abstract] """Abstract base class for function-like nodes. N.B: Although this has SymbolNode subclasses (FuncDef, @@ -710,7 +710,7 @@ def __init__( ] -class FuncItem(FuncBase): +class FuncItem(FuncBase): # type: ignore[abstract] """Base class for nodes usable as overloaded function items.""" __slots__ = ( @@ -1021,6 +1021,7 @@ class Var(SymbolNode): "is_settable_property", "is_classvar", "is_abstract_var", + "is_read_only", "is_final", "is_index_var", "final_unset_in_class", @@ -1058,6 +1059,7 @@ def __init__(self, name: str, type: mypy.types.Type | None = None) -> None: self.is_classvar = False self.is_abstract_var = False self.is_index_var = False + self.is_read_only = False # Set to true when this variable refers to a module we were unable to # parse for some reason (eg a silenced module) self.is_suppressed_import = False @@ -2558,7 +2560,7 @@ def accept(self, visitor: ExpressionVisitor[T]) -> T: VARIANCE_NOT_READY: Final = 3 # Variance hasn't been inferred (using Python 3.12 syntax) -class TypeVarLikeExpr(SymbolNode, Expression): +class TypeVarLikeExpr(SymbolNode, Expression): # type: ignore[abstract] """Base class for TypeVarExpr, ParamSpecExpr and TypeVarTupleExpr. Note that they are constructed by the semantic analyzer. diff --git a/mypy/plugin.py b/mypy/plugin.py index fcbbc32f6..6c2750708 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -151,7 +151,7 @@ class C: pass @trait -class TypeAnalyzerPluginInterface: +class TypeAnalyzerPluginInterface: # type: ignore[abstract] """Interface for accessing semantic analyzer functionality in plugins. Methods docstrings contain only basic info. Look for corresponding implementation @@ -195,7 +195,7 @@ class AnalyzeTypeContext(NamedTuple): @mypyc_attr(allow_interpreted_subclasses=True) -class CommonPluginApi: +class CommonPluginApi: # type: ignore[abstract] """ A common plugin API (shared between semantic analysis and type checking phases) that all plugin hooks get independently of the context. @@ -217,7 +217,7 @@ def lookup_fully_qualified(self, fullname: str) -> SymbolTableNode | None: @trait -class CheckerPluginInterface: +class CheckerPluginInterface: # type: ignore[abstract] """Interface for accessing type checker functionality in plugins. Methods docstrings contain only basic info. Look for corresponding implementation @@ -254,7 +254,7 @@ def get_expression_type(self, node: Expression, type_context: Type | None = None @trait -class SemanticAnalyzerPluginInterface: +class SemanticAnalyzerPluginInterface: # type: ignore[abstract] """Interface for accessing semantic analyzer functionality in plugins. Methods docstrings contain only basic info. Look for corresponding implementation diff --git a/mypy/report.py b/mypy/report.py index 80e0ae21c..1c0bcebe7 100644 --- a/mypy/report.py +++ b/mypy/report.py @@ -705,7 +705,7 @@ def on_finish(self) -> None: register_reporter("cobertura-xml", CoberturaXmlReporter, needs_lxml=True) -class AbstractXmlReporter(AbstractReporter): +class AbstractXmlReporter(AbstractReporter): # type: ignore[abstract] """Internal abstract class for reporters that work via XML.""" def __init__(self, reports: Reports, output_dir: str) -> None: diff --git a/mypy/semanal.py b/mypy/semanal.py index 2f34392c4..daef97408 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1821,7 +1821,7 @@ def visit_decorator(self, dec: Decorator) -> None: could_be_decorated_property = False for i, d in enumerate(dec.decorators): # A bunch of decorators are special cased here. - if refers_to_fullname(d, "abc.abstractmethod"): + if refers_to_fullname(d, ("abc.abstractmethod", "basedtyping.abstract")): removed.append(i) dec.func.abstract_status = IS_ABSTRACT self.check_decorated_function_is_method("abstractmethod", dec) @@ -1847,6 +1847,7 @@ def visit_decorator(self, dec: Decorator) -> None: ( "builtins.property", "abc.abstractproperty", + "basedtyping.abstract", "functools.cached_property", "enum.property", "types.DynamicClassAttribute", @@ -1855,7 +1856,7 @@ def visit_decorator(self, dec: Decorator) -> None: removed.append(i) dec.func.is_property = True dec.var.is_property = True - if refers_to_fullname(d, "abc.abstractproperty"): + if refers_to_fullname(d, ("abc.abstractproperty", "basedtyping.abstract")): dec.func.abstract_status = IS_ABSTRACT elif refers_to_fullname(d, "functools.cached_property"): dec.var.is_settable_property = True @@ -2340,6 +2341,8 @@ def analyze_class_decorator_common( """ if refers_to_fullname(decorator, FINAL_DECORATOR_NAMES): info.is_final = True + elif refers_to_fullname(decorator, "basedtyping.abstract"): + info.is_abstract = True elif refers_to_fullname(decorator, TYPE_CHECK_ONLY_NAMES): info.is_type_check_only = True elif (deprecated := self.get_deprecated(decorator)) is not None: @@ -3410,7 +3413,9 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: self.analyze_lvalues(s) self.check_final_implicit_def(s) self.store_final_status(s) - self.check_classvar(s) + # this is a bit lazy, but gets the job done + while self.check_abstract(s) or self.check_classvar(s) or self.check_read_only(s): + pass self.process_type_annotation(s) self.apply_dynamic_class_hook(s) if not s.type: @@ -5226,13 +5231,13 @@ def analyze_value_types(self, items: list[Expression]) -> list[Type]: result.append(AnyType(TypeOfAny.from_error)) return result - def check_classvar(self, s: AssignmentStmt) -> None: + def check_classvar(self, s: AssignmentStmt) -> bool: """Check if assignment defines a class variable.""" lvalue = s.lvalues[0] if len(s.lvalues) != 1 or not isinstance(lvalue, RefExpr): - return + return False if not s.type or not self.is_classvar(s.type): - return + return False if self.is_class_scope() and isinstance(lvalue, NameExpr): node = lvalue.node if isinstance(node, Var): @@ -5257,8 +5262,51 @@ def check_classvar(self, s: AssignmentStmt) -> None: # In case of member access, report error only when assigning to self # Other kinds of member assignments should be already reported self.fail_invalid_classvar(lvalue) + if s.type.args: + s.type = s.type.args[0] + else: + s.type = None + return True + + def check_abstract(self, s: AssignmentStmt) -> bool: + """Check if assignment defines an abstract variable.""" + lvalue = s.lvalues[0] + if len(s.lvalues) != 1 or not isinstance(lvalue, RefExpr): + return False + if not s.type or not self.is_abstract(s.type): + return False + if self.is_class_scope() and isinstance(lvalue, NameExpr): + node = lvalue.node + if isinstance(node, Var): + node.is_abstract_var = True + assert self.type is not None + elif not isinstance(lvalue, MemberExpr) or self.is_self_member_ref(lvalue): + # In case of member access, report error only when assigning to self + # Other kinds of member assignments should be already reported + self.fail_invalid_abstract(lvalue) + s.type = s.type.args[0] + return True - def is_classvar(self, typ: Type) -> bool: + def check_read_only(self, s: AssignmentStmt) -> bool: + """Check if assignment defines a read only variable.""" + lvalue = s.lvalues[0] + if len(s.lvalues) != 1 or not isinstance(lvalue, RefExpr): + return False + if not s.type or not self.is_read_only_type(s.type): + return False + node = lvalue.node + if isinstance(node, Var): + node.is_read_only = True + s.is_final_def = True + if not mypy.options._based: + return False + if s.type.args: + s.type = s.type.args[0] + else: + s.type = None + return True + + def is_classvar(self, typ: Type) -> typ is UnboundType if True else False: if not isinstance(typ, UnboundType): return False sym = self.lookup_qualified(typ.name, typ) @@ -5266,7 +5314,15 @@ def is_classvar(self, typ: Type) -> bool: return False return sym.node.fullname == "typing.ClassVar" - def is_final_type(self, typ: Type | None) -> bool: + def is_abstract(self, typ: Type) -> typ is UnboundType if True else False: + if not isinstance(typ, UnboundType): + return False + sym = self.lookup_qualified(typ.name, typ) + if not sym or not sym.node: + return False + return sym.node.fullname == "basedtyping.Abstract" + + def is_final_type(self, typ: Type | None) -> typ is UnboundType if True else False: if not isinstance(typ, UnboundType): return False sym = self.lookup_qualified(typ.name, typ) @@ -5274,9 +5330,20 @@ def is_final_type(self, typ: Type | None) -> bool: return False return sym.node.fullname in FINAL_TYPE_NAMES + def is_read_only_type(self, typ: Type | None) -> typ is UnboundType if True else False: + if not isinstance(typ, UnboundType): + return False + sym = self.lookup_qualified(typ.name, typ) + if not sym or not sym.node: + return False + return sym.node.fullname in ("typing.ReadOnly", "typing_extensions.ReadOnly") + def fail_invalid_classvar(self, context: Context) -> None: self.fail(message_registry.CLASS_VAR_OUTSIDE_OF_CLASS, context) + def fail_invalid_abstract(self, context: Context) -> None: + self.fail(message_registry.ABSTRACT_OUTSIDE_OF_CLASS, context) + def process_module_assignment( self, lvals: list[Lvalue], rval: Expression, ctx: AssignmentStmt ) -> None: diff --git a/mypy/semanal_classprop.py b/mypy/semanal_classprop.py index c5ad34122..286a51f93 100644 --- a/mypy/semanal_classprop.py +++ b/mypy/semanal_classprop.py @@ -7,6 +7,8 @@ from typing import Final +import mypy.options +from mypy import errorcodes from mypy.errors import Errors from mypy.nodes import ( IMPLICITLY_ABSTRACT, @@ -23,6 +25,7 @@ ) from mypy.options import Options from mypy.types import MYPYC_NATIVE_INT_NAMES, Instance, ProperType +from mypy.util import plural_s # Hard coded type promotions (shared between all Python versions). # These add extra ad-hoc edges to the subtyping relation. For example, @@ -46,7 +49,7 @@ def calculate_class_abstract_status(typ: TypeInfo, is_stub_file: bool, errors: E abstract attribute. Also compute a list of abstract attributes. Report error is required ABCMeta metaclass is missing. """ - typ.is_abstract = False + was_abstract = typ.is_abstract typ.abstract_attributes = [] if typ.typeddict_type: return # TypedDict can't be abstract @@ -91,24 +94,39 @@ def calculate_class_abstract_status(typ: TypeInfo, is_stub_file: bool, errors: E if base is typ: abstract_in_this_class.append(name) concrete.add(name) - # In stubs, abstract classes need to be explicitly marked because it is too + # In programming, abstract classes need to be explicitly marked because it is too # easy to accidentally leave a concrete class abstract by forgetting to # implement some methods. typ.abstract_attributes = sorted(abstract) - if is_stub_file: + if mypy.options._based or is_stub_file: if typ.declared_metaclass and typ.declared_metaclass.type.has_base("abc.ABCMeta"): return + if any(base.type.fullname == "abc.ABC" for base in typ.bases): + return if typ.is_protocol: return - if abstract and not abstract_in_this_class: + if was_abstract: + return + derives_protocol = any(base.type.is_protocol for base in typ.bases) + if abstract and (not abstract_in_this_class or mypy.options._based): def report(message: str, severity: str) -> None: - errors.report(typ.line, typ.column, message, severity=severity) + errors.report( + typ.line, typ.column, message, severity=severity, code=errorcodes.ABSTRACT + ) attrs = ", ".join(f'"{attr}"' for attr, _ in sorted(abstract)) - report(f"Class {typ.fullname} has abstract attributes {attrs}", "error") report( - "If it is meant to be abstract, add 'abc.ABCMeta' as an explicit metaclass", "note" + f"Class {typ.fullname} has abstract attribute{plural_s(abstract)} {attrs}", "error" + ) + if derives_protocol: + report( + "If it is meant to be a protocol, add `typing.Protocol` as a base class", + "note", + ) + report( + "If it is meant to be abstract, decorate the class with `basedtyping.abstract`, or add `abc.ABC` as a base class, or `abc.ABCMeta` as the metaclass", + "note", ) if typ.is_final and abstract: attrs = ", ".join(f'"{attr}"' for attr, _ in sorted(abstract)) diff --git a/mypy/semanal_shared.py b/mypy/semanal_shared.py index cb0bdebab..77cf7d5b1 100644 --- a/mypy/semanal_shared.py +++ b/mypy/semanal_shared.py @@ -63,7 +63,7 @@ @trait -class SemanticAnalyzerCoreInterface: +class SemanticAnalyzerCoreInterface: # type: ignore[abstract] """A core abstract interface to generic semantic analyzer functionality. This is implemented by both semantic analyzer passes 2 and 3. @@ -143,7 +143,7 @@ def type(self) -> TypeInfo | None: @trait -class SemanticAnalyzerInterface(SemanticAnalyzerCoreInterface): +class SemanticAnalyzerInterface(SemanticAnalyzerCoreInterface): # type: ignore[abstract] """A limited abstract interface to some generic semantic analyzer pass 2 functionality. We use this interface for various reasons: diff --git a/mypy/stubutil.py b/mypy/stubutil.py index a3b78e1d2..7e81a72d3 100644 --- a/mypy/stubutil.py +++ b/mypy/stubutil.py @@ -400,7 +400,7 @@ def infer_method_arg_types( @mypyc_attr(allow_interpreted_subclasses=True) -class SignatureGenerator: +class SignatureGenerator: # type: ignore[abstract] """Abstract base class for extracting a list of FunctionSigs for each function.""" def remove_self_type( diff --git a/mypy/test/data.py b/mypy/test/data.py index 25f94fd10..2e8a36179 100644 --- a/mypy/test/data.py +++ b/mypy/test/data.py @@ -806,7 +806,7 @@ def has_stable_flags(testcase: DataDrivenTestCase) -> bool: return True -class DataSuite: +class DataSuite: # type: ignore[abstract] # option fields - class variables files: list[str] diff --git a/mypy/type_visitor.py b/mypy/type_visitor.py index 044a1afd0..cc00040d7 100644 --- a/mypy/type_visitor.py +++ b/mypy/type_visitor.py @@ -57,7 +57,7 @@ @trait @mypyc_attr(allow_interpreted_subclasses=True) -class TypeVisitor(Generic[T]): +class TypeVisitor(Generic[T]): # type: ignore[abstract] """Visitor class for types (Type subclasses). The parameter T is the return type of the visit methods. @@ -157,7 +157,7 @@ def visit_typeguard_type(self, t: TypeGuardType) -> T: @trait @mypyc_attr(allow_interpreted_subclasses=True) -class SyntheticTypeVisitor(TypeVisitor[T]): +class SyntheticTypeVisitor(TypeVisitor[T]): # type: ignore[abstract] """A TypeVisitor that also knows how to visit synthetic AST constructs. Not just real types. @@ -185,7 +185,7 @@ def visit_placeholder_type(self, t: PlaceholderType) -> T: @mypyc_attr(allow_interpreted_subclasses=True) -class TypeTranslator(TypeVisitor[Type]): +class TypeTranslator(TypeVisitor[Type]): # type: ignore[abstract] """Identity type transformation. Subclass this and override some methods to implement a non-trivial diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 9d6d0ec4d..c9f8c0c22 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -737,6 +737,15 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ return self.anal_type( t.args[0], allow_typed_dict_special_forms=self.allow_typed_dict_special_forms ) + elif fullname == "basedtyping.Abstract": + if not t.args: + self.fail( + "Abstract[...] must have exactly one type argument", t, code=codes.VALID_TYPE + ) + return AnyType(TypeOfAny.from_error) + return self.anal_type( + t.args[0], allow_typed_dict_special_forms=self.allow_typed_dict_special_forms + ) elif fullname in ("typing_extensions.Required", "typing.Required"): if not self.allow_typed_dict_special_forms: self.fail( @@ -770,7 +779,7 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ self.anal_type(t.args[0], allow_typed_dict_special_forms=True), required=False ) elif fullname in ("typing_extensions.ReadOnly", "typing.ReadOnly"): - if not self.allow_typed_dict_special_forms: + if not mypy.options._based and not self.allow_typed_dict_special_forms: self.fail( "ReadOnly[] can be only used in a TypedDict definition", t, diff --git a/mypy/types.py b/mypy/types.py index 6036065c4..34b5f8076 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1743,7 +1743,7 @@ def is_singleton_type(self) -> bool: ) -class FunctionLike(ProperType): +class FunctionLike(ProperType): # type: ignore[abstract] """Abstract base class for function types.""" __slots__ = ("fallback",) @@ -3962,7 +3962,7 @@ def union_str(self, a: Iterable[Type]) -> str: return " | ".join(res) -class TrivialSyntheticTypeTranslator(TypeTranslator, SyntheticTypeVisitor[Type]): +class TrivialSyntheticTypeTranslator(TypeTranslator, SyntheticTypeVisitor[Type]): # type: ignore[abstract] """A base class for type translators that need to be run during semantic analysis.""" def visit_placeholder_type(self, t: PlaceholderType) -> Type: diff --git a/mypy/visitor.py b/mypy/visitor.py index 340e1af64..e35bf87fe 100644 --- a/mypy/visitor.py +++ b/mypy/visitor.py @@ -18,7 +18,7 @@ @trait @mypyc_attr(allow_interpreted_subclasses=True) -class ExpressionVisitor(Generic[T]): +class ExpressionVisitor(Generic[T]): # type: ignore[abstract] @abstractmethod def visit_int_expr(self, o: mypy.nodes.IntExpr) -> T: pass @@ -198,7 +198,7 @@ def visit_temp_node(self, o: mypy.nodes.TempNode) -> T: @trait @mypyc_attr(allow_interpreted_subclasses=True) -class StatementVisitor(Generic[T]): +class StatementVisitor(Generic[T]): # type: ignore[abstract] # Definitions @abstractmethod @@ -316,7 +316,7 @@ def visit_type_alias_stmt(self, o: mypy.nodes.TypeAliasStmt) -> T: @trait @mypyc_attr(allow_interpreted_subclasses=True) -class PatternVisitor(Generic[T]): +class PatternVisitor(Generic[T]): # type: ignore[abstract] @abstractmethod def visit_as_pattern(self, o: mypy.patterns.AsPattern) -> T: pass diff --git a/mypyc/analysis/dataflow.py b/mypyc/analysis/dataflow.py index 411fc8093..9f77c383c 100644 --- a/mypyc/analysis/dataflow.py +++ b/mypyc/analysis/dataflow.py @@ -169,7 +169,7 @@ def __str__(self) -> str: GenAndKill = Tuple[Set[T], Set[T]] -class BaseAnalysisVisitor(OpVisitor[GenAndKill[T]]): +class BaseAnalysisVisitor(OpVisitor[GenAndKill[T]]): # type: ignore[abstract] def visit_goto(self, op: Goto) -> GenAndKill[T]: return set(), set() diff --git a/mypyc/ir/ops.py b/mypyc/ir/ops.py index 6e186c4ef..6756913e6 100644 --- a/mypyc/ir/ops.py +++ b/mypyc/ir/ops.py @@ -211,7 +211,7 @@ def __init__(self, value: float, line: int = -1) -> None: self.line = line -class Op(Value): +class Op(Value): # type: ignore[abstract] """Abstract base class for all IR operations. Each operation must be stored in a BasicBlock (in 'ops') to be @@ -251,7 +251,7 @@ def accept(self, visitor: OpVisitor[T]) -> T: pass -class BaseAssign(Op): +class BaseAssign(Op): # type: ignore[abstract] """Base class for ops that assign to a register.""" def __init__(self, dest: Register, line: int = -1) -> None: @@ -308,7 +308,7 @@ def accept(self, visitor: OpVisitor[T]) -> T: return visitor.visit_assign_multi(self) -class ControlOp(Op): +class ControlOp(Op): # type: ignore[abstract] """Control flow operation.""" def targets(self) -> Sequence[BasicBlock]: @@ -456,7 +456,7 @@ def accept(self, visitor: OpVisitor[T]) -> T: return visitor.visit_unreachable(self) -class RegisterOp(Op): +class RegisterOp(Op): # type: ignore[abstract] """Abstract base class for operations that can be written as r1 = f(r2, ..., rn). Takes some values, performs an operation, and generates an output @@ -1560,7 +1560,7 @@ def accept(self, visitor: OpVisitor[T]) -> T: @trait -class OpVisitor(Generic[T]): +class OpVisitor(Generic[T]): # type: ignore[abstract] """Generic visitor over ops (uses the visitor design pattern).""" @abstractmethod diff --git a/mypyc/ir/rtypes.py b/mypyc/ir/rtypes.py index 493465949..c0626926c 100644 --- a/mypyc/ir/rtypes.py +++ b/mypyc/ir/rtypes.py @@ -36,7 +36,7 @@ T = TypeVar("T") -class RType: +class RType: # type: ignore[abstract] """Abstract base class for runtime types (erased, only concrete; no generics).""" name: str @@ -105,7 +105,7 @@ def deserialize_type(data: JsonDict | str, ctx: DeserMaps) -> RType: raise NotImplementedError("unexpected .class {}".format(data[".class"])) -class RTypeVisitor(Generic[T]): +class RTypeVisitor(Generic[T]): # type: ignore[abstract] """Generic visitor over RTypes (uses the visitor design pattern).""" @abstractmethod diff --git a/mypyc/irbuild/builder.py b/mypyc/irbuild/builder.py index a0837ba2b..13cf30546 100644 --- a/mypyc/irbuild/builder.py +++ b/mypyc/irbuild/builder.py @@ -142,7 +142,7 @@ int_borrow_friendly_op: Final = {"+", "-", "==", "!=", "<", "<=", ">", ">="} -class IRVisitor(ExpressionVisitor[Value], StatementVisitor[None]): +class IRVisitor(ExpressionVisitor[Value], StatementVisitor[None]): # type: ignore[abstract] pass diff --git a/mypyc/irbuild/classdef.py b/mypyc/irbuild/classdef.py index 6072efa2c..8d56f0cef 100644 --- a/mypyc/irbuild/classdef.py +++ b/mypyc/irbuild/classdef.py @@ -195,7 +195,7 @@ def transform_class_def(builder: IRBuilder, cdef: ClassDef) -> None: cls_builder.finalize(ir) -class ClassBuilder: +class ClassBuilder: # type: ignore[abstract] """Create IR for a class definition. This is an abstract base class. diff --git a/mypyc/irbuild/nonlocalcontrol.py b/mypyc/irbuild/nonlocalcontrol.py index 0ac9bd3ce..c200f8910 100644 --- a/mypyc/irbuild/nonlocalcontrol.py +++ b/mypyc/irbuild/nonlocalcontrol.py @@ -26,7 +26,7 @@ from mypyc.irbuild.builder import IRBuilder -class NonlocalControl: +class NonlocalControl: # type: ignore[abstract] """ABC representing a stack frame of constructs that modify nonlocal control flow. The nonlocal control flow constructs are break, continue, and @@ -113,7 +113,7 @@ def gen_return(self, builder: IRBuilder, value: Value, line: int) -> None: builder.builder.pop_error_handler() -class CleanupNonlocalControl(NonlocalControl): +class CleanupNonlocalControl(NonlocalControl): # type: ignore[abstract] """Abstract nonlocal control that runs some cleanup code.""" def __init__(self, outer: NonlocalControl) -> None: diff --git a/mypyc/test/testutil.py b/mypyc/test/testutil.py index bf3dc8614..8d7bbb732 100644 --- a/mypyc/test/testutil.py +++ b/mypyc/test/testutil.py @@ -31,7 +31,7 @@ TESTUTIL_PATH = os.path.join(test_data_prefix, "fixtures/testutil.py") -class MypycDataSuite(DataSuite): +class MypycDataSuite(DataSuite): # type: ignore[abstract] # Need to list no files, since this will be picked up as a suite of tests files: list[str] = [] data_prefix = test_data_prefix diff --git a/test-data/unit/check-based-abstract.test b/test-data/unit/check-based-abstract.test new file mode 100644 index 000000000..04e11e38f --- /dev/null +++ b/test-data/unit/check-based-abstract.test @@ -0,0 +1,26 @@ +[case testBaseAbstract] +from basedtyping import abstract + +@abstract +class A: + @abstract + def f(self): ... + +class B: # E: Class __main__.B has abstract attribute "f" [abstract] \ + # N: If it is meant to be abstract, decorate the class with `basedtyping.abstract`, or add `abc.ABC` as a base class, or `abc.ABCMeta` as the metaclass + @abstract + def f(self): ... + +class C(A): # E: Class __main__.C has abstract attribute "f" [abstract] \ + # N: If it is meant to be abstract, decorate the class with `basedtyping.abstract`, or add `abc.ABC` as a base class, or `abc.ABCMeta` as the metaclass + ... + + +[case testAbstractProtocolMessage] +from typing import Protocol + +class A(Protocol): + a: int +class B(A): ... # E: Class __main__.B has abstract attribute "a" [abstract] \ + # N: If it is meant to be a protocol, add `typing.Protocol` as a base class \ + # N: If it is meant to be abstract, decorate the class with `basedtyping.abstract`, or add `abc.ABC` as a base class, or `abc.ABCMeta` as the metaclass diff --git a/test-data/unit/check-based-modifiers.test b/test-data/unit/check-based-modifiers.test new file mode 100644 index 000000000..87abf6c42 --- /dev/null +++ b/test-data/unit/check-based-modifiers.test @@ -0,0 +1,28 @@ +[case testAbstractAttribute] +from basedtyping import Abstract, abstract +class A: # E: Class __main__.A has abstract attribute "a" [abstract] \ + # N: If it is meant to be abstract, decorate the class with `basedtyping.abstract`, or add `abc.ABC` as a base class, or `abc.ABCMeta` as the metaclass + a: Abstract[int] +A() # E: # E: Cannot instantiate abstract class "A" with abstract attribute "a" [abstract] + + +[case testReadOnlyAttribute] +from typing_extensions import ReadOnly +class A: + a: ReadOnly = 1 +A().a = 2 # E: Attribute "a" defined in "A" is read only [read-only] +[builtins fixtures/tuple.pyi] + + +[case testMultipleModifiers] +from basedtyping import Abstract, abstract +from typing_extensions import ReadOnly +from typing import ClassVar + +@abstract +class A: + a: ClassVar[Abstract[ReadOnly[int]]] +A().a = 1 # E: Cannot instantiate abstract class "A" with abstract attribute "a" [abstract] \ + # E: Attribute "a" defined in "A" is read only [read-only] \ + # E: Cannot assign to class variable "a" via instance [misc] +[builtins fixtures/tuple.pyi] diff --git a/test-data/unit/check-based-read-only.test b/test-data/unit/check-based-read-only.test new file mode 100644 index 000000000..9ee0110c1 --- /dev/null +++ b/test-data/unit/check-based-read-only.test @@ -0,0 +1,19 @@ +[case testBasedReadOnly] +from typing_extensions import ReadOnly + +class A: + a: ReadOnly = 1 + a = 2 # E: Name "a" is read only [read-only] + b: ReadOnly[int] = 1 + b = 2 # E: Name "b" is read only [read-only] + +a: A +a.a = 2 # E: Attribute "a" defined in "A" is read only [read-only] +a.b = 2 # E: Attribute "b" defined in "A" is read only [read-only] + +b: ReadOnly = 1 +c: ReadOnly[int] = 1 + +b = 2 # E: Name "b" is read only [read-only] +c = 2 # E: Name "c" is read only [read-only] +[builtins fixtures/tuple.pyi] diff --git a/test-data/unit/check-based-union-join.test b/test-data/unit/check-based-union-join.test index f80782c57..139f22522 100644 --- a/test-data/unit/check-based-union-join.test +++ b/test-data/unit/check-based-union-join.test @@ -44,11 +44,11 @@ from typing import Protocol class Base(Protocol): a: int class A(Base): - pass + a = 1 class B(Base): - pass + a = 1 class C(Base): - pass + a = 1 reveal_type([A, B, C]) # N: Revealed type is "list[() -> __main__.A | () -> __main__.B | () -> __main__.C]" diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 761152131..466e92747 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -5949,7 +5949,7 @@ class A: # OK, has @abstractmethod def f(self) -> None: pass -class B(A): # E: Class a.B has abstract attributes "f" # N: If it is meant to be abstract, add 'abc.ABCMeta' as an explicit metaclass +class B(A): # E: Class a.B has abstract attribute "f" # N: If it is meant to be abstract, decorate the class with `basedtyping.abstract`, or add `abc.ABC` as a base class, or `abc.ABCMeta` as the metaclass pass class C(A, metaclass=ABCMeta): # OK, has ABCMeta as a metaclass diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index f3cf83e6e..933577e0c 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -2048,7 +2048,7 @@ warn_no_return = True [case testIncrementalClassVar] from typing import ClassVar class A: - x = None # type: ClassVar + x = None # type: ClassVar[object] A().x = 0 [out1] main:4: error: Cannot assign to class variable "x" via instance diff --git a/test-data/unit/fixtures/typing-full.pyi b/test-data/unit/fixtures/typing-full.pyi index f6cda8452..b9c1bce81 100644 --- a/test-data/unit/fixtures/typing-full.pyi +++ b/test-data/unit/fixtures/typing-full.pyi @@ -88,7 +88,7 @@ class Iterator(Iterable[T_co], Protocol): @abstractmethod def __next__(self) -> T_co: pass -class Generator(Iterator[T], Generic[T, U, V]): +class Generator(Iterator[T], Generic[T, U, V]): # type: ignore[abstract] @abstractmethod def send(self, value: U) -> T: pass @@ -101,7 +101,7 @@ class Generator(Iterator[T], Generic[T, U, V]): @abstractmethod def __iter__(self) -> 'Generator[T, U, V]': pass -class AsyncGenerator(AsyncIterator[T], Generic[T, U]): +class AsyncGenerator(AsyncIterator[T], Generic[T, U]): # type: ignore[abstract] @abstractmethod def __anext__(self) -> Awaitable[T]: pass @@ -125,7 +125,7 @@ class Awaitable(Protocol[T]): class AwaitableGenerator(Generator[T, U, V], Awaitable[V], Generic[T, U, V, S], metaclass=ABCMeta): pass -class Coroutine(Awaitable[V], Generic[T, U, V]): +class Coroutine(Awaitable[V], Generic[T, U, V]): # type: ignore[abstract] @abstractmethod def send(self, value: U) -> T: pass @@ -146,11 +146,11 @@ class AsyncIterator(AsyncIterable[T], Protocol): @abstractmethod def __anext__(self) -> Awaitable[T]: pass -class Sequence(Iterable[T_co], Container[T_co]): +class Sequence(Iterable[T_co], Container[T_co]): # type: ignore[abstract] @abstractmethod def __getitem__(self, n: Any) -> T_co: pass -class MutableSequence(Sequence[T]): +class MutableSequence(Sequence[T]): # type: ignore[abstract] @abstractmethod def __setitem__(self, n: Any, o: T) -> None: pass diff --git a/test-data/unit/lib-stub/basedtyping.pyi b/test-data/unit/lib-stub/basedtyping.pyi index 6433b360f..0df27e504 100644 --- a/test-data/unit/lib-stub/basedtyping.pyi +++ b/test-data/unit/lib-stub/basedtyping.pyi @@ -4,6 +4,10 @@ # DO NOT ADD TO THIS FILE UNLESS YOU HAVE A GOOD REASON! Additional definitions # will slow down tests. +from helper import T + Untyped = 0 -Intersection = 1 -FunctionType = 2 +Intersection = 0 +FunctionType = 0 +Abstract = 0 +def abstract(fn: T) -> T: ... diff --git a/test-data/unit/semanal-classvar.test b/test-data/unit/semanal-classvar.test index a7bcec032..cb6eb7c6f 100644 --- a/test-data/unit/semanal-classvar.test +++ b/test-data/unit/semanal-classvar.test @@ -52,7 +52,7 @@ MypyFile:1( AssignmentStmt:3( NameExpr(x [m]) IntExpr(1) - Any))) + builtins.int))) [case testClassVarWithTypeVar]