Skip to content

set_attribute silently does nothing for an attribute that does not exist yet #2538

Description

@Flix6x

Summary

set_attribute only updates an attribute that is already there. Setting one that does not exist yet silently does nothing:

# flexmeasures/data/models/generic_assets.py
def has_attribute(self, attribute: str) -> bool:
    return attribute in self.attributes

def set_attribute(self, attribute: str, value):
    if self.has_attribute(attribute):
        self.attributes[attribute] = value

Sensor.set_attribute (flexmeasures/data/models/time_series.py:286) is identical. No exception, no log, no return value: a caller cannot tell the difference between a write that happened and one that did not.

Why this matters

It has already caught someone out. flexmeasures/data/services/scheduling.py:868 carries a workaround with the reason spelled out:

# Direct attribute assignment works for both new and existing attributes.
# set_attribute() is intentionally not used here because it silently
# no-ops when the attribute does not yet exist.
result_sensor.attributes["consumption_is_positive"] = intended

And the remaining production call sites look exposed to exactly that. StorageScheduler._set_soc_attributes (flexmeasures/data/models/planning/storage.py:1920) writes soc_datetime and soc_in_mwh through set_attribute, so on an asset that does not already carry those two attributes the write is dropped. That path is marked deprecated, but it is live until it goes.

It also makes a natural-looking line in a test or a script a no-op, which is how I ran into it: asset.set_attribute("timezone", "Europe/Amsterdam") followed by a commit leaves has_attribute("timezone") False.

Options

  1. Make it set. self.attributes[attribute] = value unconditionally — what the name promises, and what the scheduling.py workaround does by hand. Then audit whether any caller was relying on the guard to avoid creating an attribute (none obviously is; the only non-test callers are the two in storage.py).
  2. Make it refuse. Raise on an unknown attribute, so a dropped write becomes loud. Safer for whatever relied on the current behaviour, but noisier, and the callers above would then need attributes[...] = ... anyway.
  3. Leave it, rename it to something that says what it does (update_existing_attribute), and give callers a separate way to create one.

Option 1 seems right: the guard reads as a leftover rather than a deliberate rule, nothing documents it, and the one place that noticed worked around it rather than depending on it.

Whichever way it goes, Sensor.set_attribute should change with it, and the scheduling.py workaround and its comment can then be dropped.

Acceptance criteria

  • Setting an attribute that does not exist yet either takes effect or raises, with no third silent outcome.
  • StorageScheduler._set_soc_attributes records soc_datetime and soc_in_mwh on an asset that had neither.
  • GenericAsset and Sensor behave the same way.

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

    Labels

    DatabugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions