Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion pytm/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,14 @@ def __eq__(self, other: object) -> bool:

def __hash__(self):
"""Make Data objects hashable for use in sets."""
return hash((self.name, self.description, self.format, self.classification))
return hash(
(
getattr(self, "name", None),
getattr(self, "description", None),
getattr(self, "format", None),
getattr(self, "classification", None),
)
)

def _safeset(self, attr: str, value) -> None:
"""Safely set an attribute value."""
Expand Down
45 changes: 45 additions & 0 deletions tests/test_pydantic_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
correctly — coverage that generic functional tests do not exercise.
"""

import copy

import pytest

from pytm import (
Expand Down Expand Up @@ -337,6 +339,49 @@ def test_different_data_distinct_in_set(self):
b = Data("cookie")
assert len({a, b}) == 2

def test_deepcopy_with_dataflow_cycle(self):
# Data -> carriedBy -> Dataflow -> data (DataSet) -> same Data.
# From pydantic 2.14, __deepcopy__ memoizes the new Data before its
# fields are copied, so the DataSet copy hashes a Data with no
# attributes yet.
token = Data("token", classification=Classification.SECRET)
flow = Dataflow(Actor("user"), Server("web"), "login", data=token)
token.carriedBy = [flow]

copied = copy.deepcopy(token)

assert copied is not token
assert copied.name == "token"
assert copied.classification == Classification.SECRET
copied_flow = copied.carriedBy[0]
assert copied_flow is not flow
# Whether the flow's data is the same copy depends on the pydantic
# version; either way it must be a copy of the token, not the token.
assert [d.name for d in copied_flow.data] == ["token"]
assert all(d is not token for d in copied_flow.data)

def test_report_with_data_carried_by_multiple_flows(self, tmp_path):
# TM.report() deep-copies elements. When one Data travels over more
# than one Dataflow this used to raise
# AttributeError: 'Data' object has no attribute 'name'.
TM.reset()
tm = TM("report tm", description="test")
token = Data("token", classification=Classification.SECRET)
web = Server("web")
Dataflow(Actor("user"), web, "login", data=token)
Dataflow(web, Datastore("db"), "store token", data=token)
template = tmp_path / "template.md"
template.write_text("{tm.name}\n{data:repeat:{{item.name}}\n}")

tm.check()
tm.resolve()
assert len(token.carriedBy) == 2

report = tm.report(str(template))

assert "report tm" in report
assert "token" in report


# ---------------------------------------------------------------------------
# Dataflow.hasDataLeaks
Expand Down
Loading