diff --git a/bionetgen/modelapi/blocks.py b/bionetgen/modelapi/blocks.py index e03afa2..fa3233e 100644 --- a/bionetgen/modelapi/blocks.py +++ b/bionetgen/modelapi/blocks.py @@ -3,17 +3,11 @@ except ImportError: from collections import OrderedDict from bionetgen.core.utils.logging import BNGLogger +from bionetgen.core.utils.utils import ActionList from .structs import Parameter, Compartment, Observable from .structs import MoleculeType, Species, Function from .structs import Rule, Action from .structs import EnergyPattern, PopulationMap -from bionetgen.core.utils.utils import ActionList - -# this import fails on some python versions -try: - from typing import OrderedDict -except ImportError: - from collections import OrderedDict logger = BNGLogger() @@ -185,6 +179,61 @@ def add_items(self, item_list) -> None: for item in item_list: self.add_item(item) + def _set_item_attribute( + self, + name, + value, + *, + item_cls, + str_field, + kind, + num_field=None, + write_expr_field=None, + ) -> None: + """Shared `__setattr__` path for blocks that hold named items.""" + if not hasattr(self, "items"): + self.__dict__[name] = value + return + if name not in self.items: + self.__dict__[name] = value + return + changed = False + if isinstance(value, item_cls): + changed = True + self.items[name] = value + elif isinstance(value, str): + if self.items[name][str_field] != value: + changed = True + self.items[name][str_field] = value + if write_expr_field is not None: + setattr(self.items[name], write_expr_field, True) + elif num_field is not None: + try: + new_value = float(value) + except (TypeError, ValueError): + logger.warning( + f"Unable to set {kind} {self.items[name]['name']!r} to" + f" {value!r}; keeping existing {num_field}" + f" {self.items[name][num_field]!r}", + loc=f"{__file__} : {self.__class__.__name__}.__setattr__()", + ) + else: + if self.items[name][num_field] != new_value: + changed = True + self.items[name][num_field] = new_value + if write_expr_field is not None: + setattr(self.items[name], write_expr_field, False) + value = new_value + else: + logger.warning( + f"Unable to set {kind} {self.items[name]['name']!r} to" + f" {value!r}; keeping existing {kind}", + loc=f"{__file__} : {self.__class__.__name__}.__setattr__()", + ) + if changed: + self._changes[name] = value + self.__dict__[name] = value + class ParameterBlock(ModelBlock): """ @@ -202,45 +251,15 @@ def __init__(self) -> None: self.name = "parameters" def __setattr__(self, name, value) -> None: - changed = False - if hasattr(self, "items"): - if name in self.items: - if isinstance(value, Parameter): - # New parameter object - changed = True - self.items[name] = value - elif isinstance(value, str): - # A new expression - if self.items[name]["value"] != value: - changed = True - self.items[name]["value"] = value - self.items[name].write_expr = True - else: - try: - # try a new value, we need to make sure - # to stop printing out the expression - new_value = float(value) - if self.items[name]["value"] != new_value: - changed = True - self.items[name]["value"] = new_value - self.items[name].write_expr = False - value = new_value - except (TypeError, ValueError): - logger.warning( - "Unable to set parameter {!r} to {!r}; keeping existing value {!r}".format( - self.items[name]["name"], - value, - self.items[name]["value"], - ), - loc=f"{__file__} : ParameterBlock.__setattr__()", - ) - if changed: - self._changes[name] = value - self.__dict__[name] = value - else: - self.__dict__[name] = value - else: - self.__dict__[name] = value + self._set_item_attribute( + name, + value, + item_cls=Parameter, + str_field="value", + num_field="value", + write_expr_field="write_expr", + kind="parameter", + ) def add_parameter(self, *args, **kwargs) -> None: p = Parameter(*args, **kwargs) @@ -263,39 +282,14 @@ def __init__(self) -> None: self.name = "compartments" def __setattr__(self, name, value) -> None: - changed = False - if hasattr(self, "items"): - if name in self.items: - if isinstance(value, Compartment): - changed = True - self.items[name] = value - elif isinstance(value, str): - if self.items[name]["name"] != value: - changed = True - self.items[name]["name"] = value - else: - try: - new_value = float(value) - if self.items[name]["size"] != new_value: - changed = True - self.items[name]["size"] = new_value - value = new_value - except (TypeError, ValueError): - logger.warning( - "Unable to set compartment {!r} to {!r}; keeping existing size {!r}".format( - self.items[name]["name"], - value, - self.items[name]["size"], - ), - loc=f"{__file__} : CompartmentBlock.__setattr__()", - ) - if changed: - self._changes[name] = value - self.__dict__[name] = value - else: - self.__dict__[name] = value - else: - self.__dict__[name] = value + self._set_item_attribute( + name, + value, + item_cls=Compartment, + str_field="name", + num_field="size", + kind="compartment", + ) def add_compartment(self, *args, **kwargs) -> None: c = Compartment(*args, **kwargs) @@ -318,32 +312,13 @@ def __init__(self) -> None: self.name = "observables" def __setattr__(self, name, value) -> None: - changed = False - if hasattr(self, "items"): - if name in self.items: - if isinstance(value, Observable): - changed = True - self.items[name] = value - elif isinstance(value, str): - if self.items[name]["name"] != value: - changed = True - self.items[name]["name"] = value - else: - logger.warning( - "Unable to set observable {!r} to {!r}; keeping existing observable {!r}".format( - self.items[name]["name"], - value, - self.items[name]["name"], - ), - loc=f"{__file__} : ObservableBlock.__setattr__()", - ) - if changed: - self._changes[name] = value - self.__dict__[name] = value - else: - self.__dict__[name] = value - else: - self.__dict__[name] = value + self._set_item_attribute( + name, + value, + item_cls=Observable, + str_field="name", + kind="observable", + ) def add_observable(self, *args, **kwargs) -> None: o = Observable(*args, **kwargs) @@ -366,32 +341,13 @@ def __init__(self) -> None: self.name = "species" def __setattr__(self, name, value) -> None: - changed = False - if hasattr(self, "items"): - if name in self.items: - if isinstance(value, Species): - changed = True - self.items[name] = value - elif isinstance(value, str): - if self.items[name]["name"] != value: - changed = True - self.items[name]["name"] = value - else: - logger.warning( - "Unable to set species {!r} to {!r}; keeping existing species {!r}".format( - self.items[name]["name"], - value, - self.items[name]["name"], - ), - loc=f"{__file__} : SpeciesBlock.__setattr__()", - ) - if changed: - self._changes[name] = value - self.__dict__[name] = value - else: - self.__dict__[name] = value - else: - self.__dict__[name] = value + self._set_item_attribute( + name, + value, + item_cls=Species, + str_field="name", + kind="species", + ) def __getitem__(self, key): return self.items[key] @@ -421,29 +377,13 @@ def __init__(self) -> None: self.name = "molecule types" def __setattr__(self, name, value) -> None: - changed = False - if hasattr(self, "items"): - if name in self.items: - if isinstance(value, MoleculeType): - changed = True - self.items[name] = value - elif isinstance(value, str): - if self.items[name]["name"] != value: - changed = True - self.items[name]["name"] = value - else: - print( - "can't set molecule type {} to {}".format( - self.items[name]["name"], value - ) - ) - if changed: - self._changes[name] = value - self.__dict__[name] = value - else: - self.__dict__[name] = value - else: - self.__dict__[name] = value + self._set_item_attribute( + name, + value, + item_cls=MoleculeType, + str_field="name", + kind="molecule type", + ) def add_molecule_type(self, name, components) -> None: mt = MoleculeType(name=name, components=components) @@ -466,29 +406,13 @@ def __init__(self) -> None: self.name = "functions" def __setattr__(self, name, value) -> None: - changed = False - if hasattr(self, "items"): - if name in self.items: - if isinstance(value, Function): - changed = True - self.items[name] = value - elif isinstance(value, str): - if self.items[name]["expr"] != value: - changed = True - self.items[name]["expr"] = value - else: - print( - "can't set function {} to {}".format( - self.items[name]["name"], value - ) - ) - if changed: - self._changes[name] = value - self.__dict__[name] = value - else: - self.__dict__[name] = value - else: - self.__dict__[name] = value + self._set_item_attribute( + name, + value, + item_cls=Function, + str_field="expr", + kind="function", + ) def add_function(self, *args, **kwargs) -> None: f = Function(*args, **kwargs) @@ -516,29 +440,13 @@ def __init__(self) -> None: self.name = "reaction rules" def __setattr__(self, name, value) -> None: - changed = False - if hasattr(self, "items"): - if name in self.items: - if isinstance(value, Rule): - changed = True - self.items[name] = value - elif isinstance(value, str): - if self.items[name]["name"] != value: - changed = True - self.items[name]["name"] = value - else: - print( - "can't set rule {} to {}".format( - self.items[name]["name"], value - ) - ) - if changed: - self._changes[name] = value - self.__dict__[name] = value - else: - self.__dict__[name] = value - else: - self.__dict__[name] = value + self._set_item_attribute( + name, + value, + item_cls=Rule, + str_field="name", + kind="rule", + ) def add_rule(self, *args, **kwargs) -> None: r = Rule(*args, **kwargs) @@ -695,29 +603,13 @@ def __init__(self) -> None: self.name = "energy patterns" def __setattr__(self, name, value) -> None: - changed = False - if hasattr(self, "items"): - if name in self.items: - if isinstance(value, EnergyPattern): - changed = True - self.items[name] = value - elif isinstance(value, str): - if self.items[name]["name"] != value: - changed = True - self.items[name]["name"] = value - else: - print( - "can't set energy pattern {} to {}".format( - self.items[name]["name"], value - ) - ) - if changed: - self._changes[name] = value - self.__dict__[name] = value - else: - self.__dict__[name] = value - else: - self.__dict__[name] = value + self._set_item_attribute( + name, + value, + item_cls=EnergyPattern, + str_field="name", + kind="energy pattern", + ) def add_energy_pattern(self, *args, **kwargs) -> None: ep = EnergyPattern(*args, **kwargs) @@ -740,29 +632,13 @@ def __init__(self) -> None: self.name = "population maps" def __setattr__(self, name, value) -> None: - changed = False - if hasattr(self, "items"): - if name in self.items: - if isinstance(value, PopulationMap): - changed = True - self.items[name] = value - elif isinstance(value, str): - if self.items[name]["name"] != value: - changed = True - self.items[name]["name"] = value - else: - print( - "can't set population map {} to {}".format( - self.items[name]["name"], value - ) - ) - if changed: - self._changes[name] = value - self.__dict__[name] = value - else: - self.__dict__[name] = value - else: - self.__dict__[name] = value + self._set_item_attribute( + name, + value, + item_cls=PopulationMap, + str_field="name", + kind="population map", + ) def add_population_map(self, *args, **kwargs) -> None: pm = PopulationMap(*args, **kwargs) diff --git a/bionetgen/modelapi/structs.py b/bionetgen/modelapi/structs.py index 3e7e49e..35b26dc 100644 --- a/bionetgen/modelapi/structs.py +++ b/bionetgen/modelapi/structs.py @@ -1,7 +1,10 @@ +from bionetgen.core.exc import BNGParseError +from bionetgen.core.utils.logging import BNGLogger +from bionetgen.core.utils.utils import ActionList from bionetgen.modelapi.pattern import Molecule, Pattern from bionetgen.modelapi.rulemod import RuleMod -from bionetgen.core.utils.utils import ActionList -from bionetgen.core.exc import BNGParseError + +logger = BNGLogger() class ModelObj: @@ -318,8 +321,9 @@ def __init__(self, action_type=None, action_args=None) -> None: seen_args = [] for arg_name, arg_value in action_args.items(): if arg_name in seen_args: - print( - f"Warning: argument {arg_name} already given, using latter value {arg_value}" + logger.warning( + f"argument {arg_name} already given, using latter value {arg_value}", + loc=f"{__file__} : Action.__init__()", ) else: seen_args.append(arg_name) diff --git a/bionetgen/network/blocks.py b/bionetgen/network/blocks.py index fbcf2ab..65269df 100644 --- a/bionetgen/network/blocks.py +++ b/bionetgen/network/blocks.py @@ -7,12 +7,6 @@ from .structs import NetworkSpecies, NetworkFunction, NetworkReaction from .structs import NetworkEnergyPattern, NetworkPopulationMap -# this import fails on some python versions -try: - from typing import OrderedDict -except ImportError: - from collections import OrderedDict - logger = BNGLogger() @@ -146,6 +140,61 @@ def add_items(self, item_list) -> None: for item in item_list: self.add_item(item) + def _set_item_attribute( + self, + name, + value, + *, + item_cls, + str_field, + kind, + num_field=None, + write_expr_field=None, + ) -> None: + """Shared `__setattr__` path for blocks that hold named items.""" + if not hasattr(self, "items"): + self.__dict__[name] = value + return + if name not in self.items: + self.__dict__[name] = value + return + changed = False + if isinstance(value, item_cls): + changed = True + self.items[name] = value + elif isinstance(value, str): + if self.items[name][str_field] != value: + changed = True + self.items[name][str_field] = value + if write_expr_field is not None: + setattr(self.items[name], write_expr_field, True) + elif num_field is not None: + try: + new_value = float(value) + except (TypeError, ValueError): + logger.warning( + f"Unable to set {kind} {self.items[name]['name']!r} to" + f" {value!r}; keeping existing {num_field}" + f" {self.items[name][num_field]!r}", + loc=f"{__file__} : {self.__class__.__name__}.__setattr__()", + ) + else: + if self.items[name][num_field] != new_value: + changed = True + self.items[name][num_field] = new_value + if write_expr_field is not None: + setattr(self.items[name], write_expr_field, False) + value = new_value + else: + logger.warning( + f"Unable to set {kind} {self.items[name]['name']!r} to" + f" {value!r}; keeping existing {kind}", + loc=f"{__file__} : {self.__class__.__name__}.__setattr__()", + ) + if changed: + self._changes[name] = value + self.__dict__[name] = value + class NetworkParameterBlock(NetworkBlock): """ @@ -163,45 +212,15 @@ def __init__(self) -> None: self.name = "parameters" def __setattr__(self, name, value) -> None: - changed = False - if hasattr(self, "items"): - if name in self.items: - if isinstance(value, NetworkParameter): - # New parameter object - changed = True - self.items[name] = value - elif isinstance(value, str): - # A new expression - if self.items[name]["value"] != value: - changed = True - self.items[name]["value"] = value - self.items[name].write_expr = True - else: - try: - # try a new value, we need to make sure - # to stop printing out the expression - new_value = float(value) - if self.items[name]["value"] != new_value: - changed = True - self.items[name]["value"] = new_value - self.items[name].write_expr = False - value = new_value - except (TypeError, ValueError): - logger.warning( - "Unable to set parameter {!r} to {!r}; keeping existing value {!r}".format( - self.items[name]["name"], - value, - self.items[name]["value"], - ), - loc=f"{__file__} : NetworkParameterBlock.__setattr__()", - ) - if changed: - self._changes[name] = value - self.__dict__[name] = value - else: - self.__dict__[name] = value - else: - self.__dict__[name] = value + self._set_item_attribute( + name, + value, + item_cls=NetworkParameter, + str_field="value", + num_field="value", + write_expr_field="write_expr", + kind="parameter", + ) def add_parameter(self, *args, **kwargs) -> None: p = NetworkParameter(*args, **kwargs) @@ -224,39 +243,14 @@ def __init__(self) -> None: self.name = "compartments" def __setattr__(self, name, value) -> None: - changed = False - if hasattr(self, "items"): - if name in self.items: - if isinstance(value, NetworkCompartment): - changed = True - self.items[name] = value - elif isinstance(value, str): - if self.items[name]["name"] != value: - changed = True - self.items[name]["name"] = value - else: - try: - new_value = float(value) - if self.items[name]["size"] != new_value: - changed = True - self.items[name]["size"] = new_value - value = new_value - except (TypeError, ValueError): - logger.warning( - "Unable to set compartment {!r} to {!r}; keeping existing size {!r}".format( - self.items[name]["name"], - value, - self.items[name]["size"], - ), - loc=f"{__file__} : NetworkCompartmentBlock.__setattr__()", - ) - if changed: - self._changes[name] = value - self.__dict__[name] = value - else: - self.__dict__[name] = value - else: - self.__dict__[name] = value + self._set_item_attribute( + name, + value, + item_cls=NetworkCompartment, + str_field="name", + num_field="size", + kind="compartment", + ) def add_compartment(self, *args, **kwargs) -> None: c = NetworkCompartment(*args, **kwargs) @@ -279,32 +273,13 @@ def __init__(self) -> None: self.name = "groups" def __setattr__(self, name, value) -> None: - changed = False - if hasattr(self, "items"): - if name in self.items: - if isinstance(value, NetworkGroup): - changed = True - self.items[name] = value - elif isinstance(value, str): - if self.items[name]["name"] != value: - changed = True - self.items[name]["name"] = value - else: - logger.warning( - "Unable to set group {!r} to {!r}; keeping existing group {!r}".format( - self.items[name]["name"], - value, - self.items[name]["name"], - ), - loc=f"{__file__} : NetworkGroupBlock.__setattr__()", - ) - if changed: - self._changes[name] = value - self.__dict__[name] = value - else: - self.__dict__[name] = value - else: - self.__dict__[name] = value + self._set_item_attribute( + name, + value, + item_cls=NetworkGroup, + str_field="name", + kind="group", + ) def add_group(self, *args, **kwargs) -> None: g = NetworkGroup(*args, **kwargs) @@ -327,32 +302,13 @@ def __init__(self) -> None: self.name = "species" def __setattr__(self, name, value) -> None: - changed = False - if hasattr(self, "items"): - if name in self.items: - if isinstance(value, NetworkSpecies): - changed = True - self.items[name] = value - elif isinstance(value, str): - if self.items[name]["name"] != value: - changed = True - self.items[name]["name"] = value - else: - logger.warning( - "Unable to set species {!r} to {!r}; keeping existing species {!r}".format( - self.items[name]["name"], - value, - self.items[name]["name"], - ), - loc=f"{__file__} : NetworkSpeciesBlock.__setattr__()", - ) - if changed: - self._changes[name] = value - self.__dict__[name] = value - else: - self.__dict__[name] = value - else: - self.__dict__[name] = value + self._set_item_attribute( + name, + value, + item_cls=NetworkSpecies, + str_field="name", + kind="species", + ) def __getitem__(self, key): return self.items[key] @@ -382,29 +338,13 @@ def __init__(self) -> None: self.name = "functions" def __setattr__(self, name, value) -> None: - changed = False - if hasattr(self, "items"): - if name in self.items: - if isinstance(value, NetworkFunction): - changed = True - self.items[name] = value - elif isinstance(value, str): - if self.items[name]["expr"] != value: - changed = True - self.items[name]["expr"] = value - else: - print( - "can't set function {} to {}".format( - self.items[name]["name"], value - ) - ) - if changed: - self._changes[name] = value - self.__dict__[name] = value - else: - self.__dict__[name] = value - else: - self.__dict__[name] = value + self._set_item_attribute( + name, + value, + item_cls=NetworkFunction, + str_field="expr", + kind="function", + ) def add_function(self, *args, **kwargs) -> None: f = NetworkFunction(*args, **kwargs) @@ -432,29 +372,13 @@ def __init__(self) -> None: self.name = "reactions" def __setattr__(self, name, value) -> None: - changed = False - if hasattr(self, "items"): - if name in self.items: - if isinstance(value, NetworkReaction): - changed = True - self.items[name] = value - elif isinstance(value, str): - if self.items[name]["name"] != value: - changed = True - self.items[name]["name"] = value - else: - print( - "can't set rule {} to {}".format( - self.items[name]["name"], value - ) - ) - if changed: - self._changes[name] = value - self.__dict__[name] = value - else: - self.__dict__[name] = value - else: - self.__dict__[name] = value + self._set_item_attribute( + name, + value, + item_cls=NetworkReaction, + str_field="name", + kind="reaction", + ) def add_reaction(self, *args, **kwargs) -> None: r = NetworkReaction(*args, **kwargs) @@ -477,29 +401,13 @@ def __init__(self) -> None: self.name = "energy patterns" def __setattr__(self, name, value) -> None: - changed = False - if hasattr(self, "items"): - if name in self.items: - if isinstance(value, NetworkEnergyPattern): - changed = True - self.items[name] = value - elif isinstance(value, str): - if self.items[name]["name"] != value: - changed = True - self.items[name]["name"] = value - else: - print( - "can't set energy pattern {} to {}".format( - self.items[name]["name"], value - ) - ) - if changed: - self._changes[name] = value - self.__dict__[name] = value - else: - self.__dict__[name] = value - else: - self.__dict__[name] = value + self._set_item_attribute( + name, + value, + item_cls=NetworkEnergyPattern, + str_field="name", + kind="energy pattern", + ) def add_energy_pattern(self, *args, **kwargs) -> None: ep = NetworkEnergyPattern(*args, **kwargs) @@ -522,29 +430,13 @@ def __init__(self) -> None: self.name = "population maps" def __setattr__(self, name, value) -> None: - changed = False - if hasattr(self, "items"): - if name in self.items: - if isinstance(value, NetworkPopulationMap): - changed = True - self.items[name] = value - elif isinstance(value, str): - if self.items[name]["name"] != value: - changed = True - self.items[name]["name"] = value - else: - print( - "can't set population map {} to {}".format( - self.items[name]["name"], value - ) - ) - if changed: - self._changes[name] = value - self.__dict__[name] = value - else: - self.__dict__[name] = value - else: - self.__dict__[name] = value + self._set_item_attribute( + name, + value, + item_cls=NetworkPopulationMap, + str_field="name", + kind="population map", + ) def add_population_map(self, *args, **kwargs) -> None: pm = NetworkPopulationMap(*args, **kwargs) diff --git a/tests/test_block_setattr_logging.py b/tests/test_block_setattr_logging.py new file mode 100644 index 0000000..0dfa6ea --- /dev/null +++ b/tests/test_block_setattr_logging.py @@ -0,0 +1,362 @@ +"""Focused tests for block setattr logging and helper deduplication.""" + +from collections import OrderedDict +from unittest.mock import patch + +import pytest + +from bionetgen.modelapi.blocks import ( + CompartmentBlock, + EnergyPatternBlock, + FunctionBlock, + MoleculeTypeBlock, + ObservableBlock, + ParameterBlock, + PopulationMapBlock, + RuleBlock, + SpeciesBlock, +) +from bionetgen.modelapi.structs import Action, Rule, Species +from bionetgen.network.blocks import ( + NetworkCompartmentBlock, + NetworkEnergyPatternBlock, + NetworkFunctionBlock, + NetworkGroupBlock, + NetworkParameterBlock, + NetworkPopulationMapBlock, + NetworkReactionBlock, + NetworkSpeciesBlock, +) +from bionetgen.network.structs import NetworkReaction, NetworkSpecies + + +class FakePattern: + """Minimal mock for Pattern-like objects used by several block items.""" + + def __init__(self, name="A()"): + self.name = name + self.MatchOnce = False + + def __str__(self): + return self.name + + +class DuplicateArgsDict(dict): + """A dict subclass whose items() preserves duplicate logical arguments.""" + + def items(self): + return [("method", '"ssa"'), ("method", '"ode"')] + + +def _make_parameter_block(): + block = ParameterBlock() + block.add_parameter("k1", 0.5) + block._changes.clear() + return block, "k1", block["k1"] + + +def _make_compartment_block(): + block = CompartmentBlock() + block.add_compartment("EC", 3, 1.0) + block._changes.clear() + return block, "EC", block["EC"] + + +def _make_observable_block(): + block = ObservableBlock() + block.add_observable("obsA", "Molecules", [FakePattern("A()")]) + block._changes.clear() + return block, "obsA", block["obsA"] + + +def _make_species_block(): + block = SpeciesBlock() + block.items["A()"] = Species(pattern=FakePattern("A()"), count=100) + block._changes.clear() + return block, "A()", block["A()"] + + +def _make_molecule_type_block(): + block = MoleculeTypeBlock() + block.add_molecule_type("A", []) + block._changes.clear() + return block, "A", block["A"] + + +def _make_function_block(): + block = FunctionBlock() + block.add_function("f1", "k1*A") + block._changes.clear() + return block, "f1", block["f1"] + + +def _make_rule_block(): + block = RuleBlock() + pattern = FakePattern("A()") + block.add_rule( + "r1", reactants=[pattern], products=[pattern], rate_constants=("k1",) + ) + block._changes.clear() + return block, "r1", block["r1"] + + +def _make_energy_pattern_block(): + block = EnergyPatternBlock() + block.add_energy_pattern("ep0", "A()", "E0") + block._changes.clear() + return block, "ep0", block["ep0"] + + +def _make_population_map_block(): + block = PopulationMapBlock() + block.add_population_map("pm0", "A()", "A_pop", "lump0") + block._changes.clear() + return block, "pm0", block["pm0"] + + +def _make_network_parameter_block(): + block = NetworkParameterBlock() + block.add_parameter(1, "k1", "0.5") + block._changes.clear() + return block, "k1", block["k1"] + + +def _make_network_compartment_block(): + block = NetworkCompartmentBlock() + block.add_compartment("cytoplasm", 3, "1.0") + block._changes.clear() + return block, "cytoplasm", block["cytoplasm"] + + +def _make_network_group_block(): + block = NetworkGroupBlock() + block.add_group(1, "Atot", members=["1"]) + block._changes.clear() + return block, "Atot", block["Atot"] + + +def _make_network_species_block(): + block = NetworkSpeciesBlock() + block.items["A(b)"] = NetworkSpecies(1, "A(b)", count=100) + block._changes.clear() + return block, "A(b)", block["A(b)"] + + +def _make_network_function_block(): + block = NetworkFunctionBlock() + block.add_function("rate", "k1*A") + block._changes.clear() + return block, "rate", block["rate"] + + +def _make_network_reaction_block(): + block = NetworkReactionBlock() + block.items["rxn1"] = NetworkReaction( + 1, reactants=["1"], products=["2"], rate_constant="k1" + ) + block._changes.clear() + return block, "rxn1", block.items["rxn1"] + + +def _make_network_energy_pattern_block(): + block = NetworkEnergyPatternBlock() + block.add_energy_pattern("ep1", "A(b!1).B(a!1)", "epsilon") + block._changes.clear() + return block, "ep1", block["ep1"] + + +def _make_network_population_map_block(): + block = NetworkPopulationMapBlock() + block.add_population_map("pm1", "A(b~0)", "A_pop", "lump1") + block._changes.clear() + return block, "pm1", block["pm1"] + + +MODEL_CASES = [ + ( + _make_parameter_block, + object(), + "Unable to set parameter 'k1'", + "keeping existing value", + "ParameterBlock.__setattr__()", + ), + ( + _make_compartment_block, + object(), + "Unable to set compartment 'EC'", + "keeping existing size", + "CompartmentBlock.__setattr__()", + ), + ( + _make_observable_block, + 42, + "Unable to set observable 'obsA'", + "keeping existing observable", + "ObservableBlock.__setattr__()", + ), + ( + _make_species_block, + 42, + "Unable to set species 'A()'", + "keeping existing species", + "SpeciesBlock.__setattr__()", + ), + ( + _make_molecule_type_block, + 42, + "Unable to set molecule type 'A'", + "keeping existing molecule type", + "MoleculeTypeBlock.__setattr__()", + ), + ( + _make_function_block, + 42, + "Unable to set function 'f1'", + "keeping existing function", + "FunctionBlock.__setattr__()", + ), + ( + _make_rule_block, + 42, + "Unable to set rule 'r1'", + "keeping existing rule", + "RuleBlock.__setattr__()", + ), + ( + _make_energy_pattern_block, + 42, + "Unable to set energy pattern 'ep0'", + "keeping existing energy pattern", + "EnergyPatternBlock.__setattr__()", + ), + ( + _make_population_map_block, + 42, + "Unable to set population map 'pm0'", + "keeping existing population map", + "PopulationMapBlock.__setattr__()", + ), +] + + +NETWORK_CASES = [ + ( + _make_network_parameter_block, + object(), + "Unable to set parameter 'k1'", + "keeping existing value", + "NetworkParameterBlock.__setattr__()", + ), + ( + _make_network_compartment_block, + object(), + "Unable to set compartment 'cytoplasm'", + "keeping existing size", + "NetworkCompartmentBlock.__setattr__()", + ), + ( + _make_network_group_block, + 42, + "Unable to set group 'Atot'", + "keeping existing group", + "NetworkGroupBlock.__setattr__()", + ), + ( + _make_network_species_block, + 42, + "Unable to set species 'A(b)'", + "keeping existing species", + "NetworkSpeciesBlock.__setattr__()", + ), + ( + _make_network_function_block, + 42, + "Unable to set function 'rate'", + "keeping existing function", + "NetworkFunctionBlock.__setattr__()", + ), + ( + _make_network_reaction_block, + 42, + "Unable to set reaction 1", + "keeping existing reaction", + "NetworkReactionBlock.__setattr__()", + ), + ( + _make_network_energy_pattern_block, + 42, + "Unable to set energy pattern 'ep1'", + "keeping existing energy pattern", + "NetworkEnergyPatternBlock.__setattr__()", + ), + ( + _make_network_population_map_block, + 42, + "Unable to set population map 'pm1'", + "keeping existing population map", + "NetworkPopulationMapBlock.__setattr__()", + ), +] + + +@pytest.mark.parametrize( + ("factory", "invalid_value", "message_fragment", "keep_fragment", "loc_fragment"), + MODEL_CASES, +) +def test_model_block_setattr_invalid_type_logs_warning( + factory, invalid_value, message_fragment, keep_fragment, loc_fragment +): + from bionetgen.modelapi import blocks as blocks_module + + block, attr_name, existing_item = factory() + + with patch.object(blocks_module, "logger") as mock_logger: + setattr(block, attr_name, invalid_value) + + mock_logger.warning.assert_called_once() + warning_args, warning_kwargs = mock_logger.warning.call_args + assert message_fragment in warning_args[0] + assert keep_fragment in warning_args[0] + assert loc_fragment in warning_kwargs["loc"] + if isinstance(block._changes, OrderedDict): + assert len(block._changes) == 0 + assert block.items[attr_name] is existing_item + + +@pytest.mark.parametrize( + ("factory", "invalid_value", "message_fragment", "keep_fragment", "loc_fragment"), + NETWORK_CASES, +) +def test_network_block_setattr_invalid_type_logs_warning( + factory, invalid_value, message_fragment, keep_fragment, loc_fragment +): + from bionetgen.network import blocks as blocks_module + + block, attr_name, existing_item = factory() + + with patch.object(blocks_module, "logger") as mock_logger: + setattr(block, attr_name, invalid_value) + + mock_logger.warning.assert_called_once() + warning_args, warning_kwargs = mock_logger.warning.call_args + assert message_fragment in warning_args[0] + assert keep_fragment in warning_args[0] + assert loc_fragment in warning_kwargs["loc"] + assert len(block._changes) == 0 + assert block.items[attr_name] is existing_item + + +def test_action_duplicate_args_logs_warning(): + from bionetgen.modelapi import structs as structs_module + + with patch.object(structs_module, "logger") as mock_logger: + action = Action( + action_type="simulate", action_args=DuplicateArgsDict({"method": '"ode"'}) + ) + + mock_logger.warning.assert_called_once() + warning_args, warning_kwargs = mock_logger.warning.call_args + assert "argument method already given" in warning_args[0] + assert 'latter value "ode"' in warning_args[0] + assert "Action.__init__()" in warning_kwargs["loc"] + assert action.type == "simulate"