Skip to content

Commit

Permalink
abstract and read only
Browse files Browse the repository at this point in the history
  • Loading branch information
KotlinIsland committed Jan 2, 2025
1 parent 8cd497f commit 07dca16
Show file tree
Hide file tree
Showing 35 changed files with 318 additions and 70 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Basedmypy Changelog

## [Unreleased]
### Added
- `Abstract` and `abstract` modifiers
- `ReadOnly` attributes

## [2.9.0]
### Added
Expand Down
49 changes: 49 additions & 0 deletions docs/source/based_features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------------------

Expand Down
8 changes: 8 additions & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", "|")}

Expand Down
2 changes: 2 additions & 0 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions mypy/message_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
24 changes: 18 additions & 6 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"'
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion mypy/metastore.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import sqlite3


class MetadataStore:
class MetadataStore: # type: ignore[abstract]
"""Generic interface for metadata storage."""

@abstractmethod
Expand Down
10 changes: 6 additions & 4 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = ()
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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__ = (
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
8 changes: 4 additions & 4 deletions mypy/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion mypy/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
83 changes: 75 additions & 8 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -1847,6 +1847,7 @@ def visit_decorator(self, dec: Decorator) -> None:
(
"builtins.property",
"abc.abstractproperty",
"basedtyping.abstract",
"functools.cached_property",
"enum.property",
"types.DynamicClassAttribute",
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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):
Expand All @@ -5257,26 +5262,88 @@ 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)
if not sym or not sym.node:
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)
if not sym or not sym.node:
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:
Expand Down
Loading

0 comments on commit 07dca16

Please sign in to comment.