@@ -101,7 +101,7 @@ there is no way of defining a :class:`~typing.Protocol`, such that the only
101101requirements to satisfy are:
102102
1031031. ``hasattr(obj, name) ``
104- 2. ``isinstance(obj.name, T) `` [#invalid_typevar ]_
104+ 2. ``isinstance(obj.name, T) `` [#invalid_typevar ]_
105105
106106The above are satisfiable at runtime by all of the following:
107107
@@ -231,17 +231,18 @@ times the attribute can be assigned to in those contexts. Example:
231231 self .songs = list (songs)
232232
233233 def clear (self ) -> None :
234- # Type check error: assignment to read-only "songs" outside initialization
234+ # error: assignment to read-only "songs" outside initialization
235235 self .songs = []
236236
237237
238238 band = Band(name = " Boa" , songs = [" Duvet" ])
239- band.name = " Emma" # Ok : "name" is not read-only
240- band.songs = [] # Type check error: "songs" is read-only
241- band.songs.append(" Twilight" ) # Ok : list is mutable
239+ band.name = " Emma" # ok : "name" is not read-only
240+ band.songs = [] # error: "songs" is read-only
241+ band.songs.append(" Twilight" ) # ok : list is mutable
242242
243243 Classes which do not define `__slots__ <https://docs.python.org/3/reference/datamodel.html#object.__slots_ _>`_
244- may give the attribute a default value, overridable at instance level:
244+ retain the ability to initialize a read-only attribute both at declaration and
245+ within ``__init__ ``:
245246
246247.. code-block :: python
247248
@@ -252,19 +253,138 @@ may give the attribute a default value, overridable at instance level:
252253 if number is not None :
253254 self .number = number
254255
255- Inheritance
256- -----------
256+ Type checkers should warn on attributes declared ``ReadOnly `` which may be left
257+ uninitialized after ``__init__ `` exits, unless the class is a protocol or an ABC::
258+
259+ class Foo:
260+ id: ReadOnly[int] # error: "id" is not initialized on all code paths
261+ name: ReadOnly[str] # error: "name" is never initialized
262+
263+ def __init__(self) -> None:
264+ if random.random() > 0.5:
265+ self.id = 123
266+
267+ This rule stems from the fact the body of the class declaring the attribute is
268+ the only place able to initialize it, in contrast to non-read-only attributes,
269+ which could be initialized outside of the class body.
270+
271+ Subtyping
272+ ---------
273+
274+ Read-only attributes are covariant. This has a few subtyping implications.
275+ Borrowing from :pep: `PEP 705 <705#inheritance >`:
276+
277+ * Read-only attributes can be redeclared as writable attributes, descriptors
278+ and class variables::
279+
280+ @dataclass
281+ class HasTitle:
282+ title: ReadOnly[str]
283+
284+
285+ @dataclass
286+ class Game(HasTitle):
287+ title: str
288+ year: int
289+
290+
291+ game = Game(title="DOOM", year=1993)
292+ game.year = 1994
293+ game.title = "DOOM II" # ok: attribute is not read-only
294+
295+
296+ class TitleProxy(HasTitle):
297+ @functools.cached_property
298+ def title(self) -> str: ...
299+
300+
301+ class SharedTitle(HasTitle):
302+ title: ClassVar[str] = "Still Grey"
303+
304+ * If a read-only attribute is not redeclared, it remains read-only::
305+
306+ @dataclass
307+ class Game(HasTitle):
308+ year: int
309+
310+
311+ game = Game(title="DOOM", year=1993)
312+ game.title = "DOOM II" # error: attribute is read-only
313+
314+ * Subtypes can narrow the type of read-only attributes::
315+
316+ class GameCollection(Protocol):
317+ games: ReadOnly[abc.Collection[Game]]
318+
319+
320+ @dataclass
321+ class GameSeries(GameCollection):
322+ name: str
323+ games: ReadOnly[list[Game]] # ok: list[Game] is assignable to Collection[Game]
324+
325+ * In nominal subclasses of protocols and ABCs, a read-only attribute should be
326+ considered abstract, unless it is initialized::
327+
328+ class MyBase(abc.ABC):
329+ foo: ReadOnly[int]
330+ bar: ReadOnly[str] = "abc"
331+ baz: ReadOnly[float]
332+
333+ def __init__(self, baz: float) -> None:
334+ self.baz = baz
335+
336+ @abstractmethod
337+ def do_something(self) -> None: ...
338+
339+
340+ @final
341+ class MySubclass(MyBase):
342+ # error: MySubclass does not override "foo"
343+
344+ def do_something(self) -> None:
345+ print(self.foo, self.bar, self.baz)
346+
347+ * In a protocol attribute declaration, ``name: ReadOnly[T] `` indicates that a structural
348+ subtype must support ``.name `` access, and the returned value is compatible with ``T ``::
349+
350+ class HasName(Protocol):
351+ name: ReadOnly[str]
352+
353+
354+ class NamedAttr:
355+ name: str
356+
357+ class NamedProp:
358+ @property
359+ def name(self) -> str: ...
360+
361+ class NamedClassVar:
362+ name: ClassVar[str]
363+
364+ class NamedDescriptor:
365+ @cached_property
366+ def name(self) -> str: ...
367+
368+ # all of the following are ok
369+ has_name: HasName
370+ has_name = NamedAttr()
371+ has_name = NamedProp()
372+ has_name = NamedClassVar
373+ has_name = NamedClassVar()
374+ has_name = NamedDescriptor()
257375
258- In a protocol attribute declaration, ``name: ReadOnly[T] `` indicates that a structural
259- subtype must support ``.name `` access, and the returned value is compatible with ``T ``.
260376
261377Changes to ``Final ``
262378--------------------
263379
264380.. TODO
265- once changes are done, this probably won't be true
266- ``ReadOnly`` cannot be combined with ``Final``, as the two qualifiers differ in
267- initialization rules, leading to ambiguity and/or significance of ordering.
381+ - deletability
382+ - final in protocols?
383+ - interaction of Final and ReadOnly - once parity changes are done, the
384+ following shouldn't be true:
385+ ``ReadOnly`` cannot be combined with ``Final``, as the two qualifiers differ in
386+ initialization rules, leading to ambiguity and/or significance of ordering.
387+ - section on "Type consistency"?
268388
269389 Interaction with other special types
270390------------------------------------
@@ -328,6 +448,7 @@ or attrs' `initialization hooks <https://www.attrs.org/en/stable/init.html#hooki
328448This has been set aside for now, as defining rules regarding inclusion of
329449such methods has proven difficult.
330450
451+
331452Footnotes
332453=========
333454
0 commit comments