From 4f725c3b11f9c5f07311defa364a381fe88244c7 Mon Sep 17 00:00:00 2001 From: Bill Hlavacek Date: Sun, 10 May 2026 15:56:22 -0600 Subject: [PATCH] Fix Pattern/Molecule equality and mutable default arguments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Pattern.__eq__` and `Molecule.__eq__` only checked subset membership of molecules / components, so a pattern was considered equal to a strict superset (a one-molecule pattern would compare equal to a two-molecule pattern that happened to contain it). Add explicit length checks so the directions are symmetric. Replace mutable default args (`molecules=[]`, `components=[]`, `states=[]`) on `Pattern.__init__`, `Molecule.__init__`, `Molecule._add_component`, and `Molecule.add_component` with the standard `None` plumbing, so fresh `Pattern` / `Molecule` instances and `add_component` calls no longer share the same list across instances. Also drops a stale `# TODO: Implement __contains__` comment on `Pattern` — `__contains__` has been implemented for some time. --- bionetgen/modelapi/pattern.py | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/bionetgen/modelapi/pattern.py b/bionetgen/modelapi/pattern.py index 6ef7d929..d0b37fc8 100644 --- a/bionetgen/modelapi/pattern.py +++ b/bionetgen/modelapi/pattern.py @@ -58,9 +58,14 @@ class Pattern: """ def __init__( - self, molecules=[], bonds=None, compartment=None, label=None, canonicalize=False + self, + molecules=None, + bonds=None, + compartment=None, + label=None, + canonicalize=False, ): - self.molecules = molecules + self.molecules = list(molecules) if molecules is not None else [] self._bonds = bonds self.compartment = compartment self.label = label @@ -287,6 +292,12 @@ def __eq__(self, other): other.canonical_label is not None ): return self.canonical_label == other.canonical_label + if len(self.molecules) != len(other.molecules): + logger.debug( + f"molecule count differs: {len(self.molecules)} vs {len(other.molecules)}", + loc=loc, + ) + return False # now we can check contents for molecule in self: if molecule not in other.molecules: @@ -374,8 +385,6 @@ def __getitem__(self, key): def __iter__(self): return self.molecules.__iter__() - # TODO: Implement __contains__ - class Molecule: """ @@ -402,9 +411,9 @@ class Molecule: (for molecule types) "states" """ - def __init__(self, name="0", components=[], compartment=None, label=None): + def __init__(self, name="0", components=None, compartment=None, label=None): self._name = name - self._components = components + self._components = list(components) if components is not None else [] self._compartment = compartment self._label = label self.canonical_order = None @@ -435,6 +444,12 @@ def __eq__(self, other): # we can check canonical labels if self.canonical_label != other.canonical_label: return False + if len(self.components) != len(other.components): + logger.debug( + f"component count differs: {len(self.components)} vs {len(other.components)}", + loc=loc, + ) + return False # check components now for component in self: if component not in other.components: @@ -547,14 +562,14 @@ def label(self, value): # print("Warning: Logical checks are not complete") self._label = value - def _add_component(self, name, state=None, states=[]): + def _add_component(self, name, state=None, states=None): comp_obj = Component() comp_obj.name = name comp_obj.state = state - comp_obj.states = states + comp_obj.states = list(states) if states is not None else [] self.components.append(comp_obj) - def add_component(self, name, state=None, states=[]): + def add_component(self, name, state=None, states=None): # TODO: Add built-in logic here # print("Warning: Logical checks are not complete") self._add_component(name, state, states)