From 9a064c223d24864239a7154d7f4b4b1252968cff Mon Sep 17 00:00:00 2001 From: Tom Kingstone Date: Thu, 21 May 2026 15:27:17 +0100 Subject: [PATCH 01/15] start with a simple copy of BH.oM.BHoMObject with some simple helper properties also start JSON encoder and decoder but unsure of what to do here for decoder. --- .../src/python_toolkit/bhom/bhom_object.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py diff --git a/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py b/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py new file mode 100644 index 0000000..62a8af4 --- /dev/null +++ b/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py @@ -0,0 +1,36 @@ +import uuid +from typing import List, Dict +from json import JSONEEncoder, JSONDecoder +from .. import TOOLKIT_NAME + + +class BHoMJSONDecoder(JSONDecoder): + +class BHoMJSONEncoder(JSONEncoder): + def default(self, o): + return o.__dict__ + + +class BHoMObject: + def __init__( + self, + name: str = "", + bhom_guid: uuid.UUID = uuid.uuid4(), + tags: List[str] = [], + fragments: Dict[type, object] = {}, + custom_data: Dict[str, object] = {} + ) -> BHoMObject: + + self.name = name + self.bhom_guid = bhom_guid + self.fragments = fragments + self.tags = tags + self.custom_data = custom_data + + @property + def namespace(self): + return "BH.oM.Python" + + @property + def _t(self): + return namespace + "." + type(self).__name__ \ No newline at end of file From 91200c526e5996cb070937e02b298eae7afc462c Mon Sep 17 00:00:00 2001 From: Tom Kingstone Date: Tue, 26 May 2026 15:40:53 +0100 Subject: [PATCH 02/15] further develop BHoMObject class and helper methods, and add some tests --- .../src/python_toolkit/bhom/bhom_object.py | 104 ++++++++++++++++-- .../Python/tests/test_bhom_serialiser.py | 34 ++++++ 2 files changed, 127 insertions(+), 11 deletions(-) create mode 100644 Python_Engine/Python/tests/test_bhom_serialiser.py diff --git a/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py b/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py index 62a8af4..908fb5b 100644 --- a/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py +++ b/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py @@ -1,24 +1,98 @@ import uuid +import re from typing import List, Dict +import json from json import JSONEEncoder, JSONDecoder from .. import TOOLKIT_NAME +def convert_pascal_to_camel(s: str): + """Converts a string to camel_case.""" + sections = re.split("(?<=.)(?=[A-Z])", s) #zero-length match before capitals, skipping capital at the 0th index + parts = [] + for sec in sections: + parts.append(sec.lower()) + + return parts.join("_") + +def convert_camel_to_pascal(s: str): + """Converts a string to PascalCase, ignoring _ if it is the first character.""" + sections = re.split(r'(?<=.)_', s) #match all `_` except if it is the first character. + + parts = [] + for sec in sections: #capitalise each section unless the section is empty (in which case, append an underscore) or the section starts with an underscore (first section can begin with _) + if sec == "": + parts.append("_") + continue + elif sec.startswith("_"): + parts.append(sec) + + parts.append(sec.capitalize()) + + return ''.join(parts) class BHoMJSONDecoder(JSONDecoder): - + def __init__(self, *args, **kwargs): + super().__init__(self, object_hook=self.object_hook, *args, **kwargs) + + def object_hook(self, d): + if "_t" not in d: + return d + + #get default BHoMObject properties and replace with defaults if not present + name = d.pop("Name", "") + bhom_guid = d.pop("BHoM_Guid", uuid.uuid4()) + tags = d.pop("Tags", []) + fragments = d.pop("Fragments", {}) + custom_data = d.pop("CustomData", {}) + _t = d.pop("_t") + + #convert all properties to camel_case as python users expect + props = {} + for prop_name in d: + props[convert_pascal_to_camel(prop_name)] = d[prop_name] + + return BHoMObject(name, bhom_guid, tags, fragments, custom_data, _t, **props) + class BHoMJSONEncoder(JSONEncoder): def default(self, o): - return o.__dict__ - + if isinstance(o, BHoMObject): + #initialise special BHoMObject properties + props = { + "Name": o.name, + "BHoM_Guid": o.bhom_guid, + "Tags": o.tags, + "Fragments": o.fragments, + "CustomData": o.custom_data, + "_t": o._t + } + + #get property names with reflection and convert all properties to PascalCase as the BHoM serialiser expects + for prop_name, value in vars(o).items(): + if prop_name in ["name", "bhom_guid", "tags", "fragments", "custom_data", "_t"] + continue + props[convert_camel_to_pascal(prop_name)] = value + return props + + return super(BHoMJSONDecoder, self).default(o) #fallback to default json decoder if object is not a BHoMObject (don't convert property case). + class BHoMObject: + name: str + bhom_guid: uuid.UUID + tags: List[str] + fragments: Dict[type, object] + custom_data: Dict[str, object] + _t: str + def __init__( self, name: str = "", bhom_guid: uuid.UUID = uuid.uuid4(), tags: List[str] = [], fragments: Dict[type, object] = {}, - custom_data: Dict[str, object] = {} + custom_data: Dict[str, object] = {}, + _t: str = "BH.oM.Base.BHoMObject" + **kwargs ) -> BHoMObject: self.name = name @@ -26,11 +100,19 @@ def __init__( self.fragments = fragments self.tags = tags self.custom_data = custom_data + self._t = _t - @property - def namespace(self): - return "BH.oM.Python" - - @property - def _t(self): - return namespace + "." + type(self).__name__ \ No newline at end of file + #set non-CustomData properties with reflection. + for kwarg in kwargs: + setattr(self, kwarg, kwargs[kwarg]) + + @classmethod + def from_json(j: str) -> 'BHoMObject': + obj = json.loads(json, decoder=BHoMJSONDecoder) + if not isinstance(obj, BHoMObject): + raise TypeError("The object provided does not deserialise to a valid BHoM object.") + return obj + + def to_json(self) -> str: + s = json.dumps(self, encoder=BHoMJSONEncoder) + return s \ No newline at end of file diff --git a/Python_Engine/Python/tests/test_bhom_serialiser.py b/Python_Engine/Python/tests/test_bhom_serialiser.py new file mode 100644 index 0000000..92caecc --- /dev/null +++ b/Python_Engine/Python/tests/test_bhom_serialiser.py @@ -0,0 +1,34 @@ + +SERIALISED_BHOM_OBJECT = '{}' + +from python_toolkit.bhom.bhom_object import convert_pascal_to_camel, convert_camel_to_pascal, BHoMObject, BHoMJSONDecoder, BHoMJSONEncoder + +def test_case_convert(): + """Test that the camel and pascal converters are working correctly by using expected outputs and a round trip both ways.""" + + #arrange + test_pascal_str = "ThisIsAPascalCaseString" + test_camel_str = "this_is_a_camel_case_string" + expected_pascal_out = "this_is_a_pascal_case_string" + expected_camel_out = "ThisIsACamelCaseString" + + #act + pascal_out = convert_pascal_to_camel(test_pascal_str) + pascal_round_trip = convert_camel_to_pascal(pascal_out) + + camel_out = convert_camel_to_pascal(test_camel_str) + camel_round_trip = convert_pascal_to_camel(camel_out) + + #assert + assert pascal_out == expected_pascal_out, f"pascal conversion got '{pascal_out}' but expected '{expected_pascal_out}'." + assert camel_out == expected_camel_out, f"camel conversion got '{camel_out}' but expected '{expected_camel_out}'." + assert pascal_round_trip == test_pascal_str, f"pascal round trip got '{pascal_round_trip}' but expected '{test_pascal_str}'." + assert camel_round_trip == test_camel_str, f"camel round trip got '{camel_round_trip}' but expected '{test_camel_str}'." + +def test_serialise_bhom_object() + """Test that bhom objects serialise correctly to a format that the c# bhom serialiser accepts as valid, and with the correct property case.""" + assert False #fail for now as test is not done + +def test_deserialise_bhom_object() + """Test that bhom objects deserialise correctly with no errors with expected properties with correct case.""" + assert False #fail for now as test is not done \ No newline at end of file From 952998afec645f608a76765d7fb6a99f07e80018 Mon Sep 17 00:00:00 2001 From: Tom Kingstone Date: Tue, 26 May 2026 16:26:43 +0100 Subject: [PATCH 03/15] refine tests a little and add debug and slightly improve an error message --- .../src/python_toolkit/bhom/bhom_object.py | 7 ++++-- .../Python/tests/test_bhom_serialiser.py | 23 +++++++++++++++---- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py b/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py index 908fb5b..648e344 100644 --- a/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py +++ b/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py @@ -3,7 +3,7 @@ from typing import List, Dict import json from json import JSONEEncoder, JSONDecoder -from .. import TOOLKIT_NAME +from .logging import CONSOLE_LOGGER def convert_pascal_to_camel(s: str): """Converts a string to camel_case.""" @@ -36,6 +36,7 @@ def __init__(self, *args, **kwargs): def object_hook(self, d): if "_t" not in d: + CONSOLE_LOGGER.debug(f"BHoMJSONDecoder could not convert the following dictionary into a BHoMObject due to a missing '_t' property. Falling back to dictionary: {d}") return d #get default BHoMObject properties and replace with defaults if not present @@ -109,8 +110,10 @@ def __init__( @classmethod def from_json(j: str) -> 'BHoMObject': obj = json.loads(json, decoder=BHoMJSONDecoder) - if not isinstance(obj, BHoMObject): + + if not isinstance(obj, BHoMObject): #this only tests that the top level object was deserialised correctly, if there are problems with deep properties, change the CONSOLE_LOGGER log level to debug. raise TypeError("The object provided does not deserialise to a valid BHoM object.") + return obj def to_json(self) -> str: diff --git a/Python_Engine/Python/tests/test_bhom_serialiser.py b/Python_Engine/Python/tests/test_bhom_serialiser.py index 92caecc..92e6b62 100644 --- a/Python_Engine/Python/tests/test_bhom_serialiser.py +++ b/Python_Engine/Python/tests/test_bhom_serialiser.py @@ -1,7 +1,8 @@ - -SERIALISED_BHOM_OBJECT = '{}' - from python_toolkit.bhom.bhom_object import convert_pascal_to_camel, convert_camel_to_pascal, BHoMObject, BHoMJSONDecoder, BHoMJSONEncoder +import json + +SERIALISED_BHOM_OBJECT = '{"_t": "BH.oM.Base.BHoMObject", "Name": "test_object"}' #TODO: use the BHoM serialiser to make a BHoMObject json string, and use that here. +DESERIALISED_BHOM_OBJECT = BHoMObject(name = "test_object") #TODO: make equivalent BHoMObject here identical to the one above. def test_case_convert(): """Test that the camel and pascal converters are working correctly by using expected outputs and a round trip both ways.""" @@ -27,8 +28,20 @@ def test_case_convert(): def test_serialise_bhom_object() """Test that bhom objects serialise correctly to a format that the c# bhom serialiser accepts as valid, and with the correct property case.""" - assert False #fail for now as test is not done + + serialised = json.dumps(DESERIALISED_BHOM_OBJECT, encoder=BHoMJSONEncoder) + serialised_bhom_object_to_json = DESERIALISED_BHOM_OBJECT.to_json() + round_trip = BHoMObject.from_json(serialised) + + assert serialised == serialised_bhom_object_to_json, f"Direct serialisation to json differed to BHoMObject to_json method." + assert round_trip == DESERIALISED_BHOM_OBJECT, f"BHoMObject round trip failed for serialisation -> deserialisation." #this is the only direction the round trip can be tested without directly inspecting each dictionary entry, as it is not guaranteed that the other direction will produce identical order for json strings. def test_deserialise_bhom_object() """Test that bhom objects deserialise correctly with no errors with expected properties with correct case.""" - assert False #fail for now as test is not done \ No newline at end of file + + obj = json.loads(SERIALISED_BHOM_OBJECT, decoder=BHoMJSONDecoder) + obj_bhom_object_from_json = BHoMObject.from_json(SERIALISED_BHOM_OBJECT) + + assert isinstance(obj, BHoMObject), "JSON decoded an object of the wrong type" + assert obj == DESERIALISED_BHOM_OBJECT, f"Actual deserialised object ({obj}) was not identical to expected deserialised object ({DESERIALISED_BHOM_OBJECT})." + assert obj_bhom_object_from_json == obj, f"Direct deserialisation from json differed to BHoMObject from_json method." \ No newline at end of file From 0a1099502ef1e978a6a64abf254c89549dffff9f Mon Sep 17 00:00:00 2001 From: Tom Kingstone Date: Thu, 28 May 2026 11:13:02 +0100 Subject: [PATCH 04/15] added extra class to handle non bhom objects serialised by the bhom serialiser, and improved from_json class method to use sub class initialiser to return the correct type when BHoMObject is sub classed. --- .../src/python_toolkit/bhom/bhom_object.py | 307 ++++++++++++------ .../Python/tests/test_bhom_serialiser.py | 37 ++- 2 files changed, 239 insertions(+), 105 deletions(-) diff --git a/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py b/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py index 648e344..0dc10b4 100644 --- a/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py +++ b/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py @@ -2,17 +2,17 @@ import re from typing import List, Dict import json -from json import JSONEEncoder, JSONDecoder +from json import JSONEncoder, JSONDecoder from .logging import CONSOLE_LOGGER def convert_pascal_to_camel(s: str): - """Converts a string to camel_case.""" - sections = re.split("(?<=.)(?=[A-Z])", s) #zero-length match before capitals, skipping capital at the 0th index - parts = [] - for sec in sections: - parts.append(sec.lower()) + """Converts a string to camel_case.""" + sections = re.split("(?<=.)(?=[A-Z])", s) #zero-length match before capitals, skipping capital at the 0th index + parts = [] + for sec in sections: + parts.append(sec.lower()) - return parts.join("_") + return "_".join(parts) def convert_camel_to_pascal(s: str): """Converts a string to PascalCase, ignoring _ if it is the first character.""" @@ -20,102 +20,213 @@ def convert_camel_to_pascal(s: str): parts = [] for sec in sections: #capitalise each section unless the section is empty (in which case, append an underscore) or the section starts with an underscore (first section can begin with _) - if sec == "": - parts.append("_") - continue - elif sec.startswith("_"): - parts.append(sec) + if sec == "": + parts.append("_") + continue + elif sec.startswith("_"): + parts.append(sec) parts.append(sec.capitalize()) return ''.join(parts) class BHoMJSONDecoder(JSONDecoder): - def __init__(self, *args, **kwargs): - super().__init__(self, object_hook=self.object_hook, *args, **kwargs) - - def object_hook(self, d): - if "_t" not in d: - CONSOLE_LOGGER.debug(f"BHoMJSONDecoder could not convert the following dictionary into a BHoMObject due to a missing '_t' property. Falling back to dictionary: {d}") - return d - - #get default BHoMObject properties and replace with defaults if not present - name = d.pop("Name", "") - bhom_guid = d.pop("BHoM_Guid", uuid.uuid4()) - tags = d.pop("Tags", []) - fragments = d.pop("Fragments", {}) - custom_data = d.pop("CustomData", {}) - _t = d.pop("_t") - - #convert all properties to camel_case as python users expect - props = {} - for prop_name in d: - props[convert_pascal_to_camel(prop_name)] = d[prop_name] - - return BHoMObject(name, bhom_guid, tags, fragments, custom_data, _t, **props) + def __init__(self, *args, **kwargs): + json.JSONDecoder.__init__(self, object_hook=self.object_hook, *args, **kwargs) + + def object_hook(self, d): + if "_t" not in d: + CONSOLE_LOGGER.debug(f"BHoMJSONDecoder could not convert the following dictionary into a BHoMObject due to a missing '_t' property. Falling back to dictionary: {d}") + return d + + props = { + "_t": d.pop("_t"), + "_bhom_version": d.pop("_bhomVersion", None) + } + + if d.get("BHoM_Guid", None) is not None: + #deserialise as BHoM Object + + #get default BHoMObject properties and replace with defaults if not present + props["name"] = d.pop("Name", "") + props["bhom_guid"] = uuid.UUID(d.pop("BHoM_Guid")) + props["tags"] = d.pop("Tags", []) + props["fragments"] = d.pop("Fragments", []) + props["custom_data"] = d.pop("CustomData", {}) + + #convert all other properties to camel_case as python users expect + for prop_name in d: + props[convert_pascal_to_camel(prop_name)] = d[prop_name] + + return BHoMObject(**props) + else: + #deserialise as IObject + for prop_name in d: + props[convert_pascal_to_camel(prop_name)] = d[prop_name] + + return IObject(**props) class BHoMJSONEncoder(JSONEncoder): - def default(self, o): - if isinstance(o, BHoMObject): - #initialise special BHoMObject properties - props = { - "Name": o.name, - "BHoM_Guid": o.bhom_guid, - "Tags": o.tags, - "Fragments": o.fragments, - "CustomData": o.custom_data, - "_t": o._t - } - - #get property names with reflection and convert all properties to PascalCase as the BHoM serialiser expects - for prop_name, value in vars(o).items(): - if prop_name in ["name", "bhom_guid", "tags", "fragments", "custom_data", "_t"] - continue - props[convert_camel_to_pascal(prop_name)] = value - - return props - - return super(BHoMJSONDecoder, self).default(o) #fallback to default json decoder if object is not a BHoMObject (don't convert property case). - -class BHoMObject: - name: str - bhom_guid: uuid.UUID - tags: List[str] - fragments: Dict[type, object] - custom_data: Dict[str, object] - _t: str - - def __init__( - self, - name: str = "", - bhom_guid: uuid.UUID = uuid.uuid4(), - tags: List[str] = [], - fragments: Dict[type, object] = {}, - custom_data: Dict[str, object] = {}, - _t: str = "BH.oM.Base.BHoMObject" - **kwargs - ) -> BHoMObject: - - self.name = name - self.bhom_guid = bhom_guid - self.fragments = fragments - self.tags = tags - self.custom_data = custom_data - self._t = _t - - #set non-CustomData properties with reflection. - for kwarg in kwargs: - setattr(self, kwarg, kwargs[kwarg]) - - @classmethod - def from_json(j: str) -> 'BHoMObject': - obj = json.loads(json, decoder=BHoMJSONDecoder) - - if not isinstance(obj, BHoMObject): #this only tests that the top level object was deserialised correctly, if there are problems with deep properties, change the CONSOLE_LOGGER log level to debug. - raise TypeError("The object provided does not deserialise to a valid BHoM object.") - - return obj - - def to_json(self) -> str: - s = json.dumps(self, encoder=BHoMJSONEncoder) - return s \ No newline at end of file + def default(self, o): + if isinstance(o, BHoMObject): + #initialise special BHoMObject properties + + props = { + "Name": o.name, + "BHoM_Guid": str(o.bhom_guid), + "_t": o._t + } + + if len(o.tags) > 0: + props["Tags"] = o.tags + + if len(o.fragments) > 0: + props["Fragments"] = o.fragments + + if len(o.custom_data) > 0: + props["CustomData"] = o.custom_data + + if o._bhom_version is not None: + props["_bhomVersion"] = o._bhom_version + + #get property names with reflection and convert all properties to PascalCase as the BHoM serialiser expects + for prop_name, value in vars(o).items(): + if prop_name in ["name", "bhom_guid", "tags", "fragments", "custom_data", "_t", "_bhom_version"]: + continue + + props[convert_camel_to_pascal(prop_name)] = value + + return props + elif isinstance(o, IObject): + props = { + "_t": o._t + } + + if o._bhom_version is not None: + props["_bhomVersion"] = o._bhom_version + + for prop_name, value in vars(o).items(): + if prop_name in ["_t", "_bhom_version"]: + continue + + props[convert_camel_to_pascal(prop_name)] = value + + return props + elif isinstance(o, uuid.UUID): #UUID object is not json serialisable by default + return str(o) + + return super(type(self), self).default(o) #fallback to default json decoder if object is not a BHoMObject (don't convert property case). + +class IObject: + """More generic version of BHoMObject, for non-native objects serialised by the BHoM serialiser, but do not inherit from BHoMObject.""" + _t: str + _bhom_version: str + + def __init__( + self, + _t: str, + _bhom_version: str = None, + **kwargs + ) -> 'IObject': + self._t = _t + self._bhom_version = _bhom_version + + #set properties with reflection. + for kwarg in kwargs: + setattr(self, kwarg, kwargs[kwarg]) + + def __repr__(self) -> str: + return f"{type(self).__name__} of type {self._t}, version: '{getattr(self, "_bhom_version", "Unknown")}'" + + def __eq__(self, other) -> bool: + if not isinstance(other, IObject): + return False + + if self._t != other._t: + return False + + vself = vars(self).copy() + vother = vars(other).copy() + + #ignore these properties when comparing by property. + ignore = ["_bhom_version"] + _ = [(vself.pop(p, None), vother.pop(p, None)) for p in ignore] + + return vself == vother + + @classmethod + def from_json(cls, j: str) -> 'IObject': + obj = json.loads(j, cls=BHoMJSONDecoder) + + if not isinstance(obj, cls): #this only tests that the top level object was deserialised correctly, if there are problems with deep properties, change the CONSOLE_LOGGER log level to debug. + raise TypeError("The object provided does not deserialise to a valid BHoM object.") + + return obj + + def to_json(self) -> str: + return json.dumps(self, cls=BHoMJSONEncoder) + +class BHoMObject(IObject): + name: str + bhom_guid: uuid.UUID + tags: List[str] + fragments: List[Dict[str, object]] + custom_data: Dict[str, object] + + def __init__( + self, + _t: str, #don't make default, as subclasses of this class should set this with super().__init__ + name: str = "", + bhom_guid: uuid.UUID = uuid.uuid4(), + tags: List[str] = [], + fragments: List[Dict[str, object]] = [], + custom_data: Dict[str, object] = {}, + _bhom_version: str = None, + **kwargs + ) -> 'BHoMObject': + self._t = _t + self.name = name + self.bhom_guid = bhom_guid + self.fragments = fragments + self.tags = tags + self.custom_data = custom_data + self._bhom_version = _bhom_version + + #set non-CustomData properties with reflection. + for kwarg in kwargs: + setattr(self, kwarg, kwargs[kwarg]) + + def __repr__(self) -> str: + return f"{type(self).__name__} of type {self._t}, name: '{self.name}', version: '{getattr(self, "_bhom_version", "Unknown")}', id: '{self.bhom_guid}'" + + def __eq__(self, other) -> bool: + if not isinstance(other, BHoMObject): + return False + + if self._t != other._t: + return False + + vself = vars(self).copy() + vother = vars(other).copy() + + #ignore these properties when comparing by property. + ignore = ["bhom_guid", "_bhom_version"] + _ = [(vself.pop(p, None), vother.pop(p, None)) for p in ignore] + + return vself == vother + + @classmethod + def from_json(cls, j: str): + obj = json.loads(j, cls=BHoMJSONDecoder) + + if issubclass(cls, BHoMObject) and cls != BHoMObject: + obj = cls._from_bhom_object(obj) + + if not isinstance(obj, cls): #this only tests that the top level object was deserialised correctly, if there are problems with deep properties, change the CONSOLE_LOGGER log level to debug. + raise TypeError("The object provided does not deserialise to a valid BHoM object.") + + return obj + + @classmethod + def _from_bhom_object(cls, o: 'BHoMObject'): + return cls(**vars(o).copy()) #assuming that the sub class is correctly set up, then this should work \ No newline at end of file diff --git a/Python_Engine/Python/tests/test_bhom_serialiser.py b/Python_Engine/Python/tests/test_bhom_serialiser.py index 92e6b62..15f69f8 100644 --- a/Python_Engine/Python/tests/test_bhom_serialiser.py +++ b/Python_Engine/Python/tests/test_bhom_serialiser.py @@ -1,11 +1,13 @@ from python_toolkit.bhom.bhom_object import convert_pascal_to_camel, convert_camel_to_pascal, BHoMObject, BHoMJSONDecoder, BHoMJSONEncoder +import uuid import json -SERIALISED_BHOM_OBJECT = '{"_t": "BH.oM.Base.BHoMObject", "Name": "test_object"}' #TODO: use the BHoM serialiser to make a BHoMObject json string, and use that here. -DESERIALISED_BHOM_OBJECT = BHoMObject(name = "test_object") #TODO: make equivalent BHoMObject here identical to the one above. +SERIALISED_BHOM_OBJECT = '{"_t": "BH.oM.Base.BHoMObject", "Name": "test_object", "BHoM_Guid": "91ec5ba6-88cd-4b3c-b12a-3d88f0252cde"}' #TODO: use the BHoM serialiser to make a BHoMObject json string, and use that here. +DESERIALISED_BHOM_OBJECT = BHoMObject(_t = "BH.oM.Base.BHoMObject", bhom_guid=uuid.UUID("91ec5ba6-88cd-4b3c-b12a-3d88f0252cde"), name = "test_object") #TODO: make equivalent BHoMObject here identical to the one above. def test_case_convert(): """Test that the camel and pascal converters are working correctly by using expected outputs and a round trip both ways.""" + #TODO: find edge cases within bhom to see if round trip converters work properly. #arrange test_pascal_str = "ThisIsAPascalCaseString" @@ -26,22 +28,43 @@ def test_case_convert(): assert pascal_round_trip == test_pascal_str, f"pascal round trip got '{pascal_round_trip}' but expected '{test_pascal_str}'." assert camel_round_trip == test_camel_str, f"camel round trip got '{camel_round_trip}' but expected '{test_camel_str}'." -def test_serialise_bhom_object() +def test_serialise_bhom_object(): """Test that bhom objects serialise correctly to a format that the c# bhom serialiser accepts as valid, and with the correct property case.""" - serialised = json.dumps(DESERIALISED_BHOM_OBJECT, encoder=BHoMJSONEncoder) + serialised = json.dumps(DESERIALISED_BHOM_OBJECT, cls=BHoMJSONEncoder) serialised_bhom_object_to_json = DESERIALISED_BHOM_OBJECT.to_json() round_trip = BHoMObject.from_json(serialised) assert serialised == serialised_bhom_object_to_json, f"Direct serialisation to json differed to BHoMObject to_json method." assert round_trip == DESERIALISED_BHOM_OBJECT, f"BHoMObject round trip failed for serialisation -> deserialisation." #this is the only direction the round trip can be tested without directly inspecting each dictionary entry, as it is not guaranteed that the other direction will produce identical order for json strings. -def test_deserialise_bhom_object() +def test_deserialise_bhom_object(): """Test that bhom objects deserialise correctly with no errors with expected properties with correct case.""" - obj = json.loads(SERIALISED_BHOM_OBJECT, decoder=BHoMJSONDecoder) + obj = json.loads(SERIALISED_BHOM_OBJECT, cls=BHoMJSONDecoder) obj_bhom_object_from_json = BHoMObject.from_json(SERIALISED_BHOM_OBJECT) assert isinstance(obj, BHoMObject), "JSON decoded an object of the wrong type" assert obj == DESERIALISED_BHOM_OBJECT, f"Actual deserialised object ({obj}) was not identical to expected deserialised object ({DESERIALISED_BHOM_OBJECT})." - assert obj_bhom_object_from_json == obj, f"Direct deserialisation from json differed to BHoMObject from_json method." \ No newline at end of file + assert obj_bhom_object_from_json == obj, f"Direct deserialisation from json differed to BHoMObject from_json method." + +def test_subclass(): + + class TestSubClass(BHoMObject): + _t: str = "BH.oM.Base.CustomObject" + some_other_data: str + + def __init__(self, some_other_data, **kwargs): + self.some_other_data = some_other_data + _t = kwargs.pop("_t", self._t) + super().__init__(_t, **kwargs) + + test_object = TestSubClass("this is some test data") + + test_object_json = test_object.to_json() + + test_object_round_trip = TestSubClass.from_json(test_object_json) + + assert test_object == test_object_round_trip + assert test_object.some_other_data == "this is some test data" + assert test_object._t == "BH.oM.Base.CustomObject" \ No newline at end of file From c9672ac3dba33345180905536f30014baaf703d2 Mon Sep 17 00:00:00 2001 From: Tom Kingstone Date: Thu, 28 May 2026 11:28:41 +0100 Subject: [PATCH 05/15] added warning if mismatched bhom versions --- Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py b/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py index 0dc10b4..66910bc 100644 --- a/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py +++ b/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py @@ -4,6 +4,7 @@ import json from json import JSONEncoder, JSONDecoder from .logging import CONSOLE_LOGGER +from . import BHOM_VERSION def convert_pascal_to_camel(s: str): """Converts a string to camel_case.""" @@ -44,6 +45,9 @@ def object_hook(self, d): "_bhom_version": d.pop("_bhomVersion", None) } + if (props["_bhom_version"] is not None) and (props["_bhom_version"] != ".".join(BHOM_VERSION.split(".")[0:1])) + CONSOLE_LOGGER.warning(f"The bhom version specified in the encoded json ({props["_bhom_version"]}) is different from the BHoM version that python_toolkit was installed with ({BHOM_VERSION}). There may be versioning issues with this object. Consider deserialising and then serialising again with the BHoM serialiser to get the correct version, or update BHoM to the correct version.") + if d.get("BHoM_Guid", None) is not None: #deserialise as BHoM Object From e648cdc011ff0662bbf2f51a786f9437a4502e74 Mon Sep 17 00:00:00 2001 From: Tom Kingstone Date: Thu, 28 May 2026 11:34:40 +0100 Subject: [PATCH 06/15] I hate not having an interpreter available --- Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py b/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py index 66910bc..4d62301 100644 --- a/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py +++ b/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py @@ -45,7 +45,7 @@ def object_hook(self, d): "_bhom_version": d.pop("_bhomVersion", None) } - if (props["_bhom_version"] is not None) and (props["_bhom_version"] != ".".join(BHOM_VERSION.split(".")[0:1])) + if (props["_bhom_version"] is not None) and (props["_bhom_version"] != ".".join(BHOM_VERSION.split(".")[0:1])): CONSOLE_LOGGER.warning(f"The bhom version specified in the encoded json ({props["_bhom_version"]}) is different from the BHoM version that python_toolkit was installed with ({BHOM_VERSION}). There may be versioning issues with this object. Consider deserialising and then serialising again with the BHoM serialiser to get the correct version, or update BHoM to the correct version.") if d.get("BHoM_Guid", None) is not None: From 6a681b36ab63a4b727da00935327a4fdd981e084 Mon Sep 17 00:00:00 2001 From: Tom Kingstone Date: Thu, 28 May 2026 11:39:30 +0100 Subject: [PATCH 07/15] use two version numbers instead of one :) --- Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py b/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py index 4d62301..35bfbc2 100644 --- a/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py +++ b/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py @@ -45,7 +45,7 @@ def object_hook(self, d): "_bhom_version": d.pop("_bhomVersion", None) } - if (props["_bhom_version"] is not None) and (props["_bhom_version"] != ".".join(BHOM_VERSION.split(".")[0:1])): + if (props["_bhom_version"] is not None) and (props["_bhom_version"] != ".".join(BHOM_VERSION.split(".")[0:2])): CONSOLE_LOGGER.warning(f"The bhom version specified in the encoded json ({props["_bhom_version"]}) is different from the BHoM version that python_toolkit was installed with ({BHOM_VERSION}). There may be versioning issues with this object. Consider deserialising and then serialising again with the BHoM serialiser to get the correct version, or update BHoM to the correct version.") if d.get("BHoM_Guid", None) is not None: From e1e84899e6ef14c010fb694e75dd81b920dc18ff Mon Sep 17 00:00:00 2001 From: Tom Kingstone Date: Mon, 1 Jun 2026 10:22:20 +0100 Subject: [PATCH 08/15] improve error messages and fix bugs found by new test scripts/methods, and handle json deserialisation of lists (as BHoM serialiser in grasshopper likes to make json arrays) --- .../src/python_toolkit/bhom/bhom_object.py | 40 ++++++++++++++--- .../Python/tests/test_bhom_serialiser.py | 43 +++++++++++++++---- 2 files changed, 68 insertions(+), 15 deletions(-) diff --git a/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py b/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py index 35bfbc2..cf4fbfb 100644 --- a/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py +++ b/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py @@ -5,6 +5,9 @@ from json import JSONEncoder, JSONDecoder from .logging import CONSOLE_LOGGER from . import BHOM_VERSION +import pandas as pd + +BHOM_SHORT_VERSION = ".".join(BHOM_VERSION.split(".")[0:2]) def convert_pascal_to_camel(s: str): """Converts a string to camel_case.""" @@ -42,11 +45,11 @@ def object_hook(self, d): props = { "_t": d.pop("_t"), - "_bhom_version": d.pop("_bhomVersion", None) + "_bhom_version": d.pop("_bhomVersion", BHOM_SHORT_VERSION) } - if (props["_bhom_version"] is not None) and (props["_bhom_version"] != ".".join(BHOM_VERSION.split(".")[0:2])): - CONSOLE_LOGGER.warning(f"The bhom version specified in the encoded json ({props["_bhom_version"]}) is different from the BHoM version that python_toolkit was installed with ({BHOM_VERSION}). There may be versioning issues with this object. Consider deserialising and then serialising again with the BHoM serialiser to get the correct version, or update BHoM to the correct version.") + if (props["_bhom_version"] is not None) and (props["_bhom_version"] != BHOM_SHORT_VERSION): + CONSOLE_LOGGER.warning(f"The bhom version specified in the encoded json ({props['_bhom_version']}) is different from the BHoM version that python_toolkit was installed with ({BHOM_SHORT_VERSION}). There may be versioning issues with this object. Consider deserialising and then serialising again with the BHoM serialiser to get the correct version, or update BHoM to the correct version.") if d.get("BHoM_Guid", None) is not None: #deserialise as BHoM Object @@ -64,6 +67,9 @@ def object_hook(self, d): return BHoMObject(**props) else: + if props["_t"].startswith("System.Collections.Generic.List"): #this handles when the BHoM serialiser makes generic lists from CustomObject list properties, which get converted to a dictionary. The BHoM serialiser does recognise lists if it can find the object definition via reflection. + return d["_v"] + #deserialise as IObject for prop_name in d: props[convert_pascal_to_camel(prop_name)] = d[prop_name] @@ -118,7 +124,9 @@ def default(self, o): return props elif isinstance(o, uuid.UUID): #UUID object is not json serialisable by default return str(o) - + elif isinstance(o, pd.DatetimeIndex): + return [date.isoformat() for date in o] + return super(type(self), self).default(o) #fallback to default json decoder if object is not a BHoMObject (don't convert property case). class IObject: @@ -140,7 +148,7 @@ def __init__( setattr(self, kwarg, kwargs[kwarg]) def __repr__(self) -> str: - return f"{type(self).__name__} of type {self._t}, version: '{getattr(self, "_bhom_version", "Unknown")}'" + return f"{type(self).__name__} of type {self._t}, version: '{getattr(self, '_bhom_version', 'Unknown')}'" def __eq__(self, other) -> bool: if not isinstance(other, IObject): @@ -201,7 +209,7 @@ def __init__( setattr(self, kwarg, kwargs[kwarg]) def __repr__(self) -> str: - return f"{type(self).__name__} of type {self._t}, name: '{self.name}', version: '{getattr(self, "_bhom_version", "Unknown")}', id: '{self.bhom_guid}'" + return f"{type(self).__name__} of type {self._t}, name: '{self.name}', version: '{getattr(self, '_bhom_version', 'Unknown')}', id: '{self.bhom_guid}'" def __eq__(self, other) -> bool: if not isinstance(other, BHoMObject): @@ -223,6 +231,10 @@ def __eq__(self, other) -> bool: def from_json(cls, j: str): obj = json.loads(j, cls=BHoMJSONDecoder) + if isinstance(obj, list): + CONSOLE_LOGGER.warning("The root element of the JSON provided was a list, assuming that the first item is the desired object. If you intended to deserialise a list, please use the `.from_json_array(j)` method instead.") + obj = obj[0] + if issubclass(cls, BHoMObject) and cls != BHoMObject: obj = cls._from_bhom_object(obj) @@ -231,6 +243,22 @@ def from_json(cls, j: str): return obj + @classmethod + def from_json_array(cls, j: str): + objs = json.loads(j, cls=BHoMJSONDecoder) + + if not isinstance(objs, list): + raise TypeError("The root element of the JSON provided was not a JSON array. Perhaps you intended to use `.from_json(j)` instead?") + + out = [] + for obj in objs: + if issubclass(cls, BHoMObject) and cls != BHoMObject: + out.append(cls._from_bhom_object(obj)) + continue + out.append(obj) + + return out + @classmethod def _from_bhom_object(cls, o: 'BHoMObject'): return cls(**vars(o).copy()) #assuming that the sub class is correctly set up, then this should work \ No newline at end of file diff --git a/Python_Engine/Python/tests/test_bhom_serialiser.py b/Python_Engine/Python/tests/test_bhom_serialiser.py index 15f69f8..0d1385f 100644 --- a/Python_Engine/Python/tests/test_bhom_serialiser.py +++ b/Python_Engine/Python/tests/test_bhom_serialiser.py @@ -1,14 +1,16 @@ -from python_toolkit.bhom.bhom_object import convert_pascal_to_camel, convert_camel_to_pascal, BHoMObject, BHoMJSONDecoder, BHoMJSONEncoder +from python_toolkit.bhom.bhom_object import convert_pascal_to_camel, convert_camel_to_pascal, BHoMObject, IObject, BHoMJSONDecoder, BHoMJSONEncoder import uuid import json -SERIALISED_BHOM_OBJECT = '{"_t": "BH.oM.Base.BHoMObject", "Name": "test_object", "BHoM_Guid": "91ec5ba6-88cd-4b3c-b12a-3d88f0252cde"}' #TODO: use the BHoM serialiser to make a BHoMObject json string, and use that here. -DESERIALISED_BHOM_OBJECT = BHoMObject(_t = "BH.oM.Base.BHoMObject", bhom_guid=uuid.UUID("91ec5ba6-88cd-4b3c-b12a-3d88f0252cde"), name = "test_object") #TODO: make equivalent BHoMObject here identical to the one above. +SERIALISED_BHOM_OBJECT = '{ "_t" : "BH.oM.Adapter.FileSettings", "FileName" : "test.txt", "Directory" : "path/to/file", "BHoM_Guid" : "f428e614-bda1-4228-882e-21d0f318a322", "Name" : "", "_bhomVersion" : "9.2" }' #TODO: use the BHoM serialiser to make a BHoMObject json string, and use that here. +DESERIALISED_BHOM_OBJECT = BHoMObject(_t = "BH.oM.Adapter.FileSettings", bhom_guid = uuid.UUID("f428e614-bda1-4228-882e-21d0f318a322"), file_name = "test.txt", directory = "path/to/file", _bhom_version = "9.2") #TODO: make equivalent BHoMObject here identical to the one above. + +SERIALISED_IOBJECT = '{ "_t" : "BH.oM.Geometry.Point", "X" : 0.10000000000000001, "Y" : 0.10000000000000001, "Z" : 0.10000000000000001, "_bhomVersion" : "9.2" }' +DESERIALISED_IOBJECT = IObject(_t = "BH.oM.Geometry.Point", x = 0.10000000000000001, y = 0.10000000000000001, z = 0.10000000000000001, _bhom_version = "9.2") def test_case_convert(): """Test that the camel and pascal converters are working correctly by using expected outputs and a round trip both ways.""" #TODO: find edge cases within bhom to see if round trip converters work properly. - #arrange test_pascal_str = "ThisIsAPascalCaseString" test_camel_str = "this_is_a_camel_case_string" @@ -30,26 +32,49 @@ def test_case_convert(): def test_serialise_bhom_object(): """Test that bhom objects serialise correctly to a format that the c# bhom serialiser accepts as valid, and with the correct property case.""" - + #act serialised = json.dumps(DESERIALISED_BHOM_OBJECT, cls=BHoMJSONEncoder) serialised_bhom_object_to_json = DESERIALISED_BHOM_OBJECT.to_json() round_trip = BHoMObject.from_json(serialised) + #assert assert serialised == serialised_bhom_object_to_json, f"Direct serialisation to json differed to BHoMObject to_json method." assert round_trip == DESERIALISED_BHOM_OBJECT, f"BHoMObject round trip failed for serialisation -> deserialisation." #this is the only direction the round trip can be tested without directly inspecting each dictionary entry, as it is not guaranteed that the other direction will produce identical order for json strings. +def test_serialise_iobject(): + #act + serialised = json.dumps(DESERIALISED_IOBJECT, cls=BHoMJSONEncoder) + serialised_iobject_to_json = DESERIALISED_IOBJECT.to_json() + round_trip = IObject.from_json(serialised) + + #assert + assert serialised == serialised_iobject_to_json, f"Direct serialisation to json differed to IObject to_json method." + assert round_trip == DESERIALISED_IOBJECT, f"IObject round trip failed for serialisation -> deserialisation." #this is the only direction the round trip can be tested without directly inspecting each dictionary entry, as it is not guaranteed that the other direction will produce identical order for json strings. + def test_deserialise_bhom_object(): """Test that bhom objects deserialise correctly with no errors with expected properties with correct case.""" - + #act obj = json.loads(SERIALISED_BHOM_OBJECT, cls=BHoMJSONDecoder) obj_bhom_object_from_json = BHoMObject.from_json(SERIALISED_BHOM_OBJECT) + #assert assert isinstance(obj, BHoMObject), "JSON decoded an object of the wrong type" assert obj == DESERIALISED_BHOM_OBJECT, f"Actual deserialised object ({obj}) was not identical to expected deserialised object ({DESERIALISED_BHOM_OBJECT})." assert obj_bhom_object_from_json == obj, f"Direct deserialisation from json differed to BHoMObject from_json method." +def test_deserialise_iobject(): + """Test that bhom objects deserialise correctly with no errors with expected properties with correct case.""" + #act + obj = json.loads(SERIALISED_IOBJECT, cls=BHoMJSONDecoder) + obj_iobject_from_json = IObject.from_json(SERIALISED_IOBJECT) + + #assert + assert isinstance(obj, IObject), "JSON decoded an object of the wrong type" + assert obj == DESERIALISED_IOBJECT, f"Actual deserialised object ({obj}) was not identical to expected deserialised object ({DESERIALISED_IOBJECT})." + assert obj_iobject_from_json == obj, f"Direct deserialisation from json differed to IObject from_json method." + def test_subclass(): - + #arrange class TestSubClass(BHoMObject): _t: str = "BH.oM.Base.CustomObject" some_other_data: str @@ -59,12 +84,12 @@ def __init__(self, some_other_data, **kwargs): _t = kwargs.pop("_t", self._t) super().__init__(_t, **kwargs) + #act test_object = TestSubClass("this is some test data") - test_object_json = test_object.to_json() - test_object_round_trip = TestSubClass.from_json(test_object_json) + #assert assert test_object == test_object_round_trip assert test_object.some_other_data == "this is some test data" assert test_object._t == "BH.oM.Base.CustomObject" \ No newline at end of file From 78ae584c3ee15ff2a8b1993a7e6faec404c1cf2e Mon Sep 17 00:00:00 2001 From: Tom Kingstone Date: Mon, 1 Jun 2026 16:12:56 +0100 Subject: [PATCH 09/15] add from and to_dict methods to IObject to enable subclasses to use dictionary versions for converting between objects (for instance for the ladybugtools python package --- .../Python/src/python_toolkit/bhom/bhom_object.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py b/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py index cf4fbfb..d691b31 100644 --- a/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py +++ b/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py @@ -6,6 +6,7 @@ from .logging import CONSOLE_LOGGER from . import BHOM_VERSION import pandas as pd +import copy BHOM_SHORT_VERSION = ".".join(BHOM_VERSION.split(".")[0:2]) @@ -178,6 +179,19 @@ def from_json(cls, j: str) -> 'IObject': def to_json(self) -> str: return json.dumps(self, cls=BHoMJSONEncoder) + @classmethod + def from_dict(cls, d: dict) -> 'IObject' + try: + return cls(**d) #should be valid as long as the dictionary has all necessary entries. + except ArgumentError as ae: + raise ArgumentError("Input dictionary was missing some required arguments, see traceback for more information.") from ae + + def to_dict(self) + """Convert this IObject to a dictionary via a json round-trip.""" + j = self.to_json(default=str) + d = json.loads(j) + return d + class BHoMObject(IObject): name: str bhom_guid: uuid.UUID From e09beafc3fd2691f28ce326c63f9a88629d32b6b Mon Sep 17 00:00:00 2001 From: Tom Kingstone Date: Tue, 2 Jun 2026 11:14:11 +0100 Subject: [PATCH 10/15] save point as changing laptops --- .../Python/src/python_toolkit/bhom/bhom_object.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py b/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py index d691b31..a01a7a2 100644 --- a/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py +++ b/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py @@ -127,6 +127,8 @@ def default(self, o): return str(o) elif isinstance(o, pd.DatetimeIndex): return [date.isoformat() for date in o] + elif hasattr(o, "to_json") and callable(getattr(o, "to_json")): #custom handle classes that might have their own json converters + return o.to_json() return super(type(self), self).default(o) #fallback to default json decoder if object is not a BHoMObject (don't convert property case). @@ -180,14 +182,14 @@ def to_json(self) -> str: return json.dumps(self, cls=BHoMJSONEncoder) @classmethod - def from_dict(cls, d: dict) -> 'IObject' + def from_dict(cls, d: dict) -> 'IObject': try: return cls(**d) #should be valid as long as the dictionary has all necessary entries. except ArgumentError as ae: raise ArgumentError("Input dictionary was missing some required arguments, see traceback for more information.") from ae - def to_dict(self) - """Convert this IObject to a dictionary via a json round-trip.""" + def to_dict(self) -> dict: + """Convert this IObject to a dictionary via a json round-trip.""" j = self.to_json(default=str) d = json.loads(j) return d From bd2e7656fa93b1bf0bcfba29cb976ea9aebc9275 Mon Sep 17 00:00:00 2001 From: Tom Kingstone Date: Tue, 2 Jun 2026 15:40:56 +0100 Subject: [PATCH 11/15] add a few default converters to json format for common non-serialisable types and fixed some bugs --- .../src/python_toolkit/bhom/bhom_object.py | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py b/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py index a01a7a2..bb2bdc0 100644 --- a/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py +++ b/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py @@ -1,3 +1,5 @@ +from ctypes import ArgumentError +from pathlib import Path import uuid import re from typing import List, Dict @@ -6,7 +8,7 @@ from .logging import CONSOLE_LOGGER from . import BHOM_VERSION import pandas as pd -import copy +import numpy as np BHOM_SHORT_VERSION = ".".join(BHOM_VERSION.split(".")[0:2]) @@ -101,7 +103,7 @@ def default(self, o): props["_bhomVersion"] = o._bhom_version #get property names with reflection and convert all properties to PascalCase as the BHoM serialiser expects - for prop_name, value in vars(o).items(): + for prop_name, value in vars(o).copy().items(): if prop_name in ["name", "bhom_guid", "tags", "fragments", "custom_data", "_t", "_bhom_version"]: continue @@ -116,21 +118,32 @@ def default(self, o): if o._bhom_version is not None: props["_bhomVersion"] = o._bhom_version - for prop_name, value in vars(o).items(): + for prop_name, value in vars(o).copy().items(): if prop_name in ["_t", "_bhom_version"]: continue props[convert_camel_to_pascal(prop_name)] = value return props + #handle common non-serialisable types elif isinstance(o, uuid.UUID): #UUID object is not json serialisable by default return str(o) elif isinstance(o, pd.DatetimeIndex): return [date.isoformat() for date in o] - elif hasattr(o, "to_json") and callable(getattr(o, "to_json")): #custom handle classes that might have their own json converters - return o.to_json() + elif isinstance(o, np.ndarray): + return o.tolist() + elif isinstance(o, pd.Timestamp): + return o.isoformat() + elif isinstance(o, pd.Series): + return dict(zip(o.index.astype(str), o)) + elif isinstance(o, Path): + return str(o) + elif hasattr(o, "to_dict") and callable(getattr(o, "to_dict")): #custom handle classes that have their own dict converters + return o.to_dict() + elif hasattr(o, "__dict__"): + return vars(o).copy() - return super(type(self), self).default(o) #fallback to default json decoder if object is not a BHoMObject (don't convert property case). + return super(type(self), self).default(o) #fallback to default json decoder (ValueError) if object is not a BHoMObject or common serialisable type. class IObject: """More generic version of BHoMObject, for non-native objects serialised by the BHoM serialiser, but do not inherit from BHoMObject.""" From 393ddbed763b8e88cf30dd309fe54a9f48ef9c28 Mon Sep 17 00:00:00 2001 From: Tom Kingstone Date: Thu, 4 Jun 2026 15:38:05 +0100 Subject: [PATCH 12/15] add extra method called by both decoder and encoder to more easily allow subclassing of the BHoM serialisers, and changed to_dict() mthod to deepcopy --- .../src/python_toolkit/bhom/bhom_object.py | 58 +++++++++++-------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py b/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py index bb2bdc0..61d564d 100644 --- a/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py +++ b/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py @@ -1,8 +1,11 @@ +from base64 import decode +import copy from ctypes import ArgumentError +from json import encoder from pathlib import Path import uuid import re -from typing import List, Dict +from typing import Any, List, Dict, Union import json from json import JSONEncoder, JSONDecoder from .logging import CONSOLE_LOGGER @@ -44,7 +47,7 @@ def __init__(self, *args, **kwargs): def object_hook(self, d): if "_t" not in d: CONSOLE_LOGGER.debug(f"BHoMJSONDecoder could not convert the following dictionary into a BHoMObject due to a missing '_t' property. Falling back to dictionary: {d}") - return d + return self.deserialise_unknown(d) props = { "_t": d.pop("_t"), @@ -68,7 +71,7 @@ def object_hook(self, d): for prop_name in d: props[convert_pascal_to_camel(prop_name)] = d[prop_name] - return BHoMObject(**props) + return self.deserialise_unknown(BHoMObject(**props)) else: if props["_t"].startswith("System.Collections.Generic.List"): #this handles when the BHoM serialiser makes generic lists from CustomObject list properties, which get converted to a dictionary. The BHoM serialiser does recognise lists if it can find the object definition via reflection. return d["_v"] @@ -77,7 +80,12 @@ def object_hook(self, d): for prop_name in d: props[convert_pascal_to_camel(prop_name)] = d[prop_name] - return IObject(**props) + return self.deserialise_unknown(IObject(**props)) + + def deserialise_unknown(self, obj: Union['IObject', 'BHoMObject', dict]): + """Override this method in a subclass to add extra object hook on top of the existing method. If the object is an object serialised by the BHoM serialiser, the output object will be a BHoMObject or IObject, so use isinstance() to select.""" + return obj + class BHoMJSONEncoder(JSONEncoder): def default(self, o): @@ -137,13 +145,19 @@ def default(self, o): elif isinstance(o, pd.Series): return dict(zip(o.index.astype(str), o)) elif isinstance(o, Path): - return str(o) - elif hasattr(o, "to_dict") and callable(getattr(o, "to_dict")): #custom handle classes that have their own dict converters - return o.to_dict() - elif hasattr(o, "__dict__"): - return vars(o).copy() + return { "FileName": str(o.name), "Directory": str(o.parent), "_t": "BH.oM.Adapter.FileSettings", "BHoM_Guid": str(uuid.uuid4()) } + + return self.serialise_unknown(o) #if the object is unknown at this point, call serialise_unknown to allow subclasses to add their own serialisation logic. + + def serialise_unknown(self, obj: Any) -> Union[Dict[str, str], str, List[str], Any]: + """ + This is called by the BHoMJSONEncoder to serialise unknown objects so that the BHoM can handle them if they exist as BHoM Objects in c# but not in python (i.e. an object from an external library). + + Override this method in a sub class with its own logic, and call super().serialise_unknown(obj) for objects that are unknown, or do not wish to serialise. + The following objects won't be passed to this method: BHoMObject, IObject, uuid.UUID, pandas.DatetimeIndex, numpy.ndarray, pandas.Timestamp, pandas.Series, pathlib.Path + """ + return super(type(self), self).default(obj) #fallback to default json decoder (ValueError) if object is not a BHoMObject or common serialisable type. - return super(type(self), self).default(o) #fallback to default json decoder (ValueError) if object is not a BHoMObject or common serialisable type. class IObject: """More generic version of BHoMObject, for non-native objects serialised by the BHoM serialiser, but do not inherit from BHoMObject.""" @@ -183,16 +197,16 @@ def __eq__(self, other) -> bool: return vself == vother @classmethod - def from_json(cls, j: str) -> 'IObject': - obj = json.loads(j, cls=BHoMJSONDecoder) + def from_json(cls, j: str, decoder_class: type = BHoMJSONDecoder) -> 'IObject': + obj = json.loads(j, cls=decoder_class) if not isinstance(obj, cls): #this only tests that the top level object was deserialised correctly, if there are problems with deep properties, change the CONSOLE_LOGGER log level to debug. raise TypeError("The object provided does not deserialise to a valid BHoM object.") return obj - def to_json(self) -> str: - return json.dumps(self, cls=BHoMJSONEncoder) + def to_json(self, encoder_class: type = BHoMJSONEncoder) -> str: + return json.dumps(self, cls=encoder_class) @classmethod def from_dict(cls, d: dict) -> 'IObject': @@ -201,11 +215,9 @@ def from_dict(cls, d: dict) -> 'IObject': except ArgumentError as ae: raise ArgumentError("Input dictionary was missing some required arguments, see traceback for more information.") from ae - def to_dict(self) -> dict: - """Convert this IObject to a dictionary via a json round-trip.""" - j = self.to_json(default=str) - d = json.loads(j) - return d + def to_dict(self, encoder_class: type = BHoMJSONEncoder) -> dict: + """Convert this IObject to a dictionary via deep copying vars(self).""" + return copy.deepcopy(vars(self)) class BHoMObject(IObject): name: str @@ -257,8 +269,8 @@ def __eq__(self, other) -> bool: return vself == vother @classmethod - def from_json(cls, j: str): - obj = json.loads(j, cls=BHoMJSONDecoder) + def from_json(cls, j: str, decoder_class: type = BHoMJSONDecoder): + obj = json.loads(j, cls=decoder_class) if isinstance(obj, list): CONSOLE_LOGGER.warning("The root element of the JSON provided was a list, assuming that the first item is the desired object. If you intended to deserialise a list, please use the `.from_json_array(j)` method instead.") @@ -273,8 +285,8 @@ def from_json(cls, j: str): return obj @classmethod - def from_json_array(cls, j: str): - objs = json.loads(j, cls=BHoMJSONDecoder) + def from_json_array(cls, j: str, decoder_class: type = BHoMJSONDecoder): + objs = json.loads(j, cls=decoder_class) if not isinstance(objs, list): raise TypeError("The root element of the JSON provided was not a JSON array. Perhaps you intended to use `.from_json(j)` instead?") From 1ee42a53e7738b40db6a5ca9f34ed50b400f27ce Mon Sep 17 00:00:00 2001 From: Tom Kingstone Date: Tue, 16 Jun 2026 08:57:09 +0100 Subject: [PATCH 13/15] minor edits --- Python_Engine/Python/src/python_toolkit/bhom/analytics.py | 2 +- Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Python_Engine/Python/src/python_toolkit/bhom/analytics.py b/Python_Engine/Python/src/python_toolkit/bhom/analytics.py index a39b66c..7ba81cb 100644 --- a/Python_Engine/Python/src/python_toolkit/bhom/analytics.py +++ b/Python_Engine/Python/src/python_toolkit/bhom/analytics.py @@ -154,7 +154,7 @@ def decorator(function: Callable): @wraps(function) def wrapper(*args, **kwargs) -> Any: """A wrapper around the function that captures usage analytics.""" - + if disable: CONSOLE_LOGGER.debug("bhom_analytics is curently disabled.") return function(*args, **kwargs) diff --git a/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py b/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py index 61d564d..c09f76d 100644 --- a/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py +++ b/Python_Engine/Python/src/python_toolkit/bhom/bhom_object.py @@ -86,7 +86,6 @@ def deserialise_unknown(self, obj: Union['IObject', 'BHoMObject', dict]): """Override this method in a subclass to add extra object hook on top of the existing method. If the object is an object serialised by the BHoM serialiser, the output object will be a BHoMObject or IObject, so use isinstance() to select.""" return obj - class BHoMJSONEncoder(JSONEncoder): def default(self, o): if isinstance(o, BHoMObject): @@ -158,7 +157,6 @@ def serialise_unknown(self, obj: Any) -> Union[Dict[str, str], str, List[str], A """ return super(type(self), self).default(obj) #fallback to default json decoder (ValueError) if object is not a BHoMObject or common serialisable type. - class IObject: """More generic version of BHoMObject, for non-native objects serialised by the BHoM serialiser, but do not inherit from BHoMObject.""" _t: str From cf6985ded277fb90021a0935efc99e5718e7c897 Mon Sep 17 00:00:00 2001 From: Tom Kingstone Date: Thu, 2 Jul 2026 16:03:55 +0100 Subject: [PATCH 14/15] Add decorator for bhom callable methods, unfinished --- .../bhom/decorators/__init__.py | 1 + .../decorators/bhom_callable_decorator.py | 65 +++++++++++++++++++ Python_Engine/Python_Engine.csproj | 2 +- 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 Python_Engine/Python/src/python_toolkit/bhom/decorators/__init__.py create mode 100644 Python_Engine/Python/src/python_toolkit/bhom/decorators/bhom_callable_decorator.py diff --git a/Python_Engine/Python/src/python_toolkit/bhom/decorators/__init__.py b/Python_Engine/Python/src/python_toolkit/bhom/decorators/__init__.py new file mode 100644 index 0000000..cbb34ff --- /dev/null +++ b/Python_Engine/Python/src/python_toolkit/bhom/decorators/__init__.py @@ -0,0 +1 @@ +from .bhom_callable_decorator import bhom_callable \ No newline at end of file diff --git a/Python_Engine/Python/src/python_toolkit/bhom/decorators/bhom_callable_decorator.py b/Python_Engine/Python/src/python_toolkit/bhom/decorators/bhom_callable_decorator.py new file mode 100644 index 0000000..7e16707 --- /dev/null +++ b/Python_Engine/Python/src/python_toolkit/bhom/decorators/bhom_callable_decorator.py @@ -0,0 +1,65 @@ +import json +from typing import Any, Callable, Union, Dict +from functools import wraps +from ..bhom_object import CONSOLE_LOGGER, BHoMJSONDecoder, BHoMJSONEncoder, BHoMObject + +def bhom_callable(argument_types:Dict[str, type] = {}, encoder_cls: type = BHoMJSONEncoder, decoder_cls: type = BHoMJSONDecoder): + """Decorator for functions to be made callable from BHoM C# methods/adapters. + + Note: methods that this wraps must not have "__input_json__" as a default kwarg, as this is used internally to allow BHoM adapters to call the method. + + when __input_json__ is set, this will cause the method to always output BHoM style json (using the encoder_cls provided to this decorator) + + Args: + argument_types (dict[str, type]): this is a dictionary that is used to map the argument names to types (specifically BHoMObject types) to subclasses of BHoMObjects. + For example, if you have a class that is a subclass of BHoMObject, the default serialiser will only deserialise json to a BHoMObject. + To go the extra step to get your class, you must provide the type in this dictionary to allow the wrapper to convert the BHoMObject type to your desired type. + + encoder_cls (JSONEncoder): A JSONEncoder (ideally one that is a subclass of BHoMJSONEncoder). Mainly this is for if a custom encoder has been implemented for a specific toolkit. + + decoder_cls (JSONDecoder): same as encoder_cls but for JSONDecoder. + """ + def decorator(function: Callable): + + @wraps(function) + def wrapper(*args, **kwargs) -> Union[str, Any]: + + do_wrap:bool = False + + if "__input_json__" in kwargs and len(args) == 0: + do_wrap = True + + #get dictionary from input as file path or json like string. + input_json = kwargs.pop("__input_json__") + + if not input_json.startswith("{"): #assume it's a path + with open(input_json, "r") as f: + input_json = f.read() + + try: + json_kwargs: dict = json.loads(input_json, cls=decoder_cls) + except: + CONSOLE_LOGGER.error("Could not load JSON from file or string due to invalid JSON. Attempting to run with given args and kwargs.", exc_info=1) + + #update kwargs with json + for kwarg_name in json_kwargs: + val = json_kwargs[kwarg_name] + + if kwarg_name in argument_types: + t = argument_types[kwarg_name] + + if issubclass(t, BHoMObject) and type(json_kwargs[kwarg_name]) is BHoMObject: + val = t._from_bhom_object(json_kwargs[kwarg_name]) + + kwargs[kwarg_name] = val + + rtn = function(*args, **kwargs) + + if do_wrap: + json_rtn = json.dumps(rtn, cls=encoder_cls) + + return json_rtn + + return rtn + return wrapper + return decorator \ No newline at end of file diff --git a/Python_Engine/Python_Engine.csproj b/Python_Engine/Python_Engine.csproj index 429318f..d10626a 100644 --- a/Python_Engine/Python_Engine.csproj +++ b/Python_Engine/Python_Engine.csproj @@ -39,6 +39,6 @@ - + From b10621f3b39e4235bf3912160684191fd07aa023 Mon Sep 17 00:00:00 2001 From: Tom Kingstone Date: Fri, 31 Jul 2026 13:26:00 +0100 Subject: [PATCH 15/15] change decorator to also record which methods have been decorated (which are only accessible if the file contained is imported) --- .../bhom/decorators/__init__.py | 2 +- .../decorators/_bhom_callable_decorator.py | 83 +++++++++++++++++++ .../decorators/bhom_callable_decorator.py | 65 --------------- .../src/python_toolkit/bhom/run_wrapped.py | 7 ++ .../python_toolkit/bhom/wrapped/__init__.py | 13 +++ .../bhom/wrapped/_test_wrapped.py | 10 +++ 6 files changed, 114 insertions(+), 66 deletions(-) create mode 100644 Python_Engine/Python/src/python_toolkit/bhom/decorators/_bhom_callable_decorator.py delete mode 100644 Python_Engine/Python/src/python_toolkit/bhom/decorators/bhom_callable_decorator.py create mode 100644 Python_Engine/Python/src/python_toolkit/bhom/run_wrapped.py create mode 100644 Python_Engine/Python/src/python_toolkit/bhom/wrapped/__init__.py create mode 100644 Python_Engine/Python/src/python_toolkit/bhom/wrapped/_test_wrapped.py diff --git a/Python_Engine/Python/src/python_toolkit/bhom/decorators/__init__.py b/Python_Engine/Python/src/python_toolkit/bhom/decorators/__init__.py index cbb34ff..70cea52 100644 --- a/Python_Engine/Python/src/python_toolkit/bhom/decorators/__init__.py +++ b/Python_Engine/Python/src/python_toolkit/bhom/decorators/__init__.py @@ -1 +1 @@ -from .bhom_callable_decorator import bhom_callable \ No newline at end of file +from ._bhom_callable_decorator import bhom_wrapper \ No newline at end of file diff --git a/Python_Engine/Python/src/python_toolkit/bhom/decorators/_bhom_callable_decorator.py b/Python_Engine/Python/src/python_toolkit/bhom/decorators/_bhom_callable_decorator.py new file mode 100644 index 0000000..04e9963 --- /dev/null +++ b/Python_Engine/Python/src/python_toolkit/bhom/decorators/_bhom_callable_decorator.py @@ -0,0 +1,83 @@ +import json +from typing import Any, Callable, Union, Dict +from functools import wraps +from ..bhom_object import CONSOLE_LOGGER, BHoMJSONDecoder, BHoMJSONEncoder, BHoMObject + +class _BHoMWrapper: + _registered_methods: Dict[str, Callable] = {} + + def bhom_callable(self, identifier: str, argument_types:Dict[str, type] = {}, encoder_cls: type = BHoMJSONEncoder, decoder_cls: type = BHoMJSONDecoder): + """Decorator for functions to be made callable from BHoM C# methods/adapters. + + Note: methods that this wraps must not have "__input_json__" as a default kwarg, as this is used internally to allow BHoM adapters to call the method. + + when __input_json__ is set, this will cause the method to always output BHoM style json (using the encoder_cls provided to this decorator) + + Args: + argument_types (dict[str, type]): this is a dictionary that is used to map the argument names to types (specifically BHoMObject types) to subclasses of BHoMObjects. + For example, if you have a class that is a subclass of BHoMObject, the default serialiser will only deserialise json to a BHoMObject. + To go the extra step to get your class, you must provide the type in this dictionary to allow the wrapper to convert the BHoMObject type to your desired type. + + encoder_cls (JSONEncoder): A JSONEncoder (ideally one that is a subclass of BHoMJSONEncoder). Mainly this is for if a custom encoder has been implemented for a specific toolkit. + + decoder_cls (JSONDecoder): same as encoder_cls but for JSONDecoder. + """ + def decorator(function: Callable): + + @wraps(function) + def wrapper(*args, **kwargs) -> Union[str, Any]: + + do_wrap:bool = False + + if "__input_json__" in kwargs and len(args) == 0: + do_wrap = True + + #get dictionary from input as file path or json like string. + input_json = kwargs.pop("__input_json__") + + if not input_json.startswith("{"): #assume it's a path + with open(input_json, "r") as f: + input_json = f.read() + + try: + json_kwargs: dict = json.loads(input_json, cls=decoder_cls) + except: + CONSOLE_LOGGER.error("Could not load JSON from file or string due to invalid JSON. Attempting to run with given args and kwargs.", exc_info=1) + + #update kwargs with json + for kwarg_name in json_kwargs: + val = json_kwargs[kwarg_name] + + if kwarg_name in argument_types: + t = argument_types[kwarg_name] + + if issubclass(t, BHoMObject) and type(json_kwargs[kwarg_name]) is BHoMObject: + val = t._from_bhom_object(json_kwargs[kwarg_name]) + + kwargs[kwarg_name] = val + + rtn = function(*args, **kwargs) + + if do_wrap: + json_rtn = json.dumps(rtn, cls=encoder_cls) + + return json_rtn + + return rtn + + self._registered_methods[identifier] = wrapper + return wrapper + + return decorator + + def get_registered_method(self, method_identifier: str): + method = self._registered_methods.get(method_identifier, None) + + if method is None: + CONSOLE_LOGGER.error("The requested method could not be found.") + + return method + +#the registered methods are stored as a class attribute, so this isn't actually needed +#but this is easier to use, otherwise a new instance must be created every time a method needs to be wrapped +bhom_wrapper = _BHoMWrapper() \ No newline at end of file diff --git a/Python_Engine/Python/src/python_toolkit/bhom/decorators/bhom_callable_decorator.py b/Python_Engine/Python/src/python_toolkit/bhom/decorators/bhom_callable_decorator.py deleted file mode 100644 index 7e16707..0000000 --- a/Python_Engine/Python/src/python_toolkit/bhom/decorators/bhom_callable_decorator.py +++ /dev/null @@ -1,65 +0,0 @@ -import json -from typing import Any, Callable, Union, Dict -from functools import wraps -from ..bhom_object import CONSOLE_LOGGER, BHoMJSONDecoder, BHoMJSONEncoder, BHoMObject - -def bhom_callable(argument_types:Dict[str, type] = {}, encoder_cls: type = BHoMJSONEncoder, decoder_cls: type = BHoMJSONDecoder): - """Decorator for functions to be made callable from BHoM C# methods/adapters. - - Note: methods that this wraps must not have "__input_json__" as a default kwarg, as this is used internally to allow BHoM adapters to call the method. - - when __input_json__ is set, this will cause the method to always output BHoM style json (using the encoder_cls provided to this decorator) - - Args: - argument_types (dict[str, type]): this is a dictionary that is used to map the argument names to types (specifically BHoMObject types) to subclasses of BHoMObjects. - For example, if you have a class that is a subclass of BHoMObject, the default serialiser will only deserialise json to a BHoMObject. - To go the extra step to get your class, you must provide the type in this dictionary to allow the wrapper to convert the BHoMObject type to your desired type. - - encoder_cls (JSONEncoder): A JSONEncoder (ideally one that is a subclass of BHoMJSONEncoder). Mainly this is for if a custom encoder has been implemented for a specific toolkit. - - decoder_cls (JSONDecoder): same as encoder_cls but for JSONDecoder. - """ - def decorator(function: Callable): - - @wraps(function) - def wrapper(*args, **kwargs) -> Union[str, Any]: - - do_wrap:bool = False - - if "__input_json__" in kwargs and len(args) == 0: - do_wrap = True - - #get dictionary from input as file path or json like string. - input_json = kwargs.pop("__input_json__") - - if not input_json.startswith("{"): #assume it's a path - with open(input_json, "r") as f: - input_json = f.read() - - try: - json_kwargs: dict = json.loads(input_json, cls=decoder_cls) - except: - CONSOLE_LOGGER.error("Could not load JSON from file or string due to invalid JSON. Attempting to run with given args and kwargs.", exc_info=1) - - #update kwargs with json - for kwarg_name in json_kwargs: - val = json_kwargs[kwarg_name] - - if kwarg_name in argument_types: - t = argument_types[kwarg_name] - - if issubclass(t, BHoMObject) and type(json_kwargs[kwarg_name]) is BHoMObject: - val = t._from_bhom_object(json_kwargs[kwarg_name]) - - kwargs[kwarg_name] = val - - rtn = function(*args, **kwargs) - - if do_wrap: - json_rtn = json.dumps(rtn, cls=encoder_cls) - - return json_rtn - - return rtn - return wrapper - return decorator \ No newline at end of file diff --git a/Python_Engine/Python/src/python_toolkit/bhom/run_wrapped.py b/Python_Engine/Python/src/python_toolkit/bhom/run_wrapped.py new file mode 100644 index 0000000..2a21c36 --- /dev/null +++ b/Python_Engine/Python/src/python_toolkit/bhom/run_wrapped.py @@ -0,0 +1,7 @@ +from .decorators import bhom_wrapper +from . import wrapped + +def run_wrapped(identifier: str, json: str): + method = bhom_wrapper.get_registered_method(identifier) + + print(method(__input_json__ = json)) \ No newline at end of file diff --git a/Python_Engine/Python/src/python_toolkit/bhom/wrapped/__init__.py b/Python_Engine/Python/src/python_toolkit/bhom/wrapped/__init__.py new file mode 100644 index 0000000..10636f6 --- /dev/null +++ b/Python_Engine/Python/src/python_toolkit/bhom/wrapped/__init__.py @@ -0,0 +1,13 @@ +from importlib import import_module +from pathlib import Path +import os + +python_files = Path(__file__).parent.glob("**/*.py") + +for file in python_files: + if file.name == "__init__.py": + continue + + rel = file.relative_to(Path(__file__).parent) + module = "." + str(rel).replace(".py", "").replace(os.path.sep, ".") + import_module(module, "python_toolkit.bhom.wrapped") \ No newline at end of file diff --git a/Python_Engine/Python/src/python_toolkit/bhom/wrapped/_test_wrapped.py b/Python_Engine/Python/src/python_toolkit/bhom/wrapped/_test_wrapped.py new file mode 100644 index 0000000..ac0764f --- /dev/null +++ b/Python_Engine/Python/src/python_toolkit/bhom/wrapped/_test_wrapped.py @@ -0,0 +1,10 @@ +from python_toolkit.plot.heatmap import heatmap +from ..decorators import bhom_wrapper +import pandas as pd + +@bhom_wrapper.bhom_callable("test") +def heatmap_2(geometry, **kwargs): + print(geometry) + print(kwargs["arg2"]) + geometry.x = 20 + return geometry