Skip to content

Commit 95663de

Browse files
committed
abstract and read only
1 parent c1cbfa3 commit 95663de

35 files changed

+320
-70
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## [Unreleased]
44
### Added
5+
- `Abstract` and `abstract` modifiers
6+
- `ReadOnly` attributes
7+
8+
## [2.9.0]
9+
### Added
510
- `collections.User*` should have `__repr__`
611
### Fixed
712
- cache modules that only have baseline errors

docs/source/based_features.rst

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,55 @@ represent a set of choices that the ``TypeVar`` can be replaced with:
158158
type C = B[object] # mypy doesn't report the error here
159159
160160
161+
Abstract Classes
162+
----------------
163+
164+
abstract classes are more strict:
165+
166+
.. code-block:: python
167+
168+
class A: # error: abstract class not denoted as abstract
169+
@abstractmethod
170+
def f(self): ...
171+
172+
and more flexable:
173+
174+
.. code-block:: python
175+
176+
from basedtyping import abstract
177+
178+
@abstract
179+
class A:
180+
@abstract
181+
def f(self): ...
182+
183+
184+
and there are abstract attributes:
185+
186+
.. code-block:: python
187+
188+
from basedtyping import abstract, Abstract
189+
190+
@abstract
191+
class A:
192+
a: Abstract[int]
193+
194+
195+
Read-only attributes
196+
--------------------
197+
198+
simply:
199+
200+
.. code-block:: python
201+
202+
from typing import ReadOnly
203+
204+
class A:
205+
a: ReadOnly[int]
206+
207+
A().a = 1 # error: A.a is read-only
208+
209+
161210
Reinvented type guards
162211
----------------------
163212

mypy/checker.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3604,6 +3604,14 @@ def check_assignment(
36043604
self.infer_variable_type(inferred, lvalue, rvalue_type, rvalue)
36053605
self.check_assignment_to_slots(lvalue)
36063606

3607+
if (
3608+
isinstance(lvalue, NameExpr)
3609+
and isinstance(lvalue.node, Var)
3610+
and lvalue.node.is_read_only
3611+
and not self.get_final_context()
3612+
):
3613+
self.msg.read_only(lvalue.node.name, rvalue)
3614+
36073615
# (type, operator) tuples for augmented assignments supported with partial types
36083616
partial_type_augmented_ops: Final = {("builtins.list", "+"), ("builtins.set", "|")}
36093617

mypy/checkmember.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -807,6 +807,8 @@ def analyze_var(
807807
if mx.is_lvalue and var.is_property and not var.is_settable_property:
808808
# TODO allow setting attributes in subclass (although it is probably an error)
809809
mx.msg.read_only_property(name, itype.type, mx.context)
810+
if mx.is_lvalue and var.is_read_only:
811+
mx.msg.read_only(name, mx.context, itype.type)
810812
if mx.is_lvalue and var.is_classvar:
811813
mx.msg.cant_assign_to_classvar(name, mx.context)
812814
t = freshen_all_functions_type_vars(typ)

mypy/message_registry.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ def with_additional_msg(self, info: str) -> ErrorMessage:
266266
CLASS_VAR_WITH_TYPEVARS: Final = "ClassVar cannot contain type variables"
267267
CLASS_VAR_WITH_GENERIC_SELF: Final = "ClassVar cannot contain Self type in generic classes"
268268
CLASS_VAR_OUTSIDE_OF_CLASS: Final = "ClassVar can only be used for assignments in class body"
269+
ABSTRACT_OUTSIDE_OF_CLASS: Final = "`Abstract` can only be used for assignments in a class body"
269270

270271
# Protocol
271272
RUNTIME_PROTOCOL_EXPECTED: Final = ErrorMessage(

mypy/messages.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1569,12 +1569,13 @@ def incompatible_conditional_function_def(
15691569
def cannot_instantiate_abstract_class(
15701570
self, class_name: str, abstract_attributes: dict[str, bool], context: Context
15711571
) -> None:
1572-
attrs = format_string_list([f'"{a}"' for a in abstract_attributes])
1572+
if abstract_attributes:
1573+
attrs = format_string_list([f'"{a}"' for a in abstract_attributes])
1574+
rest = f" with abstract attribute{plural_s(abstract_attributes)} {attrs}"
1575+
else:
1576+
rest = ""
15731577
self.fail(
1574-
f'Cannot instantiate abstract class "{class_name}" with abstract '
1575-
f"attribute{plural_s(abstract_attributes)} {attrs}",
1576-
context,
1577-
code=codes.ABSTRACT,
1578+
f'Cannot instantiate abstract class "{class_name}"{rest}', context, code=codes.ABSTRACT
15781579
)
15791580
attrs_with_none = [
15801581
f'"{a}"'
@@ -1655,7 +1656,18 @@ def final_without_value(self, ctx: Context) -> None:
16551656
self.fail("Final name must be initialized with a value", ctx)
16561657

16571658
def read_only_property(self, name: str, type: TypeInfo, context: Context) -> None:
1658-
self.fail(f'Property "{name}" defined in "{type.name}" is read-only', context)
1659+
self.fail(
1660+
f'Property "{name}" defined in "{type.name}" is read-only',
1661+
context,
1662+
code=ErrorCode("read-only", "", ""),
1663+
)
1664+
1665+
def read_only(self, name: str, context: Context, type: TypeInfo | None = None) -> None:
1666+
if type is None:
1667+
prefix = f'Name "{name}"'
1668+
else:
1669+
prefix = f'Attribute "{name}" defined in "{type.name}"'
1670+
self.fail(f"{prefix} is read only", context, code=ErrorCode("read-only", "", ""))
16591671

16601672
def incompatible_typevar_value(
16611673
self,

mypy/metastore.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import sqlite3
2323

2424

25-
class MetadataStore:
25+
class MetadataStore: # type: ignore[abstract]
2626
"""Generic interface for metadata storage."""
2727

2828
@abstractmethod

mypy/nodes.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ class FakeExpression(Expression):
247247

248248

249249
@trait
250-
class SymbolNode(Node):
250+
class SymbolNode(Node): # type: ignore[abstract]
251251
"""Nodes that can be stored in a symbol table."""
252252

253253
__slots__ = ()
@@ -505,7 +505,7 @@ def accept(self, visitor: StatementVisitor[T]) -> T:
505505
FUNCBASE_FLAGS: Final = ["is_property", "is_class", "is_static", "is_final", "is_type_check_only"]
506506

507507

508-
class FuncBase(Node):
508+
class FuncBase(Node): # type: ignore[abstract]
509509
"""Abstract base class for function-like nodes.
510510
511511
N.B: Although this has SymbolNode subclasses (FuncDef,
@@ -710,7 +710,7 @@ def __init__(
710710
]
711711

712712

713-
class FuncItem(FuncBase):
713+
class FuncItem(FuncBase): # type: ignore[abstract]
714714
"""Base class for nodes usable as overloaded function items."""
715715

716716
__slots__ = (
@@ -1021,6 +1021,7 @@ class Var(SymbolNode):
10211021
"is_settable_property",
10221022
"is_classvar",
10231023
"is_abstract_var",
1024+
"is_read_only",
10241025
"is_final",
10251026
"is_index_var",
10261027
"final_unset_in_class",
@@ -1058,6 +1059,7 @@ def __init__(self, name: str, type: mypy.types.Type | None = None) -> None:
10581059
self.is_classvar = False
10591060
self.is_abstract_var = False
10601061
self.is_index_var = False
1062+
self.is_read_only = False
10611063
# Set to true when this variable refers to a module we were unable to
10621064
# parse for some reason (eg a silenced module)
10631065
self.is_suppressed_import = False
@@ -2558,7 +2560,7 @@ def accept(self, visitor: ExpressionVisitor[T]) -> T:
25582560
VARIANCE_NOT_READY: Final = 3 # Variance hasn't been inferred (using Python 3.12 syntax)
25592561

25602562

2561-
class TypeVarLikeExpr(SymbolNode, Expression):
2563+
class TypeVarLikeExpr(SymbolNode, Expression): # type: ignore[abstract]
25622564
"""Base class for TypeVarExpr, ParamSpecExpr and TypeVarTupleExpr.
25632565
25642566
Note that they are constructed by the semantic analyzer.

mypy/plugin.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ class C: pass
151151

152152

153153
@trait
154-
class TypeAnalyzerPluginInterface:
154+
class TypeAnalyzerPluginInterface: # type: ignore[abstract]
155155
"""Interface for accessing semantic analyzer functionality in plugins.
156156
157157
Methods docstrings contain only basic info. Look for corresponding implementation
@@ -195,7 +195,7 @@ class AnalyzeTypeContext(NamedTuple):
195195

196196

197197
@mypyc_attr(allow_interpreted_subclasses=True)
198-
class CommonPluginApi:
198+
class CommonPluginApi: # type: ignore[abstract]
199199
"""
200200
A common plugin API (shared between semantic analysis and type checking phases)
201201
that all plugin hooks get independently of the context.
@@ -217,7 +217,7 @@ def lookup_fully_qualified(self, fullname: str) -> SymbolTableNode | None:
217217

218218

219219
@trait
220-
class CheckerPluginInterface:
220+
class CheckerPluginInterface: # type: ignore[abstract]
221221
"""Interface for accessing type checker functionality in plugins.
222222
223223
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
254254

255255

256256
@trait
257-
class SemanticAnalyzerPluginInterface:
257+
class SemanticAnalyzerPluginInterface: # type: ignore[abstract]
258258
"""Interface for accessing semantic analyzer functionality in plugins.
259259
260260
Methods docstrings contain only basic info. Look for corresponding implementation

mypy/report.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -705,7 +705,7 @@ def on_finish(self) -> None:
705705
register_reporter("cobertura-xml", CoberturaXmlReporter, needs_lxml=True)
706706

707707

708-
class AbstractXmlReporter(AbstractReporter):
708+
class AbstractXmlReporter(AbstractReporter): # type: ignore[abstract]
709709
"""Internal abstract class for reporters that work via XML."""
710710

711711
def __init__(self, reports: Reports, output_dir: str) -> None:

0 commit comments

Comments
 (0)