Skip to content

Commit 9666a4d

Browse files
authored
[core][fix] Model: resolve hierarchy not bases (#2124)
1 parent 5e2dbb3 commit 9666a4d

File tree

3 files changed

+55
-24
lines changed

3 files changed

+55
-24
lines changed

fixcore/fixcore/model/model.py

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,10 @@ def package(self) -> Optional[str]:
311311
def meta_get(self, name: str, clazz: Type[T], default: T) -> T:
312312
return default
313313

314+
@property
315+
def is_complex(self) -> bool:
316+
return False
317+
314318
# noinspection PyUnusedLocal
315319
@staticmethod
316320
def from_json(js: Json, _: type = object, **kwargs: object) -> Kind:
@@ -775,15 +779,16 @@ def check_valid(self, obj: JsonElement, **kwargs: bool) -> ValidationResult:
775779
raise AttributeError(f"TransformKind {self.fqn} is not allowed to be supplied.")
776780

777781
def resolve(self, model: Dict[str, Kind]) -> None:
778-
source = model.get(self.runtime_kind)
779-
destination = model.get(self.destination_fqn)
780-
if source and destination and isinstance(source, SimpleKind) and isinstance(destination, SimpleKind):
781-
source.resolve(model)
782-
destination.resolve(model)
783-
self.source_kind = source
784-
self.destination_kind = destination
785-
else:
786-
raise AttributeError(f"Underlying kind not known: {self.destination_fqn}")
782+
if self.source_kind is None or self.destination_kind is None:
783+
source = model.get(self.runtime_kind)
784+
destination = model.get(self.destination_fqn)
785+
if source and destination and isinstance(source, SimpleKind) and isinstance(destination, SimpleKind):
786+
source.resolve(model)
787+
destination.resolve(model)
788+
self.source_kind = source
789+
self.destination_kind = destination
790+
else:
791+
raise AttributeError(f"Underlying kind not known: {self.destination_fqn}")
787792

788793
def as_json(self, **kwargs: bool) -> Json:
789794
return {
@@ -927,6 +932,10 @@ def __init__(
927932
self.__property_by_path: List[ResolvedPropertyPath] = []
928933
self.__synthetic_props: List[ResolvedPropertyPath] = []
929934

935+
@property
936+
def is_complex(self) -> bool:
937+
return True
938+
930939
def as_json(self, **kwargs: bool) -> Json:
931940
result: Json = {"fqn": self.fqn, "aggregate_root": self.aggregate_root}
932941
if kwargs.get("with_metadata", True):
@@ -1251,7 +1260,6 @@ def walk_element(
12511260
def resolve_properties(
12521261
complex_kind: ComplexKind, model: Dict[str, Kind]
12531262
) -> Tuple[List[ResolvedPropertyPath], Dict[PropertyPath, ComplexKind]]:
1254-
visited: Dict[str, PropertyPath] = {}
12551263
result: List[ResolvedPropertyPath] = []
12561264
owner_lookup: Dict[PropertyPath, ComplexKind] = {}
12571265

@@ -1260,16 +1268,17 @@ def path_for(
12601268
prop: Property,
12611269
kind: Kind,
12621270
path: PropertyPath,
1271+
visited_kinds: Dict[str, Set[str]],
12631272
array: bool = False,
12641273
add_prop_to_path: bool = True,
12651274
) -> None:
12661275
prop_name = f"{prop.name}[]" if array else prop.name
1267-
# Detect object cycles: remember the path when we have visited this property.
1268-
# More complex cycles can be detected that way - leave it simple for now.
1276+
# Detect object cycles: remember the kinds we already visited for this property chain.
12691277
key = f"{prop_name}:{prop.kind}"
1270-
if key in visited and prop_name in visited[key].path:
1271-
return
1272-
visited[key] = path
1278+
if kind.is_complex:
1279+
if kind.fqn in visited_kinds[key]:
1280+
return
1281+
visited_kinds[key].add(kind.fqn)
12731282
relative = path.child(prop_name) if add_prop_to_path else path
12741283
# make sure the kind is resolved
12751284
kind.resolve(model)
@@ -1280,7 +1289,7 @@ def path_for(
12801289
if name := relative.last_part:
12811290
result.append(ResolvedPropertyPath(relative, Property(name, kind.fqn), kind))
12821291
owner_lookup[relative] = owner
1283-
path_for(owner, prop, kind.inner, path, True)
1292+
path_for(owner, prop, kind.inner, path, visited_kinds, True)
12841293
elif isinstance(kind, DictionaryKind):
12851294
child = relative.child(None)
12861295
if name := relative.last_part:
@@ -1290,17 +1299,28 @@ def path_for(
12901299
value = kind.value_kind
12911300
result.append(ResolvedPropertyPath(child, Property("any", value.fqn), value))
12921301
owner_lookup[child] = owner
1293-
path_for(owner, prop, kind.value_kind, child, add_prop_to_path=False)
1302+
path_for(owner, prop, kind.value_kind, child, visited_kinds, add_prop_to_path=False)
12941303
elif isinstance(kind, ComplexKind):
12951304
if name := relative.last_part:
12961305
result.append(ResolvedPropertyPath(relative, Property(name, kind.fqn), kind))
12971306
owner_lookup[relative] = owner
1298-
for_complex_kind(owner, kind, relative)
1307+
for_complex_kind(owner, kind, relative, visited_kinds)
12991308

1300-
def for_complex_kind(owner: ComplexKind, current: ComplexKind, relative: PropertyPath) -> None:
1301-
for cpx in list(current.resolved_bases().values()) + [current]:
1302-
for prop in cpx.properties:
1303-
path_for(owner, prop, cpx.__resolved_props[prop.name][1], relative)
1309+
def for_complex_kind(
1310+
owner: ComplexKind,
1311+
current: ComplexKind,
1312+
relative: PropertyPath,
1313+
visited_kinds: Optional[Dict[str, Set[str]]] = None,
1314+
) -> None:
1315+
current.resolve(model)
1316+
bases = current.kind_hierarchy() | {current.fqn}
1317+
for cpx_fqn in bases:
1318+
if isinstance(cpx := model.get(cpx_fqn), ComplexKind):
1319+
cpx.resolve(model)
1320+
for prop in cpx.properties:
1321+
path_for(
1322+
owner, prop, cpx.__resolved_props[prop.name][1], relative, visited_kinds or defaultdict(set)
1323+
)
13041324

13051325
for_complex_kind(complex_kind, complex_kind, PropertyPath([], ""))
13061326
return result, owner_lookup

fixcore/tests/fixcore/cli/command_test.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,18 @@ async def test_kinds_command(cli: CLI, foo_model: Model) -> None:
515515
assert result[0][0] == {
516516
"name": "datetime",
517517
"runtime_kind": "datetime",
518-
"appears_in": ["base", "foo", "bla", "some_complex", "predefined_properties"],
518+
"appears_in": [
519+
"base",
520+
"foo",
521+
"bla",
522+
"cloud",
523+
"account",
524+
"region",
525+
"parent",
526+
"child",
527+
"some_complex",
528+
"predefined_properties",
529+
],
519530
}
520531
with pytest.raises(Exception):
521532
await cli.execute_cli_command("kind foo bla bar", list_sink)

fixcore/tests/fixcore/model/model_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ def test_property_path_on_model(person_model: Model) -> None:
307307
# complex based property path
308308
person: ComplexKind = cast(ComplexKind, person_model["Person"])
309309
person_path = {p.path: p for p in person.resolved_property_paths()}
310-
assert len(person_path) == 35
310+
assert len(person_path) == 41
311311
assert person_path[PropertyPath(["name"])].kind == person_model["string"]
312312
assert person_path[PropertyPath(["name"])].prop.name == "name"
313313
assert person_path[PropertyPath(["list[]"])].kind == person_model["string"]

0 commit comments

Comments
 (0)