Background
When a sub space inherits Cells and References from its base, modelx marks the
inherited items as "derived" (Derivable._is_derived = True in
modelx/core/base.py). Today, assigning a new formula to a derived Cells, or
re-binding a derived Reference, silently re-defines it — the derived item
becomes a defined one and the inheritance link is broken
(see cells.py:974-980 and reference.py:80-107).
This is a footgun. A stray assignment in a notebook or a typo in a script can
quietly break inheritance with no warning, and the resulting state is hard to
notice until downstream values diverge.
Proposal
Add a writable boolean property is_locked to both Cells and References:
is_locked == False means the item can be edited normally (current behavior).
is_locked == True means edits raise an error.
Defaults
- Cells/References defined by the user are created with
is_locked = False.
- Cells/References derived in a sub space are created with
is_locked = True.
This makes the safe behavior the default while leaving an explicit escape
hatch: a user who genuinely wants to override a derived item sets
is_locked = False first, then edits.
Operations that should respect the lock
For Cells (modelx.core.cells.Cells):
Cells.formula setter, Cells.set_formula(...), Cells.clear_formula()
Cells.value setter, Cells.set_value(...), item assignment cells[key] = v
Cells.is_cached setter
For References (via UserSpace):
UserSpace.set_ref(...), UserSpace.absref(...), UserSpace.relref(...)
- Attribute assignment
space.x = ... (Space.__setattr__)
- Attribute deletion
del space.x (Space.__delattr__)
When any of the above is attempted on a locked item, raise a clear error
(e.g. ValueError with a message naming the item and suggesting
is_locked = False).
What is_locked does not do
- It does not affect read access or formula evaluation.
- It does not change derivation semantics:
is_derived and is_locked are
independent flags. A defined item can be locked; a derived item can be
unlocked.
- Setting
is_locked = False on a derived item does not by itself convert
it to defined — that still happens (as today) only when the user actually
edits it.
Example
import modelx as mx
m = mx.new_model()
base = m.new_space("Base")
@mx.defcells
def foo(x):
return x + 1
base.k = 10 # defined Reference, is_locked == False
sub = m.new_space("Sub", bases=base)
sub.foo.is_locked # True (derived)
sub.foo.formula = lambda x: x*2 # raises: derived cells is locked
sub.foo.is_locked = False # explicit opt-in
sub.foo.formula = lambda x: x*2 # ok, becomes defined in Sub
base.foo.is_locked # False (defined in Base)
base.foo.is_locked = True # user can also lock defined items
base.foo.formula = lambda x: x # raises: cells is locked
Background
When a sub space inherits Cells and References from its base, modelx marks the
inherited items as "derived" (
Derivable._is_derived = Trueinmodelx/core/base.py). Today, assigning a new formula to a derived Cells, orre-binding a derived Reference, silently re-defines it — the derived item
becomes a defined one and the inheritance link is broken
(see
cells.py:974-980andreference.py:80-107).This is a footgun. A stray assignment in a notebook or a typo in a script can
quietly break inheritance with no warning, and the resulting state is hard to
notice until downstream values diverge.
Proposal
Add a writable boolean property
is_lockedto both Cells and References:is_locked == Falsemeans the item can be edited normally (current behavior).is_locked == Truemeans edits raise an error.Defaults
is_locked = False.is_locked = True.This makes the safe behavior the default while leaving an explicit escape
hatch: a user who genuinely wants to override a derived item sets
is_locked = Falsefirst, then edits.Operations that should respect the lock
For Cells (
modelx.core.cells.Cells):Cells.formulasetter,Cells.set_formula(...),Cells.clear_formula()Cells.valuesetter,Cells.set_value(...), item assignmentcells[key] = vCells.is_cachedsetterFor References (via
UserSpace):UserSpace.set_ref(...),UserSpace.absref(...),UserSpace.relref(...)space.x = ...(Space.__setattr__)del space.x(Space.__delattr__)When any of the above is attempted on a locked item, raise a clear error
(e.g.
ValueErrorwith a message naming the item and suggestingis_locked = False).What
is_lockeddoes not dois_derivedandis_lockedareindependent flags. A defined item can be locked; a derived item can be
unlocked.
is_locked = Falseon a derived item does not by itself convertit to defined — that still happens (as today) only when the user actually
edits it.
Example