Skip to content

Commit 7668503

Browse files
authored
Allow reify types to be mutable to avoid FrozenInstanceError from attrs (#1100)
Fixes #1088 I don't really like that I'm having to do this, but given that I think such cases are likely fairly rare (and most likely confined to the `io.IOBase` types) I think it's an acceptable compromise.
1 parent 416355c commit 7668503

File tree

7 files changed

+24
-12
lines changed

7 files changed

+24
-12
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Changed
9+
* Types generated by `reify` may optionally be marked as `^:mutable` now to prevent `attrs.exceptions.FrozenInstanceError`s being thrown when mutating methods inherited from the supertype(s) are called (#1088)
810

911
## [v0.3.0]
1012
### Added

docs/concepts.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1342,6 +1342,13 @@ Reified types always implement :py:class:`basilisp.lang.interfaces.IWithMeta` an
13421342

13431343
While ``reify`` and ``deftype`` are broadly similar, ``reify`` types may not define class or static methods.
13441344

1345+
.. warning::
1346+
1347+
If a reified type is defined with a mutable "abstract" supertype (such as :external:py:class:`io.IOBase`), users may experience errors arising from the ``attrs``-generated ``__setattr__`` method for the underlying type when mutating methods are called on the resulting object.
1348+
Reified types are immutable (or "frozen" in ``attrs`` lingo) by default.
1349+
When a mutating method, such as :external:py:meth:`io.IOBase.close`, is called on the type (which may be called manually or it may be called at VM shutdown), the mutation will fail due to ``attrs`` replacing the ``__setattr__`` method on the type.
1350+
It is possible to force Basilisp to generate a mutable (non-frozen) type for reified types by applying the ``^:mutable`` metadata on the ``reify`` symbol.
1351+
13451352
.. _defrecord:
13461353

13471354
``defrecord``

src/basilisp/core.lpy

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6742,9 +6742,10 @@
67426742
the body of a method should not include that parameter, as it will be supplied
67436743
automatically."
67446744
[& method-impls]
6745-
(let [{:keys [interfaces methods]} (collect-methods method-impls)]
6745+
(let [{:keys [interfaces methods]} (collect-methods method-impls)
6746+
reify-sym (with-meta 'reify* (meta (first &form)))]
67466747
(with-meta
6747-
`(reify* :implements [~@interfaces python/object]
6748+
`(~reify-sym :implements [~@interfaces python/object]
67486749
~@methods)
67496750
(meta &form))))
67506751

src/basilisp/lang/compiler/analyzer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3107,6 +3107,7 @@ def _reify_ast(form: ISeq, ctx: AnalyzerContext) -> Reify:
31073107
members=vec.vector(members),
31083108
verified_abstract=type_abstractness.is_statically_verified_as_abstract,
31093109
artificially_abstract=type_abstractness.artificially_abstract_supertypes,
3110+
is_frozen=not _is_mutable(form.first),
31103111
use_weakref_slot=not type_abstractness.supertype_already_weakref,
31113112
env=ctx.get_node_env(pos=ctx.syntax_position),
31123113
)

src/basilisp/lang/compiler/generator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2771,7 +2771,7 @@ def _reify_to_py_ast(
27712771
),
27722772
verified_abstract=node.verified_abstract,
27732773
artificially_abstract_bases=artificially_abstract_bases,
2774-
is_frozen=True,
2774+
is_frozen=node.is_frozen,
27752775
use_slots=True,
27762776
use_weakref_slot=node.use_weakref_slot,
27772777
)

src/basilisp/lang/compiler/nodes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,7 @@ class Reify(Node[SpecialForm]):
815815
env: NodeEnv
816816
verified_abstract: bool = False
817817
artificially_abstract: IPersistentSet[DefTypeBase] = lset.PersistentSet.empty()
818+
is_frozen: bool = True
818819
use_weakref_slot: bool = True
819820
meta: NodeMeta = None
820821
children: Sequence[kw.Keyword] = vec.v(MEMBERS)

tests/basilisp/compiler_test.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4937,21 +4937,21 @@ def test_reify_disallows_extra_methods_if_not_in_a_super_type(
49374937
(
49384938
"""
49394939
(import* io)
4940-
(reify* :implements [^:abstract ^{:abstract-members '(:read)} io/IOBase]
4940+
(^:mutable reify* :implements [^:abstract ^{:abstract-members '(:read)} io/IOBase]
49414941
(read [this v]))""",
49424942
compiler.CompilerException,
49434943
),
49444944
(
49454945
"""
49464946
(import* io)
4947-
(reify* :implements [^:abstract ^{:abstract-members [:read]} io/IOBase]
4947+
(^:mutable reify* :implements [^:abstract ^{:abstract-members [:read]} io/IOBase]
49484948
(read [this v]))""",
49494949
compiler.CompilerException,
49504950
),
49514951
(
49524952
"""
49534953
(import* io)
4954-
(reify* :implements [^:abstract ^{:abstract-members #py [:read]} io/IOBase]
4954+
(^:mutable reify* :implements [^:abstract ^{:abstract-members #py [:read]} io/IOBase]
49554955
(read [this v]))""",
49564956
compiler.CompilerException,
49574957
),
@@ -4968,7 +4968,7 @@ def test_reify_abstract_members_must_have_elements(self, lcompile: CompileFn):
49684968
lcompile(
49694969
"""
49704970
(import* io)
4971-
(reify* :implements [^:abstract ^{:abstract-members #{}} io/IOBase]
4971+
(^:mutable reify* :implements [^:abstract ^{:abstract-members #{}} io/IOBase]
49724972
(read [this v]))
49734973
"""
49744974
)
@@ -4979,7 +4979,7 @@ def test_reify_abstract_should_not_have_namespaces(
49794979
lcompile(
49804980
"""
49814981
(import* io)
4982-
(reify* :implements [^:abstract ^{:abstract-members #{:io/read}} io/IOBase]
4982+
(^:mutable reify* :implements [^:abstract ^{:abstract-members #{:io/read}} io/IOBase]
49834983
(read [this v]))
49844984
"""
49854985
)
@@ -4997,7 +4997,7 @@ def test_reify_abstract_members_names_must_be_str_keyword_or_symbol(
49974997
lcompile(
49984998
"""
49994999
(import* io)
5000-
(reify* :implements [^:abstract ^{:abstract-members #{1 :read}} io/IOBase]
5000+
(^:mutable reify* :implements [^:abstract ^{:abstract-members #{1 :read}} io/IOBase]
50015001
(read [this v]))
50025002
""",
50035003
)
@@ -5007,19 +5007,19 @@ def test_reify_abstract_members_names_must_be_str_keyword_or_symbol(
50075007
[
50085008
"""
50095009
(import* io)
5010-
(reify* :implements [^:abstract ^{:abstract-members #{:read :write}} io/IOBase]
5010+
(^:mutable reify* :implements [^:abstract ^{:abstract-members #{:read :write}} io/IOBase]
50115011
(read [this n])
50125012
(write [this v]))
50135013
""",
50145014
"""
50155015
(import* io)
5016-
(reify* :implements [^:abstract ^{:abstract-members #{read write}} io/IOBase]
5016+
(^:mutable reify* :implements [^:abstract ^{:abstract-members #{read write}} io/IOBase]
50175017
(read [this n])
50185018
(write [this v]))
50195019
""",
50205020
"""
50215021
(import* io)
5022-
(reify* :implements [^:abstract ^{:abstract-members #{"read" "write"}} io/IOBase]
5022+
(^:mutable reify* :implements [^:abstract ^{:abstract-members #{"read" "write"}} io/IOBase]
50235023
(read [this n])
50245024
(write [this v]))
50255025
""",

0 commit comments

Comments
 (0)