Skip to content

Commit

Permalink
Fix class and attribute types (#40)
Browse files Browse the repository at this point in the history
- Common:
  - Fix some attribute types
  - Fix handling of Decimal as float class
- Add class property "is_a_primitive_class" and attribute property
"is_primitive_float_attribute"
  - Improve _get_attribute_type in cimgen.py
  - Improve setting of long profile names
  - Refactor _merge_profiles and _merge_classes in cimgen.py
- Add "is_a_datatype_class" and "is_datatype_attribute" (stereotype ==
"CIMDatatype"), use these instead of "is_a_float_class" and
"is_primitive_float_attribute"
- modernpython:
- Fix type and default of some attributes (for enums and attribute types
Date, DateTime, MonthDay, Status, StreetAddress, StreetDetail,
TownDetail)
- Rename writer.py to chevron_writer.py (to prevent conflicts with the
new writer in
https://github.com/zaphiro-technologies/cimgen/tree/xml-generation-and-parsing)
  • Loading branch information
m-mirz authored Nov 16, 2024
2 parents f041302 + b2376a8 commit 8010364
Show file tree
Hide file tree
Showing 10 changed files with 152 additions and 251 deletions.
280 changes: 84 additions & 196 deletions cimgen/cimgen.py

Large diffs are not rendered by default.

28 changes: 16 additions & 12 deletions cimgen/languages/cpp/lang_pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,18 +56,14 @@ def get_class_location(class_name, class_map, version): # NOSONAR
# This is the function that runs the template.
def run_template(output_path, class_details):

if class_details["is_a_float_class"]:
if class_details["is_a_datatype_class"] or class_details["class_name"] in ("Float", "Decimal"):
templates = float_template_files
elif class_details["is_an_enum_class"]:
templates = enum_template_files
else:
templates = template_files

if (
class_details["class_name"] == "Integer"
or class_details["class_name"] == "Boolean"
or class_details["class_name"] == "Date"
):
if class_details["class_name"] in ("String", "Integer", "Boolean", "Date"):
# These classes are defined already
# We have to implement operators for them
return
Expand Down Expand Up @@ -108,7 +104,7 @@ def label(text, render):
def insert_assign_fn(text, render):
attribute_txt = render(text)
attribute_json = eval(attribute_txt)
if not (attribute_json["is_primitive_attribute"] or attribute_json["is_enum_attribute"]):
if not _attribute_is_primitive_or_datatype_or_enum(attribute_json):
return ""
label = attribute_json["label"]
class_name = attribute_json["domain"]
Expand All @@ -128,7 +124,7 @@ def insert_assign_fn(text, render):
def insert_class_assign_fn(text, render):
attribute_txt = render(text)
attribute_json = eval(attribute_txt)
if attribute_json["is_primitive_attribute"] or attribute_json["is_enum_attribute"]:
if _attribute_is_primitive_or_datatype_or_enum(attribute_json):
return ""
label = attribute_json["label"]
class_name = attribute_json["domain"]
Expand Down Expand Up @@ -169,7 +165,7 @@ def create_class_assign(text, render):
attribute_json = eval(attribute_txt)
assign = ""
attribute_class = attribute_json["attribute_class"]
if attribute_json["is_primitive_attribute"] or attribute_json["is_enum_attribute"]:
if _attribute_is_primitive_or_datatype_or_enum(attribute_json):
return ""
if attribute_json["is_list_attribute"]:
assign = (
Expand Down Expand Up @@ -233,7 +229,7 @@ def create_assign(text, render):
attribute_json = eval(attribute_txt)
assign = ""
_class = attribute_json["attribute_class"]
if not (attribute_json["is_primitive_attribute"] or attribute_json["is_enum_attribute"]):
if not _attribute_is_primitive_or_datatype_or_enum(attribute_json):
return ""
label_without_keyword = attribute_json["label"]
if label_without_keyword == "switch":
Expand Down Expand Up @@ -286,7 +282,7 @@ def attribute_decl(text, render):

def _attribute_decl(attribute):
_class = attribute["attribute_class"]
if attribute["is_primitive_attribute"] or attribute["is_enum_attribute"]:
if _attribute_is_primitive_or_datatype_or_enum(attribute):
return "CIMPP::" + _class
if attribute["is_list_attribute"]:
return "std::list<CIMPP::" + _class + "*>"
Expand All @@ -303,7 +299,7 @@ def _create_attribute_includes(text, render):
if jsonStringNoHtmlEsc is not None and jsonStringNoHtmlEsc != "":
attributes = json.loads(jsonStringNoHtmlEsc)
for attribute in attributes:
if attribute["is_primitive_attribute"] or attribute["is_enum_attribute"]:
if _attribute_is_primitive_or_datatype_or_enum(attribute):
unique[attribute["attribute_class"]] = True
for clarse in unique:
include_string += '\n#include "' + clarse + '.hpp"'
Expand Down Expand Up @@ -353,6 +349,14 @@ def set_default(dataType):
return "nullptr"


def _attribute_is_primitive_or_datatype_or_enum(attribute: dict) -> bool:
return _attribute_is_primitive_or_datatype(attribute) or attribute["is_enum_attribute"]


def _attribute_is_primitive_or_datatype(attribute: dict) -> bool:
return attribute["is_primitive_attribute"] or attribute["is_datatype_attribute"]


# The code below this line is used after the main cim_generate phase to generate
# two include files. They are called CIMClassList.hpp and IEC61970.hpp, and
# contain the list of the class files and the list of define functions that add
Expand Down
20 changes: 10 additions & 10 deletions cimgen/languages/java/lang_pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,21 +47,17 @@ def run_template(output_path, class_details):

class_details["primitives"] = []
for attr in class_details["attributes"]:
if attr["is_primitive_attribute"] or attr["is_enum_attribute"]:
if _attribute_is_primitive_or_datatype_or_enum(attr):
class_details["primitives"].append(attr)

if class_details["is_a_float_class"]:
if class_details["is_a_datatype_class"] or class_details["class_name"] in ("Float", "Decimal"):
templates = float_template_files
elif class_details["is_an_enum_class"]:
templates = enum_template_files
else:
templates = template_files

if (
class_details["class_name"] == "Integer"
or class_details["class_name"] == "Boolean"
or class_details["class_name"] == "Date"
):
if class_details["class_name"] in ("String", "Integer", "Boolean", "Date"):
# These classes are defined already
# We have to implement operators for them
return
Expand Down Expand Up @@ -107,7 +103,7 @@ def create_assign(text, render):
attribute_json = eval(attribute_txt)
assign = ""
_class = attribute_json["attribute_class"]
if not (attribute_json["is_primitive_attribute"] or attribute_json["is_enum_attribute"]):
if not _attribute_is_primitive_or_datatype_or_enum(attribute_json):
return ""
label_without_keyword = attribute_json["label"]
if label_without_keyword == "switch":
Expand Down Expand Up @@ -144,7 +140,7 @@ def attribute_decl(text, render):

def _attribute_decl(attribute):
_class = attribute["attribute_class"]
if attribute["is_primitive_attribute"] or attribute["is_enum_attribute"]:
if _attribute_is_primitive_or_datatype_or_enum(attribute):
return _class
if attribute["is_list_attribute"]:
return "List<" + _class + ">"
Expand All @@ -161,7 +157,7 @@ def _create_attribute_includes(text, render):
if jsonStringNoHtmlEsc is not None and jsonStringNoHtmlEsc != "":
attributes = json.loads(jsonStringNoHtmlEsc)
for attribute in attributes:
if attribute["is_primitive_attribute"] or attribute["is_enum_attribute"]:
if _attribute_is_primitive_or_datatype_or_enum(attribute):
unique[attribute["attribute_class"]] = True
for clarse in unique:
if clarse != "String":
Expand Down Expand Up @@ -210,6 +206,10 @@ def set_default(dataType):
return "nullptr"


def _attribute_is_primitive_or_datatype_or_enum(attribute: dict) -> bool:
return attribute["is_primitive_attribute"] or attribute["is_datatype_attribute"] or attribute["is_enum_attribute"]


# The code below this line is used after the main cim_generate phase to generate
# two include files. They are called CIMClassList.hpp and IEC61970.hpp, and
# contain the list of the class files and the list of define functions that add
Expand Down
12 changes: 6 additions & 6 deletions cimgen/languages/javascript/lang_pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ def get_class_location(class_name, class_map, version): # NOSONAR
def select_primitive_render_function(class_details):
class_name = class_details["class_name"]
render = ""
if class_details["is_a_float_class"]:
if class_details["is_a_datatype_class"]:
render = aggregateRenderer["renderFloat"]
elif class_name in ("Float", "Decimal"):
render = aggregateRenderer["renderFloat"]
elif class_name == "String":
render = aggregateRenderer["renderString"]
Expand All @@ -88,9 +90,6 @@ def select_primitive_render_function(class_details):
elif class_name == "DateTime":
# TODO: Implementation Required!
render = aggregateRenderer["renderString"]
elif class_name == "Decimal":
# TODO: Implementation Required!
render = aggregateRenderer["renderString"]
elif class_name == "Integer":
# TODO: Implementation Required!
render = aggregateRenderer["renderString"]
Expand All @@ -102,6 +101,8 @@ def select_primitive_render_function(class_details):

# This is the function that runs the template.
def run_template(output_path, class_details):
if class_details["class_name"] == "String":
return

for index, attribute in enumerate(class_details["attributes"]):
if not attribute["is_used"]:
Expand Down Expand Up @@ -156,8 +157,7 @@ def _create_cgmes_profile(output_path: str, profile_details: list, cim_namespace


def _get_class_type(class_details):
class_name = class_details["class_name"]
if class_details["is_a_float_class"] or class_name in ("String", "Boolean", "Integer"):
if class_details["is_a_primitive_class"] or class_details["is_a_datatype_class"]:
return "primitive"
if class_details["is_an_enum_class"]:
return "enum"
Expand Down
35 changes: 16 additions & 19 deletions cimgen/languages/modernpython/lang_pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def get_class_location(class_name, class_map, version): # NOSONAR
partials = {}


# called by chevron, text contains the label {{dataType}}, which is evaluated by the renderer (see class template)
# called by chevron, text contains the attribute infos, which are evaluated by the renderer (see class template)
def _set_default(text, render):
return _get_type_and_default(text, render)[1]

Expand All @@ -56,32 +56,29 @@ def _set_type(text, render):
return _get_type_and_default(text, render)[0]


def _get_type_and_default(text, renderer) -> tuple[str, str]:
result = renderer(text)
# the field {{dataType}} either contains the multiplicity of an attribute if it is a reference or otherwise the
# datatype of the attribute. If no datatype is set and there is also no multiplicity entry for an attribute, the
# default value is set to None. The multiplicity is set for all attributes, but the datatype is only set for basic
# data types. If the data type entry for an attribute is missing, the attribute contains a reference and therefore
# the default value is either None or [] depending on the multiplicity. See also write_python_files
# The default will be copied as-is, hence the possibility to have default or default_factory.
if result in ["M:1", "M:0..1", "M:1..1", ""]:
def _get_type_and_default(text, render) -> tuple[str, str]:
attribute_txt = render(text)
attribute_json = eval(attribute_txt)
if attribute_json["is_class_attribute"]:
return ("Optional[str]", "default=None")
elif result in ["M:0..n", "M:1..n"] or "M:" in result:
elif attribute_json["is_list_attribute"]:
return ("list", "default_factory=list")

result = result.split("#")[1]
if result in ["integer", "Integer"]:
elif attribute_json["is_datatype_attribute"]:
return ("float", "default=0.0")
elif attribute_json["attribute_class"] in ("Float", "Decimal"):
return ("float", "default=0.0")
elif attribute_json["attribute_class"] == "Integer":
return ("int", "default=0")
elif result in ["String", "DateTime", "Date"]:
return ("str", 'default=""')
elif result == "Boolean":
elif attribute_json["attribute_class"] == "Boolean":
return ("bool", "default=False")
else:
# everything else should be a float
return ("float", "default=0.0")
# everything else should be a string
return ("str", 'default=""')


def run_template(output_path, class_details):
if class_details["is_a_primitive_class"] or class_details["is_a_datatype_class"]:
return
for template_info in template_files:
resource_file = Path(
os.path.join(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ class {{class_name}}({{sub_class_of}}):
"""

{{#attributes}}
{{label}}: {{#setType}}{{dataType}}{{/setType}} = Field(
{{#setDefault}}{{dataType}}{{/setDefault}},
{{label}}: {{#setType}}{{.}}{{/setType}} = Field(
{{#setDefault}}{{.}}{{/setDefault}},
json_schema_extra={
"in_profiles": [
{{#attr_origin}}
Expand All @@ -33,6 +33,7 @@ class {{class_name}}({{sub_class_of}}):
],
"is_used": {{#is_used}}True{{/is_used}}{{^is_used}}False{{/is_used}},
"is_class_attribute": {{#is_class_attribute}}True{{/is_class_attribute}}{{^is_class_attribute}}False{{/is_class_attribute}},
"is_datatype_attribute": {{#is_datatype_attribute}}True{{/is_datatype_attribute}}{{^is_datatype_attribute}}False{{/is_datatype_attribute}},
"is_enum_attribute": {{#is_enum_attribute}}True{{/is_enum_attribute}}{{^is_enum_attribute}}False{{/is_enum_attribute}},
"is_list_attribute": {{#is_list_attribute}}True{{/is_list_attribute}}{{^is_list_attribute}}False{{/is_list_attribute}},
"is_primitive_attribute": {{#is_primitive_attribute}}True{{/is_primitive_attribute}}{{^is_primitive_attribute}}False{{/is_primitive_attribute}},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from pycgmes.utils.profile import BaseProfile, Profile


class Writer:
class ChevronWriter:
"""Class for writing CIM RDF/XML files."""

def __init__(self, objects: dict[str, Base]):
Expand Down Expand Up @@ -96,14 +96,15 @@ def sort_attributes_to_profile(self, profile: BaseProfile, class_profile_map: di
about = []
for rdfid, obj in self.objects.items():
typ = obj.apparent_name()
if typ in class_profile_map and Writer.is_class_matching_profile(obj, profile):
if typ in class_profile_map and ChevronWriter.is_class_matching_profile(obj, profile):
class_profile = class_profile_map[typ]
main_entry_of_object = class_profile == profile

attributes = []
for attr, attr_infos in Writer.get_attribute_infos(obj).items():
for attr, attr_infos in ChevronWriter.get_attribute_infos(obj).items():
value = attr_infos["value"]
if value and attr != "mRID" and Writer.get_attribute_profile(obj, attr, class_profile) == profile:
attribute_profile = ChevronWriter.get_attribute_profile(obj, attr, class_profile)
if value and attr != "mRID" and attribute_profile == profile:
if isinstance(value, (list, tuple)):
attributes.extend(attr_infos | {"value": v} for v in value)
else:
Expand Down Expand Up @@ -150,7 +151,7 @@ def get_class_profile_map(obj_list: list[Base]) -> dict[str, BaseProfile]:
:param obj_list: List of CIM objects.
:return: Mapping of CIM type to profile.
"""
return {obj.apparent_name(): Writer.get_class_profile(obj) for obj in obj_list}
return {obj.apparent_name(): ChevronWriter.get_class_profile(obj) for obj in obj_list}

@staticmethod
def get_attribute_profile(obj: Base, attr: str, class_profile: BaseProfile) -> BaseProfile | None:
Expand Down Expand Up @@ -190,6 +191,7 @@ def get_attribute_infos(obj: Base) -> dict[str, dict[str, object]]:
"namespace": extra.get("namespace", obj.namespace),
"value": getattr(obj, attr),
"is_class_attribute": extra.get("is_class_attribute"),
"is_datatype_attribute": extra.get("is_datatype_attribute"),
"is_enum_attribute": extra.get("is_enum_attribute"),
"is_list_attribute": extra.get("is_list_attribute"),
"is_primitive_attribute": extra.get("is_primitive_attribute"),
Expand Down
6 changes: 6 additions & 0 deletions cimgen/languages/modernpython/utils/export_template.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
{{#is_class_attribute}}
<cim:{{attr_name}} rdf:resource="#{{value}}" />
{{/is_class_attribute}}
{{#is_datatype_attribute}}
<cim:{{attr_name}}>{{value}}</cim:{{attr_name}}>
{{/is_datatype_attribute}}
{{#is_enum_attribute}}
<cim:{{attr_name}} rdf:resource="{{namespace}}{{value}}" />
{{/is_enum_attribute}}
Expand All @@ -31,6 +34,9 @@
{{#is_class_attribute}}
<cim:{{attr_name}} rdf:resource="#{{value}}" />
{{/is_class_attribute}}
{{#is_datatype_attribute}}
<cim:{{attr_name}}>{{value}}</cim:{{attr_name}}>
{{/is_datatype_attribute}}
{{#is_enum_attribute}}
<cim:{{attr_name}} rdf:resource="{{namespace}}{{value}}" />
{{/is_enum_attribute}}
Expand Down
2 changes: 2 additions & 0 deletions cimgen/languages/python/lang_pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ def _set_default(text, render):


def run_template(output_path, class_details):
if class_details["class_name"] == "String":
return
for template_info in template_files:
class_file = os.path.join(output_path, class_details["class_name"] + template_info["ext"])
_write_templated_file(class_file, class_details, template_info["filename"])
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ dependencies = [
"xmltodict >= 0.13.0, < 1",
"chevron >= 0.14.0, < 1",
"pydantic < 2",
"beautifulsoup4 >= 4.12.2, < 5"
"beautifulsoup4 >= 4.12.2, < 5",
"setuptools"
]

requires-python = ">=3.11"
Expand Down

0 comments on commit 8010364

Please sign in to comment.