Skip to content

Add is_locked property to Cells and References to prevent accidental edits to derived items #221

Description

@fumitoh

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions