From 6c03fc70d9d24f78fbf10a0709fd0e7e03569e7f Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Thu, 14 May 2026 19:43:01 +0100 Subject: [PATCH] fix: exclude __exclude_identifier_fields__ attrs from model.info MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Internal attributes declared in a class's __exclude_identifier_fields__ (e.g. LightProfileLinear.pytree_token in PyAutoGalaxy, GridSearch .number_of_cores in PyAutoFit) were leaking into model.info because they are int-typed and live on objects walked by path_instance_tuples_for_class. The Identifier hash already honors this contract — model.info now does too, by walking back to each leaf's parent and consulting its class's __exclude_identifier_fields__. No public-API change. No downstream library changes needed: declaring __exclude_identifier_fields__ now suppresses an attribute from both the model identity hash and the human-readable info. Co-Authored-By: Claude Opus 4.7 (1M context) --- autofit/mapper/prior_model/abstract.py | 21 +++++++++++++++++++++ test_autofit/mapper/test_constant.py | 19 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/autofit/mapper/prior_model/abstract.py b/autofit/mapper/prior_model/abstract.py index f36ddbb28..cf4751481 100644 --- a/autofit/mapper/prior_model/abstract.py +++ b/autofit/mapper/prior_model/abstract.py @@ -1780,6 +1780,26 @@ def info(self) -> str: """ formatter = TextFormatter(line_length=info_whitespace()) + def _excluded_by_parent(path): + # Honor each parent's `__exclude_identifier_fields__` here so attributes + # that classes have already declared "not part of the model's identity" + # (e.g. JAX pytree tokens) do not leak into the human-readable model.info. + if not path: + return False + parent = self + for step in path[:-1]: + try: + if isinstance(step, int): + parent = parent[step] + elif isinstance(parent, dict): + parent = parent[step] + else: + parent = parent.__dict__[step] + except (AttributeError, KeyError, IndexError, TypeError): + return False + excluded = getattr(type(parent), "__exclude_identifier_fields__", ()) + return path[-1] in excluded + for t in find_groups( [ t @@ -1788,6 +1808,7 @@ def info(self) -> str: ignore_children=True, ) if t[0][-1] not in ("id", "item_number") + and not _excluded_by_parent(t[0]) ], limit=1, ): diff --git a/test_autofit/mapper/test_constant.py b/test_autofit/mapper/test_constant.py index 345ede14a..ac97120cc 100644 --- a/test_autofit/mapper/test_constant.py +++ b/test_autofit/mapper/test_constant.py @@ -185,3 +185,22 @@ def test_is_instance(): constant = af.Constant(1.0) assert isinstance(constant, float) + + +class _ClassWithExcludedField: + # Mirrors the LightProfileLinear pattern: an internal attribute set in + # __init__ that is declared not part of the model identity. The info + # property must respect that contract and suppress `token`. + __exclude_identifier_fields__ = ("token",) + + def __init__(self, value: float = 0.5): + self.value = value + self.token = 7 + + +def test_info_honors_exclude_identifier_fields(): + model = af.Collection(thing=_ClassWithExcludedField()) + + info = model.info + assert "token" not in info + assert "value" in info