Skip to content

Commit f2c5a00

Browse files
authored
Merge pull request #88 from apdavison/separate-reverse-properties
Separate forward and reverse properties into different attributes
2 parents 56359f8 + 5672c50 commit f2c5a00

File tree

227 files changed

+657
-76
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

227 files changed

+657
-76
lines changed

builder/fairgraph_module_template.py.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ class {{ class_name }}({{ base_class }}):
3232
{%- if prop.required %}required={{prop.required}},{% endif -%}
3333
doc="{{prop.doc}}"),
3434
{% endfor %}
35+
]
36+
reverse_properties = [
3537
{% for prop in reverse_properties -%}
3638
Property(
3739
"{{prop.name}}", {{prop.type_str}},

fairgraph/base.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919

2020

2121
from __future__ import annotations
22-
2322
from typing import TYPE_CHECKING, Optional, Dict, List, Union, Any
2423
from typing_extensions import TypeAlias
2524
from copy import copy
@@ -76,6 +75,7 @@ def resolve(
7675

7776
class ContainsMetadata(Resolvable, metaclass=Registry): # KGObject and EmbeddedMetadata
7877
properties: List[Property]
78+
reverse_properties: List[Property]
7979
context: Dict[str, str]
8080
type_: List[str]
8181
scope: Optional[str]
@@ -86,7 +86,7 @@ class ContainsMetadata(Resolvable, metaclass=Registry): # KGObject and Embedded
8686

8787
def __init__(self, data: Optional[Dict] = None, **properties):
8888
properties_copy = copy(properties)
89-
for prop in self.properties:
89+
for prop in self.__class__.all_properties:
9090
try:
9191
val = properties[prop.name]
9292
except KeyError:
@@ -169,7 +169,7 @@ def to_jsonld(
169169
data: JSONdict = {"@type": self.type_}
170170
if hasattr(self, "id") and self.id:
171171
data["@id"] = self.id
172-
for prop in self.properties:
172+
for prop in self.__class__.all_properties:
173173
if prop.intrinsic or include_reverse_properties:
174174
expanded_path = prop.expanded_path
175175
value = getattr(self, prop.name)
@@ -242,7 +242,7 @@ def set_error_handling(
242242
else:
243243
prop.error_handling = value
244244
else:
245-
for prop in cls.properties:
245+
for prop in cls.all_properties:
246246
prop.error_handling = value
247247

248248
@classmethod
@@ -267,7 +267,7 @@ def normalize_filter(cls, filter_dict: Dict[str, Any]) -> Dict[str, Any]:
267267
if name_ in filter_dict_copy:
268268
filter_dict_copy[alias_] = filter_dict_copy.pop(name_)
269269

270-
for prop in cls.properties:
270+
for prop in cls.all_properties:
271271
if prop.name in filter_dict_copy:
272272
value = filter_dict_copy[prop.name]
273273
if isinstance(value, dict):
@@ -289,7 +289,7 @@ def generate_query_properties(cls, follow_links: Optional[Dict[str, Any]] = None
289289
"""
290290
properties = [QueryProperty("@type")]
291291
reverse_aliases = invert_dict(cls.aliases)
292-
for prop in cls.properties:
292+
for prop in cls.all_properties:
293293
if prop.is_link and follow_links:
294294
if prop.name in follow_links:
295295
properties.extend(prop.get_query_properties(follow_links[prop.name]))
@@ -314,7 +314,7 @@ def generate_query_filter_properties(
314314
if filters is None:
315315
filters = {}
316316
properties = []
317-
for prop in cls.properties:
317+
for prop in cls.all_properties:
318318
if prop.name in filters:
319319
properties.append(prop.get_query_filter_property(filters[prop.name]))
320320
return properties
@@ -343,7 +343,7 @@ def _deserialize_data(cls, data: JSONdict, client: KGClient, include_id: bool =
343343
if otype not in D["@type"]:
344344
raise TypeError("type mismatch {} - {}".format(otype, D["@type"]))
345345
deserialized_data = {}
346-
for prop in cls.properties:
346+
for prop in cls.all_properties:
347347
expanded_path = expand_uri(prop.path, cls.context)
348348
data_item = D.get(expanded_path)
349349
if data_item is not None and prop.reverse:
@@ -386,7 +386,7 @@ def resolve(
386386
use_scope = scope or self.scope or "released"
387387
if follow_links:
388388
reverse_aliases = invert_dict(self.__class__.aliases)
389-
for prop in self.properties:
389+
for prop in self.__class__.all_properties:
390390
if prop.is_link:
391391
follow_name = None
392392
if prop.name in follow_links:
@@ -436,7 +436,7 @@ def _build_existence_query(self) -> Union[None, Dict[str, Any]]:
436436

437437
query_properties = []
438438
for property_name in self.existence_query_properties:
439-
for property in self.properties:
439+
for property in self.__class__.all_properties:
440440
if property.name == property_name:
441441
query_properties.append(property)
442442
break

fairgraph/kgobject.py

Lines changed: 48 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ def __init__(
7777
self.scope = scope
7878
self.allow_update = True
7979
super().__init__(data=data, **properties)
80-
for prop in self.properties:
81-
if prop.reverse and not hasattr(self, prop.name):
80+
for prop in self.reverse_properties:
81+
if not hasattr(self, prop.name):
8282
query = KGQuery(
8383
prop.types, {prop.reverse: self.id}, callback=lambda value: setattr(self, prop.name, value)
8484
)
@@ -87,7 +87,7 @@ def __init__(
8787
def __repr__(self):
8888
template_parts = (
8989
"{}={{self.{}!r}}".format(prop.name, prop.name)
90-
for prop in self.properties
90+
for prop in self.__class__.all_properties
9191
if getattr(self, prop.name) is not None
9292
)
9393
template = "{self.__class__.__name__}(" + ", ".join(template_parts) + ", space={self.space}, id={self.id})"
@@ -424,7 +424,7 @@ def _update_empty_properties(self, data: JSONdict, client: KGClient):
424424
"""Replace any empty properties (value None) with the supplied data"""
425425
cls = self.__class__
426426
deserialized_data = cls._deserialize_data(data, client, include_id=True)
427-
for prop in cls.properties:
427+
for prop in cls.all_properties:
428428
current_value = getattr(self, prop.name, None)
429429
if current_value is None:
430430
value = deserialized_data[prop.name]
@@ -445,11 +445,11 @@ def __ne__(self, other):
445445
if self.id and other.id and self.id != other.id:
446446
return True
447447
for prop in self.properties:
448-
if prop.intrinsic:
449-
val_self = getattr(self, prop.name)
450-
val_other = getattr(other, prop.name)
451-
if val_self != val_other:
452-
return True
448+
assert prop.intrinsic
449+
val_self = getattr(self, prop.name)
450+
val_other = getattr(other, prop.name)
451+
if val_self != val_other:
452+
return True
453453
return False
454454

455455
def diff(self, other):
@@ -463,11 +463,11 @@ def diff(self, other):
463463
if self.id != other.id:
464464
differences["id"] = (self.id, other.id)
465465
for prop in self.properties:
466-
if prop.intrinsic:
467-
val_self = getattr(self, prop.name)
468-
val_other = getattr(other, prop.name)
469-
if val_self != val_other:
470-
differences["properties"][prop.name] = (val_self, val_other)
466+
assert prop.intrinsic
467+
val_self = getattr(self, prop.name)
468+
val_other = getattr(other, prop.name)
469+
if val_self != val_other:
470+
differences["properties"][prop.name] = (val_self, val_other)
471471
return differences
472472

473473
def exists(self, client: KGClient) -> bool:
@@ -568,39 +568,39 @@ def save(
568568
"""
569569
if recursive:
570570
for prop in self.properties:
571-
if prop.intrinsic:
572-
# we do not save reverse properties, those objects must be saved separately
573-
# this could be revisited, but we'll have to be careful about loops
574-
# if saving recursively
575-
values = getattr(self, prop.name)
576-
for value in as_list(values):
577-
if isinstance(value, ContainsMetadata):
578-
target_space: Optional[str]
579-
if value.space:
580-
target_space = value.space
581-
elif (
582-
isinstance(value, KGObject)
583-
and value.__class__.default_space == "controlled"
584-
and value.exists(client)
585-
and value.space == "controlled"
586-
):
571+
assert prop.intrinsic
572+
# we do not save reverse properties, those objects must be saved separately
573+
# this could be revisited, but we'll have to be careful about loops
574+
# if saving recursively
575+
values = getattr(self, prop.name)
576+
for value in as_list(values):
577+
if isinstance(value, ContainsMetadata):
578+
target_space: Optional[str]
579+
if value.space:
580+
target_space = value.space
581+
elif (
582+
isinstance(value, KGObject)
583+
and value.__class__.default_space == "controlled"
584+
and value.exists(client)
585+
and value.space == "controlled"
586+
):
587+
continue
588+
elif space is None and self.space is not None:
589+
target_space = self.space
590+
else:
591+
target_space = space
592+
if target_space == "controlled":
593+
assert isinstance(value, KGObject) # for type checking
594+
if value.exists(client) and value.space == "controlled":
587595
continue
588-
elif space is None and self.space is not None:
589-
target_space = self.space
590596
else:
591-
target_space = space
592-
if target_space == "controlled":
593-
assert isinstance(value, KGObject) # for type checking
594-
if value.exists(client) and value.space == "controlled":
595-
continue
596-
else:
597-
raise AuthorizationError("Cannot write to controlled space")
598-
value.save(
599-
client,
600-
space=target_space,
601-
recursive=True,
602-
activity_log=activity_log,
603-
)
597+
raise AuthorizationError("Cannot write to controlled space")
598+
value.save(
599+
client,
600+
space=target_space,
601+
recursive=True,
602+
activity_log=activity_log,
603+
)
604604
if space is None:
605605
if self.space is None:
606606
space = self.__class__.default_space
@@ -762,7 +762,7 @@ def show(self, max_width: Optional[int] = None, include_empty_properties=False):
762762
("space", str(self.space)),
763763
("type", self.type_[0]),
764764
]
765-
for prop in self.properties:
765+
for prop in self.__class__.all_properties:
766766
value = getattr(self, prop.name, None)
767767
if include_empty_properties or not isinstance(value, (type(None), KGQuery)):
768768
data.append((prop.name, str(value)))
@@ -836,7 +836,8 @@ def children(
836836
self.resolve(client, follow_links=follow_links)
837837
all_children = []
838838
for prop in self.properties:
839-
if prop.intrinsic and prop.is_link:
839+
assert prop.intrinsic
840+
if prop.is_link:
840841
children = as_list(getattr(self, prop.name))
841842
all_children.extend(children)
842843
if follow_links:

fairgraph/openminds/chemicals/amount_of_chemical.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class AmountOfChemical(EmbeddedMetadata):
3535
doc="no description available",
3636
),
3737
]
38-
existence_query_properties = ("chemical_product", "amount")
38+
reverse_properties = []
3939

4040
def __init__(self, amount=None, chemical_product=None, id=None, data=None, space=None, scope=None):
4141
return super().__init__(data=data, amount=amount, chemical_product=chemical_product)

fairgraph/openminds/chemicals/chemical_mixture.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ class ChemicalMixture(KGObject):
5656
required=True,
5757
doc="Distinct class to which a group of entities or concepts with similar characteristics or attributes belong to.",
5858
),
59+
]
60+
reverse_properties = [
5961
Property(
6062
"composes",
6163
["openminds.ephys.Electrode", "openminds.ephys.ElectrodeArray", "openminds.ephys.Pipette"],

fairgraph/openminds/chemicals/chemical_substance.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ class ChemicalSubstance(KGObject):
4949
"vocab:purity",
5050
doc="no description available",
5151
),
52+
]
53+
reverse_properties = [
5254
Property(
5355
"composes",
5456
["openminds.ephys.Electrode", "openminds.ephys.ElectrodeArray", "openminds.ephys.Pipette"],

fairgraph/openminds/chemicals/product_source.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ class ProductSource(KGObject):
4444
"vocab:purity",
4545
doc="no description available",
4646
),
47+
]
48+
reverse_properties = [
4749
Property(
4850
"is_source_of",
4951
["openminds.chemicals.ChemicalMixture", "openminds.chemicals.ChemicalSubstance"],

fairgraph/openminds/computation/data_analysis.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@ class DataAnalysis(KGObject):
167167
"vocab:wasInformedBy",
168168
doc="no description available",
169169
),
170+
]
171+
reverse_properties = [
170172
Property(
171173
"informed",
172174
[

fairgraph/openminds/computation/data_copy.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ class DataCopy(KGObject):
163163
"vocab:wasInformedBy",
164164
doc="no description available",
165165
),
166+
]
167+
reverse_properties = [
166168
Property(
167169
"informed",
168170
[

fairgraph/openminds/computation/environment.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ class Environment(KGObject):
5353
multiple=True,
5454
doc="no description available",
5555
),
56+
]
57+
reverse_properties = [
5658
Property(
5759
"used_for",
5860
[

0 commit comments

Comments
 (0)