From f352a3266ff0c7fef924369c296ec18cb7280997 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 20 May 2026 12:50:57 +0000 Subject: [PATCH 1/7] Add Interface.info property for cells and spaces Provide a human-readable snapshot of an Interface object via a new `info` property on the `Interface` base class. The property returns a lightweight wrapper whose `repr` summarises the object: - CellsInfo: the cells' fully-qualified signature, is_cached, allow_none, formula source, cached-value count, and an abbreviated listing of cached key-value pairs. - SpaceInfo: the space's fully-qualified representation, parameters (when defined), the number of ItemSpaces, and an abbreviated listing of those item spaces. - ModelInfo: a basic summary listing the model's child spaces. Long key-value listings are truncated past five entries with a "... (N more)" sentinel. https://claude.ai/code/session_01RwTSSE7phKRY2m56zpeZi9 --- modelx/core/base.py | 40 +++++ modelx/core/info.py | 147 ++++++++++++++++++ modelx/tests/core/cells/test_cells_info.py | 91 +++++++++++ .../core/space/properties/test_space_info.py | 72 +++++++++ 4 files changed, 350 insertions(+) create mode 100644 modelx/core/info.py create mode 100644 modelx/tests/core/cells/test_cells_info.py create mode 100644 modelx/tests/core/space/properties/test_space_info.py diff --git a/modelx/core/base.py b/modelx/core/base.py index 4d3241ee..1e99aa25 100644 --- a/modelx/core/base.py +++ b/modelx/core/base.py @@ -444,6 +444,46 @@ def foo(x): """ return self._impl.doc + @property + def info(self): + """An object whose ``repr`` summarizes this Interface. + + Returns a lightweight wrapper whose string representation displays + a human-readable snapshot of this object. The exact fields depend + on the concrete type: + + * For :class:`~modelx.core.cells.Cells`: the cells' fully-qualified + representation including its signature + (e.g., ``Model1.Space1[1].foo(t, i=0)``), + :attr:`~modelx.core.cells.Cells.is_cached`, + :attr:`allow_none`, the source of the + :attr:`~modelx.core.cells.Cells.formula`, + the number of cached values, and an abbreviated listing of + cached key-value pairs. + + * For spaces: the space's fully-qualified representation + (e.g., ``Model1.Space1[1]``), + :attr:`~modelx.core.space.BaseSpace.parameters` (when defined), + the number of :class:`~modelx.core.space.ItemSpace` children, + and an abbreviated listing of those item spaces. + + Example: + .. code-block:: python + + >>> print(space.foo.info) + + is_cached: True + allow_none: None + formula: + def foo(t): + return t * 2 + cached values: 2 + (0,): 0 + (1,): 2 + """ + from modelx.core.info import build_info + return build_info(self) + def __repr__(self): type_ = self.__class__.__name__ diff --git a/modelx/core/info.py b/modelx/core/info.py new file mode 100644 index 00000000..eae4819e --- /dev/null +++ b/modelx/core/info.py @@ -0,0 +1,147 @@ +# Copyright (c) 2017-2026 Fumito Hamamura + +# This library is free software: you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation version 3. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see . +"""Informational reprs for Interface objects. + +This module defines lightweight wrappers returned by the +:attr:`~modelx.core.base.Interface.info` property. The wrappers carry no +new state of their own; they format a human-readable snapshot of the +underlying Cells or Space when ``repr()`` is called on them. +""" + +_INDENT = " " +_MAX_ITEMS = 5 + + +def _format_items(items, indent=_INDENT, max_items=_MAX_ITEMS): + """Format key-value items as indented lines, truncating long lists.""" + items = list(items) + lines = [] + if len(items) <= max_items: + for key, value in items: + lines.append(indent + repr(key) + ": " + repr(value)) + else: + head = max_items - 1 + for key, value in items[:head]: + lines.append(indent + repr(key) + ": " + repr(value)) + lines.append(indent + "... (%d more)" % (len(items) - head)) + return lines + + +class _InterfaceInfo: + """Base class for objects returned by ``Interface.info``.""" + + __slots__ = ("_interface",) + + def __init__(self, interface): + self._interface = interface + + def _header(self): + return self._interface._get_repr(fullname=True, add_params=True) + + def _body_lines(self): + return [] + + def __repr__(self): + lines = ["<%s %s>" % (type(self).__name__, self._header())] + lines.extend(self._body_lines()) + return "\n".join(lines) + + +class CellsInfo(_InterfaceInfo): + """Informational repr for :class:`~modelx.core.cells.Cells`.""" + + __slots__ = () + + def _header(self): + impl = self._interface._impl + parent_repr = impl.repr_parent() + sig = str(impl.formula.signature) + if parent_repr: + return parent_repr + "." + impl.name + sig + return impl.name + sig + + def _body_lines(self): + cells = self._interface + impl = cells._impl + lines = [] + lines.append("is_cached: " + repr(cells.is_cached)) + lines.append("allow_none: " + repr(cells.allow_none)) + + source = impl.formula.source + lines.append("formula:") + if source: + for line in source.rstrip("\n").splitlines(): + lines.append(_INDENT + line) + else: + lines.append(_INDENT + "(source not available)") + + data = impl.data + lines.append("cached values: " + str(len(data))) + lines.extend(_format_items(data.items())) + return lines + + +class SpaceInfo(_InterfaceInfo): + """Informational repr for spaces (UserSpace, ItemSpace, DynamicSpace).""" + + __slots__ = () + + def _header(self): + return self._interface._get_repr(fullname=True, add_params=True) + + def _body_lines(self): + space = self._interface + lines = [] + params = space.parameters + if params is not None: + lines.append("parameters: " + repr(params)) + + itemspaces = dict(space.itemspaces) + lines.append("itemspaces: " + str(len(itemspaces))) + lines.extend(_format_items(itemspaces.items())) + return lines + + +class ModelInfo(_InterfaceInfo): + """Informational repr for :class:`~modelx.core.model.Model`.""" + + __slots__ = () + + def _header(self): + return self._interface.name + + def _body_lines(self): + spaces = dict(self._interface.spaces) + lines = ["spaces: " + str(len(spaces))] + lines.extend(_format_items(spaces.items())) + return lines + + +def build_info(interface): + """Return an info wrapper appropriate for ``interface``. + + Imports are deferred so this module does not need to be imported + at package load time. + """ + from modelx.core.cells import Cells + from modelx.core.space import BaseSpace + from modelx.core.model import Model + + if isinstance(interface, Cells): + return CellsInfo(interface) + if isinstance(interface, BaseSpace): + return SpaceInfo(interface) + if isinstance(interface, Model): + return ModelInfo(interface) + return _InterfaceInfo(interface) diff --git a/modelx/tests/core/cells/test_cells_info.py b/modelx/tests/core/cells/test_cells_info.py new file mode 100644 index 00000000..ece20aea --- /dev/null +++ b/modelx/tests/core/cells/test_cells_info.py @@ -0,0 +1,91 @@ +import modelx as mx +import pytest + + +@pytest.fixture +def testmodel(): + m = mx.new_model('Model1') + s = m.new_space('Space1', formula=lambda i: None) + + def foo(t, i=0): + if t == 0: + return i + return foo(t - 1, i) + 1 + + s.new_cells('foo', formula=foo) + + yield m + m._impl._check_sanity() + m.close() + + +def test_info_type_name(testmodel): + m = testmodel + info = m.Space1.foo.info + assert type(info).__name__ == 'CellsInfo' + + +def test_info_header_includes_signature_with_defaults(testmodel): + m = testmodel + # Trigger one ItemSpace creation so that the [1] form is reachable + text = repr(m.Space1[1].foo.info) + first = text.splitlines()[0] + assert first.startswith('' + + +def test_info_header_for_item_space(testmodel): + m = testmodel + first = repr(m.Space1[1].info).splitlines()[0] + assert first == '' + + +def test_info_parameters_shown_when_present(testmodel): + m = testmodel + text = repr(m.Space1.info) + assert "parameters: ('i',)" in text + + +def test_info_parameters_omitted_when_absent(testmodel): + m = testmodel + text = repr(m.Space1.Child.info) + assert 'parameters:' not in text + + +def test_info_itemspaces_count_and_listing(testmodel): + m = testmodel + text = repr(m.Space1.info) + assert 'itemspaces: 2' in text + assert '1: ' in text + assert '2: ' in text + + +def test_info_itemspaces_empty(testmodel): + m = testmodel + text = repr(m.Space1.Child.info) + assert 'itemspaces: 0' in text + + +def test_info_abbreviates_many_itemspaces(): + m = mx.new_model() + try: + s = m.new_space('S', formula=lambda x: None) + for i in range(10): + s[i] + text = repr(s.info) + assert 'itemspaces: 10' in text + assert '... (' in text and 'more)' in text + finally: + m._impl._check_sanity() + m.close() From 64fb1e158040b550d40d6b47ed0f03eb7fdf682f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 20 May 2026 13:19:48 +0000 Subject: [PATCH 2/7] Refine Interface.info repr formatting Update the layout of the wrapper returned by `Interface.info`: - The header line now uses `ClassName: ` (no angle brackets), where ClassName is the actual Interface subclass (Cells, UserSpace, ItemSpace, etc.). - For Cells, formula source is shown in full. Cached values now count only computed entries; a separate `input values` section (with count and key-value pairs) is shown only when at least one input value has been assigned. - For spaces, parameters are rendered as a signature string with defaults preserved and without surrounding parentheses (e.g., `i, j=0`). - The itemspaces listing now shows only keys as an abbreviated list literal (e.g., `[1, 2, 3, ...]`) instead of key-value pairs. https://claude.ai/code/session_01RwTSSE7phKRY2m56zpeZi9 --- modelx/core/base.py | 13 +++-- modelx/core/info.py | 55 ++++++++++++++----- modelx/tests/core/cells/test_cells_info.py | 50 ++++++++++++++--- .../core/space/properties/test_space_info.py | 41 +++++++++----- 4 files changed, 119 insertions(+), 40 deletions(-) diff --git a/modelx/core/base.py b/modelx/core/base.py index 1e99aa25..6f8db1fa 100644 --- a/modelx/core/base.py +++ b/modelx/core/base.py @@ -458,20 +458,23 @@ def info(self): :attr:`~modelx.core.cells.Cells.is_cached`, :attr:`allow_none`, the source of the :attr:`~modelx.core.cells.Cells.formula`, - the number of cached values, and an abbreviated listing of - cached key-value pairs. + the number of cached values together with an abbreviated listing + of cached key-value pairs, and -- when any input values have been + assigned -- the number of input values along with an abbreviated + listing of input key-value pairs. * For spaces: the space's fully-qualified representation (e.g., ``Model1.Space1[1]``), - :attr:`~modelx.core.space.BaseSpace.parameters` (when defined), + :attr:`~modelx.core.space.BaseSpace.parameters` (when defined, + shown as a signature string such as ``i, j=0``), the number of :class:`~modelx.core.space.ItemSpace` children, - and an abbreviated listing of those item spaces. + and an abbreviated list of the item-space keys. Example: .. code-block:: python >>> print(space.foo.info) - + Cells: Model.Space.foo(t) is_cached: True allow_none: None formula: diff --git a/modelx/core/info.py b/modelx/core/info.py index eae4819e..bb644023 100644 --- a/modelx/core/info.py +++ b/modelx/core/info.py @@ -23,7 +23,7 @@ _MAX_ITEMS = 5 -def _format_items(items, indent=_INDENT, max_items=_MAX_ITEMS): +def _format_kv_items(items, indent=_INDENT, max_items=_MAX_ITEMS): """Format key-value items as indented lines, truncating long lists.""" items = list(items) lines = [] @@ -34,10 +34,27 @@ def _format_items(items, indent=_INDENT, max_items=_MAX_ITEMS): head = max_items - 1 for key, value in items[:head]: lines.append(indent + repr(key) + ": " + repr(value)) - lines.append(indent + "... (%d more)" % (len(items) - head)) + lines.append(indent + "...") return lines +def _format_keys_list(keys, max_items=_MAX_ITEMS): + """Format ``keys`` as a single-line abbreviated list literal.""" + keys = list(keys) + if len(keys) <= max_items: + return "[" + ", ".join(repr(k) for k in keys) + "]" + head = max_items - 1 + return "[" + ", ".join(repr(k) for k in keys[:head]) + ", ...]" + + +def _signature_str(formula): + """Return ``formula``'s signature without the outer parentheses.""" + s = str(formula.signature) + if s.startswith("(") and s.endswith(")"): + s = s[1:-1] + return s + + class _InterfaceInfo: """Base class for objects returned by ``Interface.info``.""" @@ -46,6 +63,9 @@ class _InterfaceInfo: def __init__(self, interface): self._interface = interface + def _class_name(self): + return type(self._interface).__name__ + def _header(self): return self._interface._get_repr(fullname=True, add_params=True) @@ -53,7 +73,7 @@ def _body_lines(self): return [] def __repr__(self): - lines = ["<%s %s>" % (type(self).__name__, self._header())] + lines = ["%s: %s" % (self._class_name(), self._header())] lines.extend(self._body_lines()) return "\n".join(lines) @@ -87,8 +107,17 @@ def _body_lines(self): lines.append(_INDENT + "(source not available)") data = impl.data - lines.append("cached values: " + str(len(data))) - lines.extend(_format_items(data.items())) + input_keys = impl.input_keys + cached_items = [(k, v) for k, v in data.items() if k not in input_keys] + input_items = [(k, v) for k, v in data.items() if k in input_keys] + + lines.append("cached values: " + str(len(cached_items))) + lines.extend(_format_kv_items(cached_items)) + + if input_items: + lines.append("input values: " + str(len(input_items))) + lines.extend(_format_kv_items(input_items)) + return lines @@ -97,19 +126,18 @@ class SpaceInfo(_InterfaceInfo): __slots__ = () - def _header(self): - return self._interface._get_repr(fullname=True, add_params=True) - def _body_lines(self): space = self._interface lines = [] - params = space.parameters - if params is not None: - lines.append("parameters: " + repr(params)) + + formula = getattr(space._impl, "formula", None) + if formula is not None: + lines.append("parameters: " + _signature_str(formula)) itemspaces = dict(space.itemspaces) lines.append("itemspaces: " + str(len(itemspaces))) - lines.extend(_format_items(itemspaces.items())) + if itemspaces: + lines.append(_INDENT + _format_keys_list(itemspaces.keys())) return lines @@ -124,7 +152,8 @@ def _header(self): def _body_lines(self): spaces = dict(self._interface.spaces) lines = ["spaces: " + str(len(spaces))] - lines.extend(_format_items(spaces.items())) + if spaces: + lines.append(_INDENT + _format_keys_list(spaces.keys())) return lines diff --git a/modelx/tests/core/cells/test_cells_info.py b/modelx/tests/core/cells/test_cells_info.py index ece20aea..4c397034 100644 --- a/modelx/tests/core/cells/test_cells_info.py +++ b/modelx/tests/core/cells/test_cells_info.py @@ -25,13 +25,10 @@ def test_info_type_name(testmodel): assert type(info).__name__ == 'CellsInfo' -def test_info_header_includes_signature_with_defaults(testmodel): +def test_info_header_uses_class_name_and_signature(testmodel): m = testmodel - # Trigger one ItemSpace creation so that the [1] form is reachable - text = repr(m.Space1[1].foo.info) - first = text.splitlines()[0] - assert first.startswith('' + assert first == 'UserSpace: Model1.Space1' def test_info_header_for_item_space(testmodel): m = testmodel first = repr(m.Space1[1].info).splitlines()[0] - assert first == '' + assert first == 'ItemSpace: Model1.Space1[1, 0]' -def test_info_parameters_shown_when_present(testmodel): +def test_info_parameters_format(testmodel): m = testmodel text = repr(m.Space1.info) - assert "parameters: ('i',)" in text + # Signature string without outer parens, with defaults preserved + assert 'parameters: i, j=0' in text -def test_info_parameters_omitted_when_absent(testmodel): +def test_info_parameters_omitted_when_no_formula(testmodel): m = testmodel text = repr(m.Space1.Child.info) assert 'parameters:' not in text -def test_info_itemspaces_count_and_listing(testmodel): +def test_info_itemspaces_count(testmodel): m = testmodel text = repr(m.Space1.info) assert 'itemspaces: 2' in text - assert '1: ' in text - assert '2: ' in text -def test_info_itemspaces_empty(testmodel): +def test_info_itemspaces_listing_keys_only(testmodel): + m = testmodel + lines = repr(m.Space1.info).splitlines() + # The last line is the bracketed key list + last = lines[-1].strip() + assert last.startswith('[') and last.endswith(']') + # Only keys are shown - no ItemSpace repr leaks in + assert 'ItemSpace' not in last + assert '(1, 0)' in last + assert '(2, 0)' in last + + +def test_info_itemspaces_empty_no_list(testmodel): m = testmodel text = repr(m.Space1.Child.info) assert 'itemspaces: 0' in text + # No list line when empty + for line in text.splitlines(): + assert not line.strip().startswith('[') def test_info_abbreviates_many_itemspaces(): @@ -64,9 +78,10 @@ def test_info_abbreviates_many_itemspaces(): s = m.new_space('S', formula=lambda x: None) for i in range(10): s[i] - text = repr(s.info) - assert 'itemspaces: 10' in text - assert '... (' in text and 'more)' in text + lines = repr(s.info).splitlines() + last = lines[-1].strip() + assert last.startswith('[') and last.endswith(']') + assert last.endswith(', ...]') finally: m._impl._check_sanity() m.close() From b740e13de0d4035b82bb10cb5a5ee8a09fa76968 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 20 May 2026 13:50:36 +0000 Subject: [PATCH 3/7] Show _is_derived for Cells and bases for spaces in info Insert `_is_derived` immediately after the header line in CellsInfo, and `bases` immediately after the header line in SpaceInfo, so the inheritance status is visible alongside the other top-level attributes. https://claude.ai/code/session_01RwTSSE7phKRY2m56zpeZi9 --- modelx/core/info.py | 3 +++ modelx/tests/core/cells/test_cells_info.py | 14 ++++++++++++++ .../tests/core/space/properties/test_space_info.py | 13 +++++++++++++ 3 files changed, 30 insertions(+) diff --git a/modelx/core/info.py b/modelx/core/info.py index bb644023..31abcc82 100644 --- a/modelx/core/info.py +++ b/modelx/core/info.py @@ -95,6 +95,7 @@ def _body_lines(self): cells = self._interface impl = cells._impl lines = [] + lines.append("_is_derived: " + repr(cells._is_derived())) lines.append("is_cached: " + repr(cells.is_cached)) lines.append("allow_none: " + repr(cells.allow_none)) @@ -130,6 +131,8 @@ def _body_lines(self): space = self._interface lines = [] + lines.append("bases: " + repr(space.bases)) + formula = getattr(space._impl, "formula", None) if formula is not None: lines.append("parameters: " + _signature_str(formula)) diff --git a/modelx/tests/core/cells/test_cells_info.py b/modelx/tests/core/cells/test_cells_info.py index 4c397034..e613ee0e 100644 --- a/modelx/tests/core/cells/test_cells_info.py +++ b/modelx/tests/core/cells/test_cells_info.py @@ -31,6 +31,20 @@ def test_info_header_uses_class_name_and_signature(testmodel): assert first == 'Cells: Model1.Space1[1].foo(t, i=0)' +def test_info_shows_is_derived(testmodel): + m = testmodel + text = repr(m.Space1.foo.info) + lines = text.splitlines() + # _is_derived appears as the second item (right after the header) + assert lines[1] == '_is_derived: False' + + # Build a derived cells via space inheritance + derived = m.new_space('Derived', bases=m.Space1) + text2 = repr(derived.foo.info) + lines2 = text2.splitlines() + assert lines2[1] == '_is_derived: True' + + def test_info_shows_is_cached_and_allow_none(testmodel): m = testmodel cells = m.Space1.foo diff --git a/modelx/tests/core/space/properties/test_space_info.py b/modelx/tests/core/space/properties/test_space_info.py index a8c65cda..eca05afa 100644 --- a/modelx/tests/core/space/properties/test_space_info.py +++ b/modelx/tests/core/space/properties/test_space_info.py @@ -32,6 +32,19 @@ def test_info_header_for_item_space(testmodel): assert first == 'ItemSpace: Model1.Space1[1, 0]' +def test_info_shows_bases(testmodel): + m = testmodel + # The base UserSpace has no bases + lines = repr(m.Space1.Child.info).splitlines() + assert lines[1] == 'bases: []' + + # A derived space lists its base spaces + derived = m.new_space('Derived', bases=m.Space1) + lines2 = repr(derived.info).splitlines() + assert lines2[1].startswith('bases: [') + assert 'Space1' in lines2[1] + + def test_info_parameters_format(testmodel): m = testmodel text = repr(m.Space1.info) From e66346097c2b902e7a6dae31098ee26f8435f287 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 20 May 2026 14:07:37 +0000 Subject: [PATCH 4/7] Polish info repr: drop leading underscore, unwrap keys, dotted bases - Rename the Cells info field from `_is_derived` to `is_derived`. - Unwrap single-argument tuple keys in the cached and input value listings (e.g. `3: 6` instead of `(3,): 6`). Scalar cells keep the empty-tuple key. - Render the SpaceInfo `bases` field as a list of plain dotted fullnames (e.g. `[Model1.Base]`) instead of `` reprs. https://claude.ai/code/session_01RwTSSE7phKRY2m56zpeZi9 --- modelx/core/info.py | 16 ++++++++--- modelx/tests/core/cells/test_cells_info.py | 28 +++++++++++++++++-- .../core/space/properties/test_space_info.py | 7 +++-- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/modelx/core/info.py b/modelx/core/info.py index 31abcc82..6ef4a9d1 100644 --- a/modelx/core/info.py +++ b/modelx/core/info.py @@ -23,17 +23,24 @@ _MAX_ITEMS = 5 +def _format_key(key): + """Render a cells data key. Single-argument tuples are unwrapped.""" + if isinstance(key, tuple) and len(key) == 1: + return repr(key[0]) + return repr(key) + + def _format_kv_items(items, indent=_INDENT, max_items=_MAX_ITEMS): """Format key-value items as indented lines, truncating long lists.""" items = list(items) lines = [] if len(items) <= max_items: for key, value in items: - lines.append(indent + repr(key) + ": " + repr(value)) + lines.append(indent + _format_key(key) + ": " + repr(value)) else: head = max_items - 1 for key, value in items[:head]: - lines.append(indent + repr(key) + ": " + repr(value)) + lines.append(indent + _format_key(key) + ": " + repr(value)) lines.append(indent + "...") return lines @@ -95,7 +102,7 @@ def _body_lines(self): cells = self._interface impl = cells._impl lines = [] - lines.append("_is_derived: " + repr(cells._is_derived())) + lines.append("is_derived: " + repr(cells._is_derived())) lines.append("is_cached: " + repr(cells.is_cached)) lines.append("allow_none: " + repr(cells.allow_none)) @@ -131,7 +138,8 @@ def _body_lines(self): space = self._interface lines = [] - lines.append("bases: " + repr(space.bases)) + base_names = [b.fullname for b in space.bases] + lines.append("bases: [" + ", ".join(base_names) + "]") formula = getattr(space._impl, "formula", None) if formula is not None: diff --git a/modelx/tests/core/cells/test_cells_info.py b/modelx/tests/core/cells/test_cells_info.py index e613ee0e..d978181e 100644 --- a/modelx/tests/core/cells/test_cells_info.py +++ b/modelx/tests/core/cells/test_cells_info.py @@ -35,14 +35,14 @@ def test_info_shows_is_derived(testmodel): m = testmodel text = repr(m.Space1.foo.info) lines = text.splitlines() - # _is_derived appears as the second item (right after the header) - assert lines[1] == '_is_derived: False' + # is_derived appears as the second item (right after the header) + assert lines[1] == 'is_derived: False' # Build a derived cells via space inheritance derived = m.new_space('Derived', bases=m.Space1) text2 = repr(derived.foo.info) lines2 = text2.splitlines() - assert lines2[1] == '_is_derived: True' + assert lines2[1] == 'is_derived: True' def test_info_shows_is_cached_and_allow_none(testmodel): @@ -131,7 +131,29 @@ def test_info_with_scalar_cells(): assert first.endswith('S.scalar()') assert first.startswith('Cells: ') assert 'cached values: 1' in text + # Scalar cells keep the empty-tuple key (no single argument to unwrap) assert '(): 42' in text finally: m._impl._check_sanity() m.close() + + +def test_info_unwraps_single_arg_keys(): + m = mx.new_model() + try: + s = m.new_space('S') + s.new_cells('bar', formula=lambda x: x * 2) + s.bar(3) + s.bar(5) + s.bar[10] = 100 + text = repr(s.bar.info) + # Single-argument keys are shown without the (,) tuple form + assert '3: 6' in text + assert '5: 10' in text + assert '10: 100' in text + assert '(3,)' not in text + assert '(5,)' not in text + assert '(10,)' not in text + finally: + m._impl._check_sanity() + m.close() diff --git a/modelx/tests/core/space/properties/test_space_info.py b/modelx/tests/core/space/properties/test_space_info.py index eca05afa..c0358919 100644 --- a/modelx/tests/core/space/properties/test_space_info.py +++ b/modelx/tests/core/space/properties/test_space_info.py @@ -38,11 +38,12 @@ def test_info_shows_bases(testmodel): lines = repr(m.Space1.Child.info).splitlines() assert lines[1] == 'bases: []' - # A derived space lists its base spaces + # A derived space lists base spaces using their dotted fullnames derived = m.new_space('Derived', bases=m.Space1) lines2 = repr(derived.info).splitlines() - assert lines2[1].startswith('bases: [') - assert 'Space1' in lines2[1] + assert lines2[1] == 'bases: [Model1.Space1]' + # Plain dotted names, no wrappers + assert ' Date: Thu, 21 May 2026 15:25:20 +0000 Subject: [PATCH 5/7] Break the line for multi-line cached values in info When a cached or input value's repr spans multiple lines (e.g. a pandas DataFrame or Series), place the value on its own lines beginning right after the key. Single-line values continue to render inline as ``key: value``. https://claude.ai/code/session_01RwTSSE7phKRY2m56zpeZi9 --- modelx/core/info.py | 22 +++++++++-- modelx/tests/core/cells/test_cells_info.py | 45 ++++++++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/modelx/core/info.py b/modelx/core/info.py index 6ef4a9d1..c2e34379 100644 --- a/modelx/core/info.py +++ b/modelx/core/info.py @@ -31,16 +31,32 @@ def _format_key(key): def _format_kv_items(items, indent=_INDENT, max_items=_MAX_ITEMS): - """Format key-value items as indented lines, truncating long lists.""" + """Format key-value items as indented lines, truncating long lists. + + When a value's ``repr`` spans multiple lines (e.g. a ``pandas`` + ``Series`` or ``DataFrame``) the value is placed on its own lines + starting immediately after the key, so the multi-line layout + survives intact. + """ items = list(items) lines = [] + + def add(key, value): + key_str = _format_key(key) + val_repr = repr(value) + if "\n" in val_repr: + lines.append(indent + key_str + ": ") + lines.extend(val_repr.splitlines()) + else: + lines.append(indent + key_str + ": " + val_repr) + if len(items) <= max_items: for key, value in items: - lines.append(indent + _format_key(key) + ": " + repr(value)) + add(key, value) else: head = max_items - 1 for key, value in items[:head]: - lines.append(indent + _format_key(key) + ": " + repr(value)) + add(key, value) lines.append(indent + "...") return lines diff --git a/modelx/tests/core/cells/test_cells_info.py b/modelx/tests/core/cells/test_cells_info.py index d978181e..b0901768 100644 --- a/modelx/tests/core/cells/test_cells_info.py +++ b/modelx/tests/core/cells/test_cells_info.py @@ -138,6 +138,51 @@ def test_info_with_scalar_cells(): m.close() +def test_info_breaks_line_for_multiline_value(): + pd = pytest.importorskip('pandas') + m = mx.new_model() + try: + s = m.new_space('S') + s.pd = pd + + def make_df(): + return pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]}) + + s.new_cells('make_df', formula=make_df) + s.make_df() + + text = repr(s.make_df.info) + lines = text.splitlines() + # Find the cached-values key line + key_line_idx = next( + i for i, ln in enumerate(lines) if ln.strip().startswith('():')) + key_line = lines[key_line_idx] + next_line = lines[key_line_idx + 1] + # The value's repr is broken onto subsequent lines, so the key + # line ends with ": " (no value content) and the following line + # contains part of the DataFrame's repr. + assert key_line.rstrip() == ' ():' + df_repr_lines = repr(s.make_df()).splitlines() + assert next_line == df_repr_lines[0] + finally: + m._impl._check_sanity() + m.close() + + +def test_info_keeps_single_line_value_inline(): + m = mx.new_model() + try: + s = m.new_space('S') + s.new_cells('scal', formula=lambda: 42) + s.scal() + text = repr(s.scal.info) + # Inline format preserved for single-line values + assert '(): 42' in text + finally: + m._impl._check_sanity() + m.close() + + def test_info_unwraps_single_arg_keys(): m = mx.new_model() try: From 02076a10c65eef8f695421a95fd5aa458a00aa6b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 23 May 2026 05:42:10 +0000 Subject: [PATCH 6/7] Document Interface.info property in reference List the new ``info`` property under the Basic properties section of each Interface subclass reference page (Cells, UserSpace, DynamicSpace, ItemSpace, Macro) and under Model properties in the Model reference page. https://claude.ai/code/session_01RwTSSE7phKRY2m56zpeZi9 --- doc/source/reference/cells.rst | 1 + doc/source/reference/macro.rst | 1 + doc/source/reference/model.rst | 1 + doc/source/reference/space/dynamicspace.rst | 1 + doc/source/reference/space/itemspace.rst | 1 + doc/source/reference/space/userspace.rst | 1 + 6 files changed, 6 insertions(+) diff --git a/doc/source/reference/cells.rst b/doc/source/reference/cells.rst index ab68d05a..d2af12af 100644 --- a/doc/source/reference/cells.rst +++ b/doc/source/reference/cells.rst @@ -23,6 +23,7 @@ Basic properties ~Cells.properties ~Cells.set_property ~Cells.is_cached + ~Cells.info Cells operations diff --git a/doc/source/reference/macro.rst b/doc/source/reference/macro.rst index 2ad6d6a8..93a3045c 100644 --- a/doc/source/reference/macro.rst +++ b/doc/source/reference/macro.rst @@ -15,3 +15,4 @@ Basic properties ~Macro.formula ~Macro.parent + ~Macro.info diff --git a/doc/source/reference/model.rst b/doc/source/reference/model.rst index b5b77cb8..5ecb17f3 100644 --- a/doc/source/reference/model.rst +++ b/doc/source/reference/model.rst @@ -26,6 +26,7 @@ Model properties ~Model.macros ~Model.iospecs ~Model.tracegraph + ~Model.info Model operations ---------------- diff --git a/doc/source/reference/space/dynamicspace.rst b/doc/source/reference/space/dynamicspace.rst index 8729b86c..6411e569 100644 --- a/doc/source/reference/space/dynamicspace.rst +++ b/doc/source/reference/space/dynamicspace.rst @@ -22,6 +22,7 @@ Basic properties ~DynamicSpace.refs ~DynamicSpace.has_params ~DynamicSpace.set_property + ~DynamicSpace.info Inheritance operations ---------------------- diff --git a/doc/source/reference/space/itemspace.rst b/doc/source/reference/space/itemspace.rst index 25fd254d..0d3edd2a 100644 --- a/doc/source/reference/space/itemspace.rst +++ b/doc/source/reference/space/itemspace.rst @@ -24,6 +24,7 @@ Basic properties ~ItemSpace.cur_space ~ItemSpace.has_params ~ItemSpace.set_property + ~ItemSpace.info Inheritance properties diff --git a/doc/source/reference/space/userspace.rst b/doc/source/reference/space/userspace.rst index a1f0c2c8..26c5c1c3 100644 --- a/doc/source/reference/space/userspace.rst +++ b/doc/source/reference/space/userspace.rst @@ -22,6 +22,7 @@ Basic properties ~UserSpace.refs ~UserSpace.has_params ~UserSpace.set_property + ~UserSpace.info Space operations ---------------- From c76cc86bf9b5e451c6665f4a8f471e4d17718675 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 23 May 2026 06:03:19 +0000 Subject: [PATCH 7/7] Tidy docstring example for Interface.info Drop the redundant print() wrapper -- the interactive prompt already displays the wrapper's repr -- and show unwrapped single-argument keys in the cached values listing to match the actual output. https://claude.ai/code/session_01RwTSSE7phKRY2m56zpeZi9 --- modelx/core/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modelx/core/base.py b/modelx/core/base.py index 6f8db1fa..f4022744 100644 --- a/modelx/core/base.py +++ b/modelx/core/base.py @@ -473,7 +473,7 @@ def info(self): Example: .. code-block:: python - >>> print(space.foo.info) + >>> space.foo.info Cells: Model.Space.foo(t) is_cached: True allow_none: None @@ -481,8 +481,8 @@ def info(self): def foo(t): return t * 2 cached values: 2 - (0,): 0 - (1,): 2 + 0: 0 + 1: 2 """ from modelx.core.info import build_info return build_info(self)