From a3c99a3c231e88e3de119491bcc8dc44f2158e38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCnther?= Date: Sat, 21 Sep 2024 17:02:29 +0200 Subject: [PATCH 01/14] Add generation of CGMESProfile class (including profile URIs) for modernpython MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Günther --- cimgen/cimgen.py | 7 +-- cimgen/languages/cpp/lang_pack.py | 8 +-- cimgen/languages/java/lang_pack.py | 6 +- cimgen/languages/javascript/lang_pack.py | 2 +- cimgen/languages/modernpython/lang_pack.py | 62 +++++++++++++------ .../modernpython/templates/__init__.py | 3 + .../cimpy_cgmesProfile_template.mustache | 52 ++++++++++++++++ .../templates/cimpy_class_template.mustache | 35 ++++++++--- .../cimpy_constants_template.mustache} | 8 ++- cimgen/languages/modernpython/utils/base.py | 9 +-- .../languages/modernpython/utils/profile.py | 36 ----------- cimgen/languages/python/lang_pack.py | 5 +- .../templates/cimpy_class_template.mustache | 2 +- 13 files changed, 149 insertions(+), 86 deletions(-) create mode 100644 cimgen/languages/modernpython/templates/__init__.py create mode 100644 cimgen/languages/modernpython/templates/cimpy_cgmesProfile_template.mustache rename cimgen/languages/modernpython/{utils/constants.py => templates/cimpy_constants_template.mustache} (57%) delete mode 100644 cimgen/languages/modernpython/utils/profile.py diff --git a/cimgen/cimgen.py b/cimgen/cimgen.py index 4f5056a0..cbaff785 100644 --- a/cimgen/cimgen.py +++ b/cimgen/cimgen.py @@ -505,7 +505,7 @@ def _write_python_files(elem_dict, lang_pack, output_path, version): class_details = { "attributes": _find_multiple_attributes(elem_dict[class_name].attributes()), - "class_location": lang_pack.get_class_location(class_name, elem_dict, output_path), + "class_location": lang_pack.get_class_location(class_name, elem_dict, version), "class_name": class_name, "class_origin": elem_dict[class_name].origins(), "instances": elem_dict[class_name].instances(), @@ -765,10 +765,7 @@ def cim_generate(directory, output_path, version, lang_pack): # get information for writing python files and write python files _write_python_files(class_dict_with_origins, lang_pack, output_path, version) - if "modernpython" in lang_pack.__name__: - lang_pack.resolve_headers(output_path, version) - else: - lang_pack.resolve_headers(output_path) + lang_pack.resolve_headers(output_path, version) logger.info("Elapsed Time: {}s\n\n".format(time() - t0)) diff --git a/cimgen/languages/cpp/lang_pack.py b/cimgen/languages/cpp/lang_pack.py index 624847c1..aeb5d434 100644 --- a/cimgen/languages/cpp/lang_pack.py +++ b/cimgen/languages/cpp/lang_pack.py @@ -430,7 +430,7 @@ def set_default(dataType): # 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 mutliplicity. See also write_python_files + # the default value is either None or [] depending on the multiplicity. See also write_python_files if dataType in ["M:1", "M:0..1", "M:1..1", "M:0..n", "M:1..n", ""] or "M:" in dataType: return "0" dataType = dataType.split("#")[1] @@ -499,7 +499,7 @@ def _create_header_include_file(directory, header_include_filename, header, foot f.writelines(header) -def resolve_headers(output_path): +def resolve_headers(path: str, version: str): # NOSONAR class_list_header = [ "#ifndef CIMCLASSLIST_H\n", "#define CIMCLASSLIST_H\n", @@ -513,7 +513,7 @@ def resolve_headers(output_path): ] _create_header_include_file( - output_path, + path, "CIMClassList.hpp", class_list_header, class_list_footer, @@ -526,7 +526,7 @@ def resolve_headers(output_path): iec61970_footer = ['#include "UnknownType.hpp"\n', "#endif"] _create_header_include_file( - output_path, + path, "IEC61970.hpp", iec61970_header, iec61970_footer, diff --git a/cimgen/languages/java/lang_pack.py b/cimgen/languages/java/lang_pack.py index 170777f4..45162cca 100644 --- a/cimgen/languages/java/lang_pack.py +++ b/cimgen/languages/java/lang_pack.py @@ -351,7 +351,7 @@ def set_default(dataType): # 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 mutliplicity. See also write_python_files + # the default value is either None or [] depending on the multiplicity. See also write_python_files if dataType in ["M:1", "M:0..1", "M:1..1", "M:0..n", "M:1..n", ""] or "M:" in dataType: return "0" dataType = dataType.split("#")[1] @@ -423,7 +423,7 @@ def _create_header_include_file(directory, header_include_filename, header, foot f.writelines(header) -def resolve_headers(output_path): +def resolve_headers(path: str, version: str): # NOSONAR class_list_header = [ "package cim4j;\n", "import java.util.Map;\n", @@ -439,7 +439,7 @@ def resolve_headers(output_path): class_list_footer = [" );\n", "}\n"] _create_header_include_file( - output_path, + path, "CIMClassMap.java", class_list_header, class_list_footer, diff --git a/cimgen/languages/javascript/lang_pack.py b/cimgen/languages/javascript/lang_pack.py index 13638718..3b3011dc 100644 --- a/cimgen/languages/javascript/lang_pack.py +++ b/cimgen/languages/javascript/lang_pack.py @@ -219,5 +219,5 @@ def _get_rid_of_hash(name): return name -def resolve_headers(output_path): +def resolve_headers(path: str, version: str): # NOSONAR pass diff --git a/cimgen/languages/modernpython/lang_pack.py b/cimgen/languages/modernpython/lang_pack.py index fe35fc82..a5b96e6b 100644 --- a/cimgen/languages/modernpython/lang_pack.py +++ b/cimgen/languages/modernpython/lang_pack.py @@ -13,10 +13,9 @@ # Setup called only once: make output directory, create base class, create profile class, etc. # This makes sure we have somewhere to write the classes, and # creates a couple of files the python implementation needs. -# cgmes_profile_details contains index, names und uris for each profile. -# We don't use that here because we aren't creating the header -# data for the separate profiles. -def setup(output_path: str, cgmes_profile_details: list, cim_namespace: str): # NOSONAR +# cgmes_profile_details contains index, names and uris for each profile. +# We use that to create the header data for the profiles. +def setup(output_path: str, cgmes_profile_details: list, cim_namespace: str): for file in Path(output_path).glob("**/*.py"): file.unlink() @@ -25,6 +24,8 @@ def setup(output_path: str, cgmes_profile_details: list, cim_namespace: str): # dest_dir = Path(output_path) / "utils" copy_tree(str(source_dir), str(dest_dir)) + _create_constants(output_path, cim_namespace) + _create_cgmes_profile(output_path, cgmes_profile_details) def location(version): @@ -35,6 +36,8 @@ def location(version): # These are the files that are used to generate the python files. template_files = [{"filename": "cimpy_class_template.mustache", "ext": ".py"}] +constants_template_files = [{"filename": "cimpy_constants_template.mustache", "ext": ".py"}] +profile_template_files = [{"filename": "cimpy_cgmesProfile_template.mustache", "ext": ".py"}] def get_class_location(class_name, class_map, version): @@ -88,7 +91,6 @@ def set_float_classes(new_float_classes): def run_template(output_path, class_details): for template_info in template_files: - resource_file = Path( os.path.join( output_path, @@ -99,23 +101,39 @@ def run_template(output_path, class_details): if not resource_file.exists(): if not (parent := resource_file.parent).exists(): parent.mkdir() + class_details["setDefault"] = _set_default + class_details["setType"] = _set_type + _write_templated_file(resource_file, class_details, template_info["filename"]) + + +def _write_templated_file(class_file, class_details, template_filename): + with open(class_file, "w", encoding="utf-8") as file: + templates = files("cimgen.languages.modernpython.templates") + with templates.joinpath(template_filename).open(encoding="utf-8") as f: + args = { + "data": class_details, + "template": f, + "partials_dict": partials, + } + output = chevron.render(**args) + file.write(output) + + +def _create_constants(output_path: str, cim_namespace: str): + for template_info in constants_template_files: + class_file = os.path.join(output_path, "utils", "constants" + template_info["ext"]) + class_details = {"cim_namespace": cim_namespace} + _write_templated_file(class_file, class_details, template_info["filename"]) - with open(resource_file, "w", encoding="utf-8") as file: - class_details["setDefault"] = _set_default - class_details["setType"] = _set_type - templates = files("cimgen.languages.modernpython.templates") - with templates.joinpath(template_info["filename"]).open(encoding="utf-8") as f: - args = { - "data": class_details, - "template": f, - "partials_dict": partials, - } - output = chevron.render(**args) - file.write(output) +def _create_cgmes_profile(output_path: str, profile_details: list): + for template_info in profile_template_files: + class_file = os.path.join(output_path, "utils", "profile" + template_info["ext"]) + class_details = {"profiles": profile_details} + _write_templated_file(class_file, class_details, template_info["filename"]) -def resolve_headers(dest: str, version: str): +def resolve_headers(path: str, version: str): """Add all classes in __init__.py""" if match := re.search(r"(?P\d+_\d+_\d+)", version): # NOSONAR @@ -123,9 +141,13 @@ def resolve_headers(dest: str, version: str): else: raise ValueError(f"Cannot parse {version} to extract a number.") - dest = Path(dest) / "resources" + src = Path(__file__).parent / "templates" + dest = Path(path) / "resources" + with open(src / "__init__.py", "r", encoding="utf-8") as template_file: + template_text = template_file.read() with open(dest / "__init__.py", "a", encoding="utf-8") as header_file: - header_file.write(f"CGMES_VERSION='{version_number}'\n") + header_file.write(template_text) + header_file.write(f'\nCGMES_VERSION = "{version_number}"\n') # # Under this, add all imports in init. Disabled becasue loading 600 unneeded classes is slow. # _all = ["CGMES_VERSION"] diff --git a/cimgen/languages/modernpython/templates/__init__.py b/cimgen/languages/modernpython/templates/__init__.py new file mode 100644 index 00000000..d8f1147b --- /dev/null +++ b/cimgen/languages/modernpython/templates/__init__.py @@ -0,0 +1,3 @@ +""" +Generated from the CGMES files via cimgen: https://github.com/sogno-platform/cimgen +""" diff --git a/cimgen/languages/modernpython/templates/cimpy_cgmesProfile_template.mustache b/cimgen/languages/modernpython/templates/cimpy_cgmesProfile_template.mustache new file mode 100644 index 00000000..95bec43a --- /dev/null +++ b/cimgen/languages/modernpython/templates/cimpy_cgmesProfile_template.mustache @@ -0,0 +1,52 @@ +""" +Generated from the CGMES files via cimgen: https://github.com/sogno-platform/cimgen +""" + +from enum import Enum +from functools import cached_property + + +class BaseProfile(str, Enum): + """ + Profile parent. Use it if you need your own profiles. + + All pycgmes objects requiring a Profile are actually asking for a `BaseProfile`. As + Enum with fields cannot be inherited or composed, just create your own CustomProfile without + trying to extend Profile. It will work. + """ + + @cached_property + def long_name(self) -> str: + """Return the long name of the profile.""" + return self.value + + @cached_property + def uris(self) -> list[str]: + """Return the list of uris of the profile.""" + raise NotImplementedError("Method has to be implemented in subclass.") + + +class Profile(BaseProfile): + """ + Enum containing all CGMES profiles and their export priority. + """ + + {{#profiles}} + {{short_name}} = "{{long_name}}" + {{/profiles}} + + @cached_property + def uris(self) -> list[str]: + """Return the list of uris of the profile.""" + return profile_uris[self.name] + + +profile_uris: dict[str, list[str]] = { # Those are strings, not real addresses, hence the NOSONAR. + {{#profiles}} + "{{short_name}}": [ + {{#uris}} + "{{uri}}", # NOSONAR + {{/uris}} + ], + {{/profiles}} +} diff --git a/cimgen/languages/modernpython/templates/cimpy_class_template.mustache b/cimgen/languages/modernpython/templates/cimpy_class_template.mustache index 21b4c551..acf2c8f2 100644 --- a/cimgen/languages/modernpython/templates/cimpy_class_template.mustache +++ b/cimgen/languages/modernpython/templates/cimpy_class_template.mustache @@ -1,15 +1,17 @@ """ -Generated from the CGMES 3 files via cimgen: https://github.com/sogno-platform/cimgen +Generated from the CGMES files via cimgen: https://github.com/sogno-platform/cimgen """ from functools import cached_property from typing import Optional + from pydantic import Field from pydantic.dataclasses import dataclass -from ..utils.profile import BaseProfile, Profile +from ..utils.profile import BaseProfile, Profile from {{class_location}} import {{sub_class_of}} + @dataclass class {{class_name}}({{sub_class_of}}): """ @@ -21,20 +23,37 @@ class {{class_name}}({{sub_class_of}}): """ {{#attributes}} - {{^isAssociationUsed}}# *Association not used* + {{#isAssociationUsed}} + {{label}}: {{#setType}}{{dataType}}{{/setType}} = Field( + {{#setDefault}}{{dataType}}{{/setDefault}}, + json_schema_extra={ + "in_profiles": [ + {{#attr_origin}} + Profile.{{origin}}, + {{/attr_origin}} + ] + }, + ) + {{/isAssociationUsed}} + {{^isAssociationUsed}} + # *Association not used* # Type {{dataType}} in CIM - # {{/isAssociationUsed}}{{label}} : {{#setType}}{{dataType}}{{/setType}} = Field({{#setDefault}}{{dataType}}{{/setDefault}}, json_schema_extra={"in_profiles":[{{#attr_origin}}Profile.{{origin}}, {{/attr_origin}}]}) {{^isAssociationUsed}}# noqa: E501{{/isAssociationUsed}} + # {{label}}: {{#setType}}{{dataType}}{{/setType}} = Field({{#setDefault}}{{dataType}}{{/setDefault}}, json_schema_extra={"in_profiles": [{{#attr_origin}}Profile.{{origin}}, {{/attr_origin}}]}) # noqa: E501 + {{/isAssociationUsed}} {{/attributes}} {{^attributes}} # No attributes defined for this class. - {{/attributes}} - + {{/attributes}} @cached_property - def possible_profiles(self)->set[BaseProfile]: + def possible_profiles(self) -> set[BaseProfile]: """ A resource can be used by multiple profiles. This is the set of profiles where this element can be found. """ - return { {{#class_origin}}Profile.{{origin}}, {{/class_origin}} } + return { + {{#class_origin}} + Profile.{{origin}}, + {{/class_origin}} + } diff --git a/cimgen/languages/modernpython/utils/constants.py b/cimgen/languages/modernpython/templates/cimpy_constants_template.mustache similarity index 57% rename from cimgen/languages/modernpython/utils/constants.py rename to cimgen/languages/modernpython/templates/cimpy_constants_template.mustache index aedb19d5..45af8344 100644 --- a/cimgen/languages/modernpython/utils/constants.py +++ b/cimgen/languages/modernpython/templates/cimpy_constants_template.mustache @@ -1,6 +1,10 @@ +""" +Generated from the CGMES files via cimgen: https://github.com/sogno-platform/cimgen +""" + # Default namespaces used by CGMES. -NAMESPACES = { # Those are strings, not real addresses, hence the NOSONAR. - "cim": "http://iec.ch/TC57/CIM100#", # NOSONAR +NAMESPACES: dict[str, str] = { # Those are strings, not real addresses, hence the NOSONAR. + "cim": "{{cim_namespace}}", # NOSONAR "entsoe": "http://entsoe.eu/CIM/SchemaExtension/3/1#", # NOSONAR "md": "http://iec.ch/TC57/61970-552/ModelDescription/1#", # NOSONAR "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", # NOSONAR diff --git a/cimgen/languages/modernpython/utils/base.py b/cimgen/languages/modernpython/utils/base.py index fc7936e9..2ee29b7c 100644 --- a/cimgen/languages/modernpython/utils/base.py +++ b/cimgen/languages/modernpython/utils/base.py @@ -4,9 +4,10 @@ from functools import cached_property from typing import Any, TypeAlias, TypedDict -from pycgmes.utils.constants import NAMESPACES from pydantic.dataclasses import dataclass +from pycgmes.utils.constants import NAMESPACES + from ..utils.config import cgmes_resource_config from .profile import BaseProfile @@ -108,7 +109,7 @@ def cgmes_attributes_in_profile(self, profile: BaseProfile | None) -> dict[str, with thus the parent class included in the attribute name. """ # What will be returned, has the qualname as key... - qual_attrs: dict[str, "CgmesAttribute"] = {} + qual_attrs: dict[str, CgmesAttribute] = {} # ... but we check existence with the unqualified (short) name. seen_attrs = set() @@ -160,6 +161,6 @@ class CgmesAttribute(TypedDict): # Actual value value: CgmesAttributeTypes - # The default will be None. Only custom attributes might have something different, given as metadata. + # Custom attributes might have something different, given as metadata. # See readme for more information. - namespace: str | None + namespace: str diff --git a/cimgen/languages/modernpython/utils/profile.py b/cimgen/languages/modernpython/utils/profile.py deleted file mode 100644 index 46f7d71d..00000000 --- a/cimgen/languages/modernpython/utils/profile.py +++ /dev/null @@ -1,36 +0,0 @@ -from enum import Enum -from functools import cached_property - - -class BaseProfile(str, Enum): - """ - Profile parent. Use it if you need your own profiles. - - All pycgmes objects requiring a Profile are actually asking for a `BaseProfile`. As - Enum with fields cannot be inherited or composed, just create your own CustomProfile without - trying to extend Profile. It will work. - """ - - @cached_property - def long_name(self) -> str: - """Return the long name of the profile.""" - return self.value - - -class Profile(BaseProfile): - """ - Enum containing all CGMES profiles and their export priority. - """ - - # DI= "DiagramLayout" # Not too sure about that one - DL = "DiagramLayout" - DY = "Dynamics" - EQ = "Equipment" - EQ_BD = "EquipmentBoundary" # Not too sure about that one - GL = "GeographicalLocation" - OP = "Operation" - SC = "ShortCircuit" - SSH = "SteadyStateHypothesis" - SV = "StateVariables" - TP = "Topology" - TP_BD = "TopologyBoundary" # Not too sure about that one diff --git a/cimgen/languages/python/lang_pack.py b/cimgen/languages/python/lang_pack.py index f9ed15b0..5a674e4d 100644 --- a/cimgen/languages/python/lang_pack.py +++ b/cimgen/languages/python/lang_pack.py @@ -55,7 +55,7 @@ def _set_default(text, render): # 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 mutliplicity. See also write_python_files + # the default value is either None or [] depending on the multiplicity. See also write_python_files if result in ["M:1", "M:0..1", "M:1..1", ""]: return "None" elif result in ["M:0..n", "M:1..n"] or "M:" in result: @@ -130,7 +130,8 @@ def _create_cgmes_profile(output_path: str, profile_details: list, cim_namespace class_blacklist = ["CGMESProfile"] -def resolve_headers(path): +def resolve_headers(path: str, version: str): # NOSONAR + """Add all classes in __init__.py""" filenames = glob.glob(path + "/*.py") include_names = [] for filename in sorted(filenames): diff --git a/cimgen/languages/python/templates/cimpy_class_template.mustache b/cimgen/languages/python/templates/cimpy_class_template.mustache index a461f9c0..9781eda5 100644 --- a/cimgen/languages/python/templates/cimpy_class_template.mustache +++ b/cimgen/languages/python/templates/cimpy_class_template.mustache @@ -21,7 +21,7 @@ class {{class_name}}({{sub_class_of}}): serializationProfile = {} recommendedClassProfile = Profile.{{recommended_class_profile}}.value - + {{#super_init}} __doc__ += "\nDocumentation of parent class {{sub_class_of}}:\n" + {{sub_class_of}}.__doc__ {{/super_init}} From e858154a13aad2208cb69db72e7130e5f1fb7aea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCnther?= Date: Sun, 22 Sep 2024 12:55:10 +0200 Subject: [PATCH 02/14] Improve class properties: rename "instances" to "enum_instances", "has_instances" to "is_an_enum_class", "is_a_float" to "is_a_float_class" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Günther --- cimgen/cimgen.py | 40 +++++++++---------- cimgen/languages/cpp/lang_pack.py | 4 +- .../cpp_enum_header_template.mustache | 4 +- .../cpp_enum_object_template.mustache | 4 +- cimgen/languages/java/lang_pack.py | 4 +- .../java/templates/java_enum.mustache | 4 +- .../templates/handlebars_template.mustache | 20 +++++----- 7 files changed, 40 insertions(+), 40 deletions(-) diff --git a/cimgen/cimgen.py b/cimgen/cimgen.py index cbaff785..bc3e0711 100644 --- a/cimgen/cimgen.py +++ b/cimgen/cimgen.py @@ -225,7 +225,7 @@ def __init__(self, rdfsEntry): self.about = rdfsEntry.about() self.attribute_list = [] self.comment = rdfsEntry.comment() - self.instance_list = [] + self.enum_instance_list = [] self.origin_list = [] self.super = rdfsEntry.subClassOf() self.subclasses = [] @@ -236,15 +236,15 @@ def attributes(self): def addAttribute(self, attribute): self.attribute_list.append(attribute) - def has_instances(self): - return len(self.instance_list) > 0 + def is_an_enum_class(self): + return len(self.enum_instance_list) > 0 - def instances(self): - return self.instance_list + def enum_instances(self): + return self.enum_instance_list - def addInstance(self, instance): - instance["index"] = len(self.instance_list) - self.instance_list.append(instance) + def addEnumInstance(self, instance): + instance["index"] = len(self.enum_instance_list) + self.enum_instance_list.append(instance) def addAttributes(self, attributes): for attribute in attributes: @@ -273,7 +273,7 @@ def _simple_float_attribute(attr): return attr["label"] == "value" and attr["dataType"] == "#Float" return False - def is_a_float(self): + def is_a_float_class(self): if self.about == "Float": return True simple_float = False @@ -422,7 +422,7 @@ def _parse_rdf(input_dic, version, lang_pack): profile_name = "" profile_uri_list = [] attributes = [] - instances = [] + enum_instances = [] global cim_namespace if not cim_namespace: @@ -442,7 +442,7 @@ def _parse_rdf(input_dic, version, lang_pack): if "property" in rdfs_entry_types: attributes.append(object_dic) if "rest_non_class_category" in rdfs_entry_types: - instances.append(object_dic) + enum_instances.append(object_dic) if "profile_name_v2_4" in rdfs_entry_types: profile_name = rdfsEntry.about() if "profile_name_v3" in rdfs_entry_types: @@ -467,13 +467,13 @@ def _parse_rdf(input_dic, version, lang_pack): else: logger.info("Class {} for attribute {} not found.".format(clarse, attribute)) - # Add instances to corresponding class - for instance in instances: + # Add enum instances to corresponding class + for instance in enum_instances: clarse = RDFSEntry._get_rid_of_hash(instance["type"]) if clarse and clarse in classes_map: - classes_map[clarse].addInstance(instance) + classes_map[clarse].addEnumInstance(instance) else: - logger.info("Class {} for instance {} not found.".format(clarse, instance)) + logger.info("Class {} for enum instance {} not found.".format(clarse, instance)) return {short_profile_name: classes_map} @@ -491,9 +491,9 @@ def _write_python_files(elem_dict, lang_pack, output_path, version): # Iterate over Classes for class_definition in elem_dict: - if elem_dict[class_definition].is_a_float(): + if elem_dict[class_definition].is_a_float_class(): float_classes[class_definition] = True - if elem_dict[class_definition].has_instances(): + if elem_dict[class_definition].is_an_enum_class(): enum_classes[class_definition] = True lang_pack.set_float_classes(float_classes) @@ -508,9 +508,9 @@ def _write_python_files(elem_dict, lang_pack, output_path, version): "class_location": lang_pack.get_class_location(class_name, elem_dict, version), "class_name": class_name, "class_origin": elem_dict[class_name].origins(), - "instances": elem_dict[class_name].instances(), - "has_instances": elem_dict[class_name].has_instances(), - "is_a_float": elem_dict[class_name].is_a_float(), + "enum_instances": elem_dict[class_name].enum_instances(), + "is_an_enum_class": elem_dict[class_name].is_an_enum_class(), + "is_a_float_class": elem_dict[class_name].is_a_float_class(), "langPack": lang_pack, "sub_class_of": elem_dict[class_name].superClass(), "sub_classes": elem_dict[class_name].subClasses(), diff --git a/cimgen/languages/cpp/lang_pack.py b/cimgen/languages/cpp/lang_pack.py index aeb5d434..f34bc341 100644 --- a/cimgen/languages/cpp/lang_pack.py +++ b/cimgen/languages/cpp/lang_pack.py @@ -59,9 +59,9 @@ def get_class_location(class_name, class_map, version): # This is the function that runs the template. def run_template(output_path, class_details): - if class_details["is_a_float"]: + if class_details["is_a_float_class"]: templates = float_template_files - elif class_details["has_instances"]: + elif class_details["is_an_enum_class"]: templates = enum_template_files else: templates = template_files diff --git a/cimgen/languages/cpp/templates/cpp_enum_header_template.mustache b/cimgen/languages/cpp/templates/cpp_enum_header_template.mustache index 6532028d..888ac3c6 100644 --- a/cimgen/languages/cpp/templates/cpp_enum_header_template.mustache +++ b/cimgen/languages/cpp/templates/cpp_enum_header_template.mustache @@ -7,12 +7,12 @@ namespace CIMPP { */ enum class {{class_name}} { -{{#instances}} +{{#enum_instances}} /** * {{comment}} */ {{label}}, -{{/instances}} +{{/enum_instances}} }; std::istream& operator>>(std::istream& lop, CIMPP::{{class_name}}& rop); } diff --git a/cimgen/languages/cpp/templates/cpp_enum_object_template.mustache b/cimgen/languages/cpp/templates/cpp_enum_object_template.mustache index e237da52..63eecaed 100644 --- a/cimgen/languages/cpp/templates/cpp_enum_object_template.mustache +++ b/cimgen/languages/cpp/templates/cpp_enum_object_template.mustache @@ -19,13 +19,13 @@ namespace CIMPP { EnumSymbol = EnumSymbol.substr(pos + 1); -{{#instances}} +{{#enum_instances}} if(EnumSymbol == "{{label}}") { rop = {{class_name}}::{{label}}; return lop; } -{{/instances}} +{{/enum_instances}} lop.setstate(std::ios::failbit); return lop; diff --git a/cimgen/languages/java/lang_pack.py b/cimgen/languages/java/lang_pack.py index 45162cca..e8de497a 100644 --- a/cimgen/languages/java/lang_pack.py +++ b/cimgen/languages/java/lang_pack.py @@ -51,9 +51,9 @@ def run_template(output_path, class_details): for attr in class_details["attributes"]: if attribute_type(attr) == "primitive": class_details["primitives"].append(attr) - if class_details["is_a_float"]: + if class_details["is_a_float_class"]: templates = float_template_files - elif class_details["has_instances"]: + elif class_details["is_an_enum_class"]: templates = enum_template_files else: templates = template_files diff --git a/cimgen/languages/java/templates/java_enum.mustache b/cimgen/languages/java/templates/java_enum.mustache index 5f8b418a..35027149 100644 --- a/cimgen/languages/java/templates/java_enum.mustache +++ b/cimgen/languages/java/templates/java_enum.mustache @@ -12,12 +12,12 @@ public class {{class_name}} extends BaseClass { private enum {{class_name}}_ENUM { - {{#instances}} + {{#enum_instances}} /** * {{comment}} */ {{label}}, - {{/instances}} + {{/enum_instances}} MAX_{{class_name}}_ENUM; } diff --git a/cimgen/languages/javascript/templates/handlebars_template.mustache b/cimgen/languages/javascript/templates/handlebars_template.mustache index 1cf9959e..c047eaeb 100644 --- a/cimgen/languages/javascript/templates/handlebars_template.mustache +++ b/cimgen/languages/javascript/templates/handlebars_template.mustache @@ -3,25 +3,25 @@ import {{sub_class_of}} from "./{{sub_class_of}}.js" import common from "../../src/common.js" import CGMESProfile from "./CGMESProfile.js" -{{#has_instances}} +{{#is_an_enum_class}} const {{class_name}}Enum = { -{{#instances}} + {{#enum_instances}} {{label}}:{{index}}, -{{/instances}} + {{/enum_instances}} } const possibleValues = [ { "value": "--"}, -{{#instances}} + {{#enum_instances}} { "value": "{{about}}", "label": "{{label}}" }, -{{/instances}} + {{/enum_instances}} ] -{{/has_instances}} +{{/is_an_enum_class}} class {{class_name}} extends {{sub_class_of}} { static attributeHTML(object, cimmenu, classType="{{class_name}}") { let attributeEntries = {{sub_class_of}}.attributeHTML(object, cimmenu, classType); - {{#attributes}} + {{#attributes}} if ('cim:{{about}}' in object) { attributeEntries['filledEntries']['cim:{{about}}'] = cimmenu.getAggregateComponentMenu( @@ -42,7 +42,7 @@ class {{class_name}} extends {{sub_class_of}} { 'cim:{{about}}' ); } - {{/attributes}} + {{/attributes}} return attributeEntries; } @@ -87,9 +87,9 @@ class {{class_name}} extends {{sub_class_of}} { {{{renderAttribute}}} static subClassList() { let subClasses = [ - {{#sub_classes}} + {{#sub_classes}} "{{.}}", - {{/sub_classes}} + {{/sub_classes}} ]; return subClasses; } From b3a4e1ddf18cf2fc3ad1afcd4c272d07608baeea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCnther?= Date: Sat, 28 Sep 2024 13:04:29 +0200 Subject: [PATCH 03/14] Add attribute properties: "add is_class_attribute", "is_enum_attribute", "is_list_attribute" and "is_primitive_attribute" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Günther --- cimgen/cimgen.py | 103 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 70 insertions(+), 33 deletions(-) diff --git a/cimgen/cimgen.py b/cimgen/cimgen.py index bc3e0711..df028c8c 100644 --- a/cimgen/cimgen.py +++ b/cimgen/cimgen.py @@ -50,7 +50,7 @@ def asJson(self, lang_pack): def about(self): if "$rdf:about" in self.jsonDefinition: - return RDFSEntry._get_rid_of_hash(RDFSEntry._get_about_or_resource(self.jsonDefinition["$rdf:about"])) + return _get_rid_of_hash(RDFSEntry._get_about_or_resource(self.jsonDefinition["$rdf:about"])) else: return None @@ -91,7 +91,7 @@ def dataType(self): def domain(self): if "rdfs:domain" in self.jsonDefinition: - return RDFSEntry._get_rid_of_hash(RDFSEntry._extract_string(self.jsonDefinition["rdfs:domain"])) + return _get_rid_of_hash(RDFSEntry._extract_string(self.jsonDefinition["rdfs:domain"])) else: return None @@ -115,7 +115,7 @@ def title(self): def inverseRole(self): if "cims:inverseRoleName" in self.jsonDefinition: - return RDFSEntry._get_rid_of_hash(RDFSEntry._extract_string(self.jsonDefinition["cims:inverseRoleName"])) + return _get_rid_of_hash(RDFSEntry._extract_string(self.jsonDefinition["cims:inverseRoleName"])) else: return None @@ -135,7 +135,7 @@ def label(self): def multiplicity(self): if "cims:multiplicity" in self.jsonDefinition: - return RDFSEntry._get_rid_of_hash(RDFSEntry._extract_string(self.jsonDefinition["cims:multiplicity"])) + return _get_rid_of_hash(RDFSEntry._extract_string(self.jsonDefinition["cims:multiplicity"])) else: return None @@ -165,7 +165,7 @@ def version_iri(self): def subClassOf(self): if "rdfs:subClassOf" in self.jsonDefinition: - return RDFSEntry._get_rid_of_hash(RDFSEntry._extract_string(self.jsonDefinition["rdfs:subClassOf"])) + return _get_rid_of_hash(RDFSEntry._extract_string(self.jsonDefinition["rdfs:subClassOf"])) else: return None @@ -209,16 +209,6 @@ def _get_about_or_resource(object_dic): return object_dic["$rdfs:Literal"] return object_dic - # Some names are encoded as #name or http://some-url#name - # This function returns the name - def _get_rid_of_hash(name): - tokens = name.split("#") - if len(tokens) == 1: - return tokens[0] - if len(tokens) > 1: - return tokens[1] - return name - class CIMComponentDefinition: def __init__(self, rdfsEntry): @@ -469,7 +459,7 @@ def _parse_rdf(input_dic, version, lang_pack): # Add enum instances to corresponding class for instance in enum_instances: - clarse = RDFSEntry._get_rid_of_hash(instance["type"]) + clarse = _get_rid_of_hash(instance["type"]) if clarse and clarse in classes_map: classes_map[clarse].addEnumInstance(instance) else: @@ -537,11 +527,22 @@ def _write_python_files(elem_dict, lang_pack, output_path, version): initial_indent="", subsequent_indent=" " * 6, ) + attribute_class = _get_attribute_class(attribute) + is_an_enum_class = attribute_class in elem_dict and elem_dict[attribute_class].is_an_enum_class() + attribute_type = _get_attribute_type(attribute, is_an_enum_class) + attribute["is_class_attribute"] = _get_bool_string(attribute_type == "class") + attribute["is_enum_attribute"] = _get_bool_string(attribute_type == "enum") + attribute["is_list_attribute"] = _get_bool_string(attribute_type == "list") + attribute["is_primitive_attribute"] = _get_bool_string(attribute_type == "primitive") + attribute["class_name"] = attribute_class + class_details["attributes"].sort(key=lambda d: d["label"]) _write_files(class_details, output_path, version) -def get_rid_of_hash(name): +# Some names are encoded as #name or http://some-url#name +# This function returns the name +def _get_rid_of_hash(name): tokens = name.split("#") if len(tokens) == 1: return tokens[0] @@ -550,13 +551,6 @@ def get_rid_of_hash(name): return name -def format_class(_range, _dataType): - if _range == "": - return get_rid_of_hash(_dataType) - else: - return get_rid_of_hash(_range) - - def _write_files(class_details, output_path, version): if class_details["sub_class_of"] is None: # If class has no subClassOf key it is a subclass of the Base class @@ -576,15 +570,6 @@ def _write_files(class_details, output_path, version): ): class_details["attributes"][i]["dataType"] = class_details["attributes"][i]["multiplicity"] - for attr in class_details["attributes"]: - _range = "" - _dataType = "" - if "range" in attr: - _range = attr["range"] - if "dataType" in attr: - _dataType = attr["dataType"] - attr["class_name"] = format_class(_range, _dataType) - class_details["langPack"].run_template(output_path, class_details) @@ -859,3 +844,55 @@ def _get_recommended_class_profiles(elem_dict): else: recommended_class_profiles[class_name] = _get_sorted_profile_keys(class_profiles)[0] return recommended_class_profiles + + +def _get_attribute_class(attribute: dict) -> str: + """Get the class name of an attribute. + + :param attribute: Dictionary with information about an attribute of a class. + :return: Class name of the attribute. + """ + name = attribute.get("range") or attribute.get("dataType") + return _get_rid_of_hash(name) + + +def _get_attribute_type(attribute: dict, is_an_enum_class: bool) -> str: + """Get the type of an attribute: "class", "enum", "list", or "primitive". + + :param attribute: Dictionary with information about an attribute of a class. + :param is_an_enum_class: Is this attribute an enumation? + :return: Type of the attribute. + """ + so_far_not_primitive = _get_attribute_class(attribute) in ( + "Date", + "DateTime", + "MonthDay", + "Status", + "StreetAddress", + "StreetDetail", + "TownDetail", + ) + attribute_type = "class" + if "dataType" in attribute and not so_far_not_primitive: + attribute_type = "primitive" + elif is_an_enum_class: + attribute_type = "enum" + elif attribute.get("multiplicity") in ("M:0..n", "M:1..n"): + attribute_type = "list" + return attribute_type + + +def _get_bool_string(bool_value: bool) -> str: + """Convert boolean value into a string which is usable in both Python and Json. + + Valid boolean values in Python are capitalized True/False. + But these values are not valid in Json. + Strings with value "true" and "" are recognized as True/False in both languages. + + :param bool_value: Valid boolean value. + :return: String "true" for True and "" for False. + """ + if bool_value: + return "true" + else: + return "" From 9d2985e75255720691ae52d7b96eb5f5d871685a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCnther?= Date: Sat, 28 Sep 2024 15:41:53 +0200 Subject: [PATCH 04/14] Replace attribute properties "isAssociationUsed" and "associationUsed" by "is_used" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Günther --- cimgen/cimgen.py | 13 ++----------- cimgen/languages/cpp/lang_pack.py | 6 +----- cimgen/languages/java/lang_pack.py | 6 +----- cimgen/languages/javascript/lang_pack.py | 11 +---------- .../templates/cimpy_class_template.mustache | 8 ++++---- 5 files changed, 9 insertions(+), 35 deletions(-) diff --git a/cimgen/cimgen.py b/cimgen/cimgen.py index df028c8c..0f41dffa 100644 --- a/cimgen/cimgen.py +++ b/cimgen/cimgen.py @@ -42,10 +42,7 @@ def asJson(self, lang_pack): jsonObject["subClassOf"] = self.subClassOf() if self.inverseRole() is not None: jsonObject["inverseRole"] = self.inverseRole() - if self.associationUsed() is not None: - jsonObject["associationUsed"] = self.associationUsed() - if "modernpython" in lang_pack.__name__: - jsonObject["isAssociationUsed"] = self.isAssociationUsed() + jsonObject["is_used"] = _get_bool_string(self.is_used()) return jsonObject def about(self): @@ -54,15 +51,9 @@ def about(self): else: return None - def associationUsed(self): - if "cims:AssociationUsed" in self.jsonDefinition: - return RDFSEntry._extract_string(self.jsonDefinition["cims:AssociationUsed"]) - else: - return None - # Capitalized True/False is valid in python but not in json. # Do not use this function in combination with json.load() - def isAssociationUsed(self) -> bool: + def is_used(self) -> bool: if "cims:AssociationUsed" in self.jsonDefinition: return "yes" == RDFSEntry._extract_string(self.jsonDefinition["cims:AssociationUsed"]).lower() else: diff --git a/cimgen/languages/cpp/lang_pack.py b/cimgen/languages/cpp/lang_pack.py index f34bc341..4105e3ea 100644 --- a/cimgen/languages/cpp/lang_pack.py +++ b/cimgen/languages/cpp/lang_pack.py @@ -257,11 +257,7 @@ def create_class_assign(text, render): .replace("ATTRIBUTE_CLASS", attribute_class) .replace("LABEL", attribute_json["label"]) ) - elif ( - "inverseRole" in attribute_json - and "associationUsed" in attribute_json - and attribute_json["associationUsed"] != "No" - ): + elif "inverseRole" in attribute_json and attribute_json["is_used"]: inverse = attribute_json["inverseRole"].split(".") assign = ( """ diff --git a/cimgen/languages/java/lang_pack.py b/cimgen/languages/java/lang_pack.py index e8de497a..4ab2e3c1 100644 --- a/cimgen/languages/java/lang_pack.py +++ b/cimgen/languages/java/lang_pack.py @@ -199,11 +199,7 @@ def create_class_assign(text, render): .replace("ATTRIBUTE_CLASS", attribute_class) .replace("LABEL", attribute_json["label"]) ) - elif ( - "inverseRole" in attribute_json - and "associationUsed" in attribute_json - and attribute_json["associationUsed"] != "No" - ): + elif "inverseRole" in attribute_json and attribute_json["is_used"]: inverse = attribute_json["inverseRole"].split(".") assign = ( """ diff --git a/cimgen/languages/javascript/lang_pack.py b/cimgen/languages/javascript/lang_pack.py index 3b3011dc..a092b77d 100644 --- a/cimgen/languages/javascript/lang_pack.py +++ b/cimgen/languages/javascript/lang_pack.py @@ -140,7 +140,7 @@ def run_template(output_path, class_details): attr["attributeClass"] = _get_rid_of_hash(attr["dataType"]) for index, attribute in enumerate(class_details["attributes"]): - if is_an_unused_attribute(attribute): + if not attribute["is_used"]: del class_details["attributes"][index] renderAttribute = "" @@ -191,15 +191,6 @@ def _create_cgmes_profile(output_path: str, profile_details: list, cim_namespace _write_templated_file(class_file, class_details, template_info["filename"]) -def is_an_unused_attribute(attr_details, debug=False): - is_unused = ( - "inverseRole" in attr_details and "associationUsed" in attr_details and attr_details["associationUsed"] == "No" - ) - if debug and is_unused: - print(attr_details["about"], " is_unused: ", is_unused) - return is_unused - - def attribute_type(class_details): class_name = class_details["class_name"] if is_a_float_class(class_name) or class_name == "String" or class_name == "Boolean" or class_name == "Integer": diff --git a/cimgen/languages/modernpython/templates/cimpy_class_template.mustache b/cimgen/languages/modernpython/templates/cimpy_class_template.mustache index acf2c8f2..b58bb082 100644 --- a/cimgen/languages/modernpython/templates/cimpy_class_template.mustache +++ b/cimgen/languages/modernpython/templates/cimpy_class_template.mustache @@ -23,7 +23,7 @@ class {{class_name}}({{sub_class_of}}): """ {{#attributes}} - {{#isAssociationUsed}} + {{#is_used}} {{label}}: {{#setType}}{{dataType}}{{/setType}} = Field( {{#setDefault}}{{dataType}}{{/setDefault}}, json_schema_extra={ @@ -34,12 +34,12 @@ class {{class_name}}({{sub_class_of}}): ] }, ) - {{/isAssociationUsed}} - {{^isAssociationUsed}} + {{/is_used}} + {{^is_used}} # *Association not used* # Type {{dataType}} in CIM # {{label}}: {{#setType}}{{dataType}}{{/setType}} = Field({{#setDefault}}{{dataType}}{{/setDefault}}, json_schema_extra={"in_profiles": [{{#attr_origin}}Profile.{{origin}}, {{/attr_origin}}]}) # noqa: E501 - {{/isAssociationUsed}} + {{/is_used}} {{/attributes}} {{^attributes}} From 8cacbd3a496253bf1f2c6c18e48d7650bbcd2dad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCnther?= Date: Sat, 28 Sep 2024 17:30:44 +0200 Subject: [PATCH 05/14] Rename attribute property "class_name" to "attribute_class" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Günther --- cimgen/cimgen.py | 2 +- cimgen/languages/cpp/lang_pack.py | 12 ++++++------ .../cpp/templates/cpp_object_template.mustache | 2 +- cimgen/languages/java/lang_pack.py | 12 ++++++------ .../java/templates/java_class.mustache | 2 +- cimgen/languages/javascript/lang_pack.py | 17 ----------------- .../templates/handlebars_template.mustache | 4 ++-- 7 files changed, 17 insertions(+), 34 deletions(-) diff --git a/cimgen/cimgen.py b/cimgen/cimgen.py index 0f41dffa..23dafc49 100644 --- a/cimgen/cimgen.py +++ b/cimgen/cimgen.py @@ -525,7 +525,7 @@ def _write_python_files(elem_dict, lang_pack, output_path, version): attribute["is_enum_attribute"] = _get_bool_string(attribute_type == "enum") attribute["is_list_attribute"] = _get_bool_string(attribute_type == "list") attribute["is_primitive_attribute"] = _get_bool_string(attribute_type == "primitive") - attribute["class_name"] = attribute_class + attribute["attribute_class"] = attribute_class class_details["attributes"].sort(key=lambda d: d["label"]) _write_files(class_details, output_path, version) diff --git a/cimgen/languages/cpp/lang_pack.py b/cimgen/languages/cpp/lang_pack.py index 4105e3ea..7979d8e9 100644 --- a/cimgen/languages/cpp/lang_pack.py +++ b/cimgen/languages/cpp/lang_pack.py @@ -112,7 +112,7 @@ def label(text, render): # - attributes with multiplicity of 1..n or 0..n will be std::lists # of pointers to classes read from a different part of the file def attribute_type(attribute): - class_name = attribute["class_name"] + class_name = attribute["attribute_class"] if attribute["multiplicity"] == "M:0..n" or attribute["multiplicity"] == "M:1..n": return "list" if ( @@ -237,7 +237,7 @@ def create_class_assign(text, render): attribute_txt = render(text) attribute_json = eval(attribute_txt) assign = "" - attribute_class = attribute_json["class_name"] + attribute_class = attribute_json["attribute_class"] if attribute_type(attribute_json) == "primitive": return "" if attribute_json["multiplicity"] == "M:0..n" or attribute_json["multiplicity"] == "M:1..n": @@ -301,7 +301,7 @@ def create_assign(text, render): attribute_txt = render(text) attribute_json = eval(attribute_txt) assign = "" - _class = attribute_json["class_name"] + _class = attribute_json["attribute_class"] if not attribute_type(attribute_json) == "primitive": return "" label_without_keyword = attribute_json["label"] @@ -366,7 +366,7 @@ def attribute_decl(text, render): def _attribute_decl(attribute): _type = attribute_type(attribute) - _class = attribute["class_name"] + _class = attribute["attribute_class"] if _type == "primitive": return "CIMPP::" + _class if _type == "list": @@ -385,7 +385,7 @@ def _create_attribute_includes(text, render): attributes = json.loads(jsonStringNoHtmlEsc) for attribute in attributes: _type = attribute_type(attribute) - class_name = attribute["class_name"] + class_name = attribute["attribute_class"] if class_name != "" and class_name not in unique: unique[class_name] = _type for clarse in unique: @@ -405,7 +405,7 @@ def _create_attribute_class_declarations(text, render): attributes = json.loads(jsonStringNoHtmlEsc) for attribute in attributes: _type = attribute_type(attribute) - class_name = attribute["class_name"] + class_name = attribute["attribute_class"] if class_name != "" and class_name not in unique: unique[class_name] = _type for clarse in unique: diff --git a/cimgen/languages/cpp/templates/cpp_object_template.mustache b/cimgen/languages/cpp/templates/cpp_object_template.mustache index 529623c4..ae60165d 100644 --- a/cimgen/languages/cpp/templates/cpp_object_template.mustache +++ b/cimgen/languages/cpp/templates/cpp_object_template.mustache @@ -3,7 +3,7 @@ #include "{{class_name}}.hpp" {{#attributes}} -#include "{{class_name}}.hpp" +#include "{{attribute_class}}.hpp" {{/attributes}} using namespace CIMPP; diff --git a/cimgen/languages/java/lang_pack.py b/cimgen/languages/java/lang_pack.py index 4ab2e3c1..2257b0ce 100644 --- a/cimgen/languages/java/lang_pack.py +++ b/cimgen/languages/java/lang_pack.py @@ -104,7 +104,7 @@ def label(text, render): # - attributes with multiplicity of 1..n or 0..n will be std::lists # of pointers to classes read from a different part of the file def attribute_type(attribute): - class_name = attribute["class_name"] + class_name = attribute["attribute_class"] if attribute["multiplicity"] == "M:0..n" or attribute["multiplicity"] == "M:1..n": return "list" if ( @@ -182,7 +182,7 @@ def create_class_assign(text, render): attribute_txt = render(text) attribute_json = eval(attribute_txt) assign = "" - attribute_class = attribute_json["class_name"] + attribute_class = attribute_json["attribute_class"] if attribute_type(attribute_json) == "primitive": return "" if attribute_json["multiplicity"] == "M:0..n" or attribute_json["multiplicity"] == "M:1..n": @@ -237,7 +237,7 @@ def create_assign(text, render): attribute_txt = render(text) attribute_json = eval(attribute_txt) assign = "" - _class = attribute_json["class_name"] + _class = attribute_json["attribute_class"] if not attribute_type(attribute_json) == "primitive": return "" label_without_keyword = attribute_json["label"] @@ -286,7 +286,7 @@ def attribute_decl(text, render): def _attribute_decl(attribute): _type = attribute_type(attribute) - _class = attribute["class_name"] + _class = attribute["attribute_class"] if _type == "primitive": return _class if _type == "list": @@ -305,7 +305,7 @@ def _create_attribute_includes(text, render): attributes = json.loads(jsonStringNoHtmlEsc) for attribute in attributes: _type = attribute_type(attribute) - class_name = attribute["class_name"] + class_name = attribute["attribute_class"] if class_name != "" and class_name not in unique: unique[class_name] = _type for clarse in unique: @@ -326,7 +326,7 @@ def _create_attribute_class_declarations(text, render): attributes = json.loads(jsonStringNoHtmlEsc) for attribute in attributes: _type = attribute_type(attribute) - class_name = attribute["class_name"] + class_name = attribute["attribute_class"] if class_name != "" and class_name not in unique: unique[class_name] = _type for clarse in unique: diff --git a/cimgen/languages/java/templates/java_class.mustache b/cimgen/languages/java/templates/java_class.mustache index d176d522..c65050d3 100644 --- a/cimgen/languages/java/templates/java_class.mustache +++ b/cimgen/languages/java/templates/java_class.mustache @@ -32,7 +32,7 @@ public class {{class_name}} extends {{sub_class_of}} {{#primitives}} {{> label}}(){ public BaseClass construct (java.lang.String value) { - return new {{class_name}}(value); + return new {{attribute_class}}(value); } }, {{/primitives}} diff --git a/cimgen/languages/javascript/lang_pack.py b/cimgen/languages/javascript/lang_pack.py index a092b77d..453d7506 100644 --- a/cimgen/languages/javascript/lang_pack.py +++ b/cimgen/languages/javascript/lang_pack.py @@ -132,13 +132,6 @@ def selectPrimitiveRenderFunction(primitive): # This is the function that runs the template. def run_template(output_path, class_details): - class_details["is_not_terminal"] = class_details["class_name"] != "Terminal" - for attr in class_details["attributes"]: - if "range" in attr: - attr["attributeClass"] = _get_rid_of_hash(attr["range"]) - elif "dataType" in attr: - attr["attributeClass"] = _get_rid_of_hash(attr["dataType"]) - for index, attribute in enumerate(class_details["attributes"]): if not attribute["is_used"]: del class_details["attributes"][index] @@ -200,15 +193,5 @@ def attribute_type(class_details): return "class" -# This function returns the name -def _get_rid_of_hash(name): - tokens = name.split("#") - if len(tokens) == 1: - return tokens[0] - if len(tokens) > 1: - return tokens[1] - return name - - def resolve_headers(path: str, version: str): # NOSONAR pass diff --git a/cimgen/languages/javascript/templates/handlebars_template.mustache b/cimgen/languages/javascript/templates/handlebars_template.mustache index c047eaeb..5732b091 100644 --- a/cimgen/languages/javascript/templates/handlebars_template.mustache +++ b/cimgen/languages/javascript/templates/handlebars_template.mustache @@ -28,7 +28,7 @@ class {{class_name}} extends {{sub_class_of}} { 'cim:'+classType, object['pintura:rdfid'], object['{{about}}'], - 'cim:{{attributeClass}}', + 'cim:{{attribute_class}}', 'cim:{{about}}' ); } @@ -38,7 +38,7 @@ class {{class_name}} extends {{sub_class_of}} { 'cim:'+classType, object['pintura:rdfid'], object['{{about}}'], - 'cim:{{attributeClass}}', + 'cim:{{attribute_class}}', 'cim:{{about}}' ); } From 11ae4e24ac3dedfe17616ca403b8ad6556b73c48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCnther?= Date: Sat, 28 Sep 2024 18:18:35 +0200 Subject: [PATCH 06/14] Improve cpp/java using the new attributes, remove unused functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Günther --- cimgen/cimgen.py | 13 -- cimgen/languages/cpp/lang_pack.py | 127 +++----------- cimgen/languages/java/lang_pack.py | 187 ++------------------- cimgen/languages/javascript/lang_pack.py | 73 +++----- cimgen/languages/modernpython/lang_pack.py | 12 +- cimgen/languages/python/lang_pack.py | 8 - 6 files changed, 61 insertions(+), 359 deletions(-) diff --git a/cimgen/cimgen.py b/cimgen/cimgen.py index 23dafc49..e5214011 100644 --- a/cimgen/cimgen.py +++ b/cimgen/cimgen.py @@ -467,19 +467,6 @@ def _write_python_files(elem_dict, lang_pack, output_path, version): # Setup called only once: make output directory, create base class, create profile class, etc. lang_pack.setup(output_path, _get_profile_details(package_listed_by_short_name), cim_namespace) - float_classes = {} - enum_classes = {} - - # Iterate over Classes - for class_definition in elem_dict: - if elem_dict[class_definition].is_a_float_class(): - float_classes[class_definition] = True - if elem_dict[class_definition].is_an_enum_class(): - enum_classes[class_definition] = True - - lang_pack.set_float_classes(float_classes) - lang_pack.set_enum_classes(enum_classes) - recommended_class_profiles = _get_recommended_class_profiles(elem_dict) for class_name in elem_dict.keys(): diff --git a/cimgen/languages/cpp/lang_pack.py b/cimgen/languages/cpp/lang_pack.py index 7979d8e9..09d73a97 100644 --- a/cimgen/languages/cpp/lang_pack.py +++ b/cimgen/languages/cpp/lang_pack.py @@ -4,8 +4,8 @@ from importlib.resources import files -def location(version): - return "BaseClass.hpp" +def location(version): # NOSONAR + return "" # Setup called only once: make output directory, create base class, create profile class, etc. @@ -13,7 +13,7 @@ def location(version): # cgmes_profile_details contains index, names and uris for each profile. # We don't use that here because we aren't exporting into # separate profiles. -def setup(output_path: str, cgmes_profile_details: list, cim_namespace: str): +def setup(output_path: str, cgmes_profile_details: list, cim_namespace: str): # NOSONAR if not os.path.exists(output_path): os.makedirs(output_path) else: @@ -41,18 +41,15 @@ def setup(output_path: str, cgmes_profile_details: list, cim_namespace: str): ] -def get_class_location(class_name, class_map, version): - pass +def get_class_location(class_name, class_map, version): # NOSONAR + return "" partials = { "attribute": "{{#langPack.attribute_decl}}{{.}}{{/langPack.attribute_decl}}", "label": "{{#langPack.label}}{{label}}{{/langPack.label}}", - "create_init_list": "{{#langPack.null_init_list}}{{attributes}}{{/langPack.null_init_list}}", - "create_construct_list": "{{#langPack.create_construct_list}}{{attributes}}{{/langPack.create_construct_list}}", "insert_assign": "{{#langPack.insert_assign_fn}}{{.}}{{/langPack.insert_assign_fn}}", "insert_class_assign": "{{#langPack.insert_class_assign_fn}}{{.}}{{/langPack.insert_class_assign_fn}}", - "read_istream": "{{#langPack.create_istream_op}}{{class_name}} {{label}}{{/langPack.create_istream_op}}", } @@ -104,59 +101,6 @@ def label(text, render): return result -# This function determines how the attribute code will be implemented. -# - attributes which are primitives will be read from the file and -# stored in the class object we are reading at the time -# - attributes which are classes will be stored as pointers to the -# actual class, which will be read from another part of the file. -# - attributes with multiplicity of 1..n or 0..n will be std::lists -# of pointers to classes read from a different part of the file -def attribute_type(attribute): - class_name = attribute["attribute_class"] - if attribute["multiplicity"] == "M:0..n" or attribute["multiplicity"] == "M:1..n": - return "list" - if ( - is_a_float_class(class_name) - or class_name == "String" - or class_name == "Boolean" - or class_name == "Integer" - or is_an_enum_class(class_name) - ): - return "primitive" - else: - return "class" - - -# We need to keep track of which class types are secretly float -# primitives. We will use a different template to create the class -# definitions, and we will read them from the file directly into -# an attribute instead of creating a class. -float_classes = {} - - -def set_float_classes(new_float_classes): - for new_class in new_float_classes: - float_classes[new_class] = new_float_classes[new_class] - - -def is_a_float_class(name): - if name in float_classes: - return float_classes[name] - - -enum_classes = {} - - -def set_enum_classes(new_enum_classes): - for new_class in new_enum_classes: - enum_classes[new_class] = new_enum_classes[new_class] - - -def is_an_enum_class(name): - if name in enum_classes: - return enum_classes[name] - - # These insert_ functions are used to generate the entries in the dynamic_switch # maps, for use in assignments.cpp and Task.cpp # TODO: implement this as one function, determine in template if it should be called. @@ -164,7 +108,7 @@ def is_an_enum_class(name): def insert_assign_fn(text, render): attribute_txt = render(text) attribute_json = eval(attribute_txt) - if not attribute_type(attribute_json) == "primitive": + if not (attribute_json["is_primitive_attribute"] or attribute_json["is_enum_attribute"]): return "" label = attribute_json["label"] class_name = attribute_json["domain"] @@ -184,7 +128,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_type(attribute_json) == "primitive": + if attribute_json["is_primitive_attribute"] or attribute_json["is_enum_attribute"]: return "" label = attribute_json["label"] class_name = attribute_json["domain"] @@ -201,15 +145,6 @@ def insert_class_assign_fn(text, render): ) -def get_dataType_and_range(attribute): - _range = _dataType = "" - if "range" in attribute: - _range = attribute["range"] - if "dataType" in attribute: - _dataType = attribute["dataType"] - return (_range, _dataType) - - def create_nullptr_assigns(text, render): attributes_txt = render(text) if attributes_txt.strip() == "": @@ -218,11 +153,7 @@ def create_nullptr_assigns(text, render): attributes_json = eval(attributes_txt) nullptr_init_string = ": " for attribute in attributes_json: - if attribute_type(attribute) == "primitive": - continue - if attribute["multiplicity"] == "M:0..n" or attribute["multiplicity"] == "M:1..n": - continue - else: + if attribute["is_class_attribute"]: nullptr_init_string += "LABEL(nullptr), ".replace("LABEL", attribute["label"]) if len(nullptr_init_string) > 2: @@ -238,9 +169,9 @@ def create_class_assign(text, render): attribute_json = eval(attribute_txt) assign = "" attribute_class = attribute_json["attribute_class"] - if attribute_type(attribute_json) == "primitive": + if attribute_json["is_primitive_attribute"] or attribute_json["is_enum_attribute"]: return "" - if attribute_json["multiplicity"] == "M:0..n" or attribute_json["multiplicity"] == "M:1..n": + if attribute_json["is_list_attribute"]: assign = ( """ bool assign_OBJECT_CLASS_LABEL(BaseClass* BaseClass_ptr1, BaseClass* BaseClass_ptr2) { @@ -302,7 +233,7 @@ def create_assign(text, render): attribute_json = eval(attribute_txt) assign = "" _class = attribute_json["attribute_class"] - if not attribute_type(attribute_json) == "primitive": + if not (attribute_json["is_primitive_attribute"] or attribute_json["is_enum_attribute"]): return "" label_without_keyword = attribute_json["label"] if label_without_keyword == "switch": @@ -347,17 +278,6 @@ def create_assign(text, render): return assign -# Some names are encoded as #name or http://some-url#name -# This function returns the name -def _get_rid_of_hash(name): - tokens = name.split("#") - if len(tokens) == 1: - return tokens[0] - if len(tokens) > 1: - return tokens[1] - return name - - def attribute_decl(text, render): attribute_txt = render(text) attribute_json = eval(attribute_txt) @@ -365,11 +285,10 @@ def attribute_decl(text, render): def _attribute_decl(attribute): - _type = attribute_type(attribute) _class = attribute["attribute_class"] - if _type == "primitive": + if attribute["is_primitive_attribute"] or attribute["is_enum_attribute"]: return "CIMPP::" + _class - if _type == "list": + if attribute["is_list_attribute"]: return "std::list" else: return "CIMPP::" + _class + "*" @@ -384,14 +303,10 @@ def _create_attribute_includes(text, render): if jsonStringNoHtmlEsc is not None and jsonStringNoHtmlEsc != "": attributes = json.loads(jsonStringNoHtmlEsc) for attribute in attributes: - _type = attribute_type(attribute) - class_name = attribute["attribute_class"] - if class_name != "" and class_name not in unique: - unique[class_name] = _type + if attribute["is_primitive_attribute"] or attribute["is_enum_attribute"]: + unique[attribute["attribute_class"]] = True for clarse in unique: - if unique[clarse] == "primitive": - include_string += '\n#include "' + clarse + '.hpp"' - + include_string += '\n#include "' + clarse + '.hpp"' return include_string @@ -404,14 +319,10 @@ def _create_attribute_class_declarations(text, render): if jsonStringNoHtmlEsc is not None and jsonStringNoHtmlEsc != "": attributes = json.loads(jsonStringNoHtmlEsc) for attribute in attributes: - _type = attribute_type(attribute) - class_name = attribute["attribute_class"] - if class_name != "" and class_name not in unique: - unique[class_name] = _type + if attribute["is_class_attribute"] or attribute["is_list_attribute"]: + unique[attribute["attribute_class"]] = True for clarse in unique: - if unique[clarse] == "class" or unique[clarse] == "list": - include_string += "\nclass " + clarse + ";" - + include_string += "\nclass " + clarse + ";" return include_string diff --git a/cimgen/languages/java/lang_pack.py b/cimgen/languages/java/lang_pack.py index 2257b0ce..66ac565d 100644 --- a/cimgen/languages/java/lang_pack.py +++ b/cimgen/languages/java/lang_pack.py @@ -4,16 +4,16 @@ from importlib.resources import files -def location(version): - return "BaseClass" +def location(version): # NOSONAR + return "" # Setup called only once: make output directory, create base class, create profile class, etc. # This just makes sure we have somewhere to write the classes. -# cgmes_profile_details contains index, names und uris for each profile. +# cgmes_profile_details contains index, names and uris for each profile. # We don't use that here because we aren't exporting into # separate profiles. -def setup(output_path: str, cgmes_profile_details: list, cim_namespace: str): +def setup(output_path: str, cgmes_profile_details: list, cim_namespace: str): # NOSONAR if not os.path.exists(output_path): os.makedirs(output_path) else: @@ -32,15 +32,13 @@ def setup(output_path: str, cgmes_profile_details: list, cim_namespace: str): enum_template_files = [{"filename": "java_enum.mustache", "ext": ".java"}] -def get_class_location(class_name, class_map, version): - pass +def get_class_location(class_name, class_map, version): # NOSONAR + return "" partials = { "attribute": "{{#langPack.attribute_decl}}{{.}}{{/langPack.attribute_decl}}", "label": "{{#langPack.label}}{{label}}{{/langPack.label}}", - "insert_assign": "{{#langPack.insert_assign_fn}}{{.}}{{/langPack.insert_assign_fn}}", - "insert_class_assign": "{{#langPack.insert_class_assign_fn}}{{.}}{{/langPack.insert_class_assign_fn}}", } @@ -49,8 +47,9 @@ def run_template(output_path, class_details): class_details["primitives"] = [] for attr in class_details["attributes"]: - if attribute_type(attr) == "primitive": + if attr["is_primitive_attribute"] or attr["is_enum_attribute"]: class_details["primitives"].append(attr) + if class_details["is_a_float_class"]: templates = float_template_files elif class_details["is_an_enum_class"]: @@ -96,141 +95,11 @@ def label(text, render): return result -# This function determines how the attribute code will be implemented. -# - attributes which are primitives will be read from the file and -# stored in the class object we are reading at the time -# - attributes which are classes will be stored as pointers to the -# actual class, which will be read from another part of the file. -# - attributes with multiplicity of 1..n or 0..n will be std::lists -# of pointers to classes read from a different part of the file -def attribute_type(attribute): - class_name = attribute["attribute_class"] - if attribute["multiplicity"] == "M:0..n" or attribute["multiplicity"] == "M:1..n": - return "list" - if ( - is_a_float_class(class_name) - or class_name == "String" - or class_name == "Boolean" - or class_name == "Integer" - or is_an_enum_class(class_name) - ): - return "primitive" - else: - return "class" - - -# We need to keep track of which class types are secretly float -# primitives. We will use a different template to create the class -# definitions, and we will read them from the file directly into -# an attribute instead of creating a class. -float_classes = {} - - -def set_float_classes(new_float_classes): - for new_class in new_float_classes: - float_classes[new_class] = new_float_classes[new_class] - - -def is_a_float_class(name): - if name in float_classes: - return float_classes[name] - - -enum_classes = {} - - -def set_enum_classes(new_enum_classes): - for new_class in new_enum_classes: - enum_classes[new_class] = new_enum_classes[new_class] - - -def is_an_enum_class(name): - if name in enum_classes: - return enum_classes[name] - - -# These insert_ functions are used to generate the entries in the dynamic_switch -# maps, for use in assignments.cpp and Task.cpp -# TODO: implement this as one function, determine in template if it should be called. -# TODO: reorganize json object so we don't have to keep doing the same processing. -def insert_assign_fn(text, render): - attribute_txt = render(text) - attribute_json = eval(attribute_txt) - primitive = attribute_type(attribute_json) == "primitive" - label = attribute_json["label"] - class_name = attribute_json["domain"] - if primitive: - return "OUTPUT FROM insert_assign_fn" + label + " in " + class_name + "\n" - else: - return "primitive OUTPUT FROM insert_assign_fn" + label + " in " + class_name + "\n" - - -def get_dataType_and_range(attribute): - _range = _dataType = "" - if "range" in attribute: - _range = attribute["range"] - if "dataType" in attribute: - _dataType = attribute["dataType"] - return (_range, _dataType) - - # These create_ functions are used to generate the implementations for # the entries in the dynamic_switch maps referenced in assignments.cpp and Task.cpp def create_class_assign(text, render): # TODO REMOVE: return "" - attribute_txt = render(text) - attribute_json = eval(attribute_txt) - assign = "" - attribute_class = attribute_json["attribute_class"] - if attribute_type(attribute_json) == "primitive": - return "" - if attribute_json["multiplicity"] == "M:0..n" or attribute_json["multiplicity"] == "M:1..n": - assign = ( - """ - OUTPUT FROM create_class_assign case 1 - with Label as LABEL - and Object Class Label as OBJECT_CLASS_LABEL - and Object Class as OBJECT_CLASS - and Attribute Class as ATTRIBUTE_CLASS - """.replace( - "OBJECT_CLASS", attribute_json["domain"] - ) - .replace("ATTRIBUTE_CLASS", attribute_class) - .replace("LABEL", attribute_json["label"]) - ) - elif "inverseRole" in attribute_json and attribute_json["is_used"]: - inverse = attribute_json["inverseRole"].split(".") - assign = ( - """ - OUTPUT FROM create_class_assign case 2 - with Object Class Label as OBJECT_CLASS_LABEL - and Object Class as OBJECT_CLASS - and Attribute Class as ATTRIBUTE_CLASS - and Inversec as INVERSEC - and Inversel as INVERSEL - """.replace( # noqa: E101,W191 - "OBJECT_CLASS", attribute_json["domain"] - ) - .replace("ATTRIBUTE_CLASS", attribute_class) - .replace("LABEL", attribute_json["label"]) - .replace("INVERSEC", inverse[0]) - .replace("INVERSEL", inverse[1]) - ) - else: - assign = ( - """ - OUTPUT FROM create_class_assign case 3 - with Label as LABEL - and Object Class as OBJECT_CLASS - and Attribute Class as ATTRIBUTE_CLASS - """.replace( # noqa: E101,W191 - "OBJECT_CLASS", attribute_json["domain"] - ) - .replace("ATTRIBUTE_CLASS", attribute_class) - .replace("LABEL", attribute_json["label"]) - ) - return assign def create_assign(text, render): @@ -238,7 +107,7 @@ def create_assign(text, render): attribute_json = eval(attribute_txt) assign = "" _class = attribute_json["attribute_class"] - if not attribute_type(attribute_json) == "primitive": + if not (attribute_json["is_primitive_attribute"] or attribute_json["is_enum_attribute"]): return "" label_without_keyword = attribute_json["label"] if label_without_keyword == "switch": @@ -267,17 +136,6 @@ def create_assign(text, render): return assign -# Some names are encoded as #name or http://some-url#name -# This function returns the name -def _get_rid_of_hash(name): - tokens = name.split("#") - if len(tokens) == 1: - return tokens[0] - if len(tokens) > 1: - return tokens[1] - return name - - def attribute_decl(text, render): attribute_txt = render(text) attribute_json = eval(attribute_txt) @@ -285,11 +143,10 @@ def attribute_decl(text, render): def _attribute_decl(attribute): - _type = attribute_type(attribute) _class = attribute["attribute_class"] - if _type == "primitive": + if attribute["is_primitive_attribute"] or attribute["is_enum_attribute"]: return _class - if _type == "list": + if attribute["is_list_attribute"]: return "List<" + _class + ">" else: return _class @@ -304,15 +161,11 @@ def _create_attribute_includes(text, render): if jsonStringNoHtmlEsc is not None and jsonStringNoHtmlEsc != "": attributes = json.loads(jsonStringNoHtmlEsc) for attribute in attributes: - _type = attribute_type(attribute) - class_name = attribute["attribute_class"] - if class_name != "" and class_name not in unique: - unique[class_name] = _type + if attribute["is_primitive_attribute"] or attribute["is_enum_attribute"]: + unique[attribute["attribute_class"]] = True for clarse in unique: - if unique[clarse] == "primitive": - if clarse != "String": - include_string += "\nimport cim4j." + clarse + ";" - + if clarse != "String": + include_string += "\nimport cim4j." + clarse + ";" return include_string @@ -325,14 +178,10 @@ def _create_attribute_class_declarations(text, render): if jsonStringNoHtmlEsc is not None and jsonStringNoHtmlEsc != "": attributes = json.loads(jsonStringNoHtmlEsc) for attribute in attributes: - _type = attribute_type(attribute) - class_name = attribute["attribute_class"] - if class_name != "" and class_name not in unique: - unique[class_name] = _type + if attribute["is_class_attribute"] or attribute["is_list_attribute"]: + unique[attribute["attribute_class"]] = True for clarse in unique: - if unique[clarse] == "class" or unique[clarse] == "list": - include_string += "\nimport cim4j." + clarse + ";" - + include_string += "\nimport cim4j." + clarse + ";" return include_string diff --git a/cimgen/languages/javascript/lang_pack.py b/cimgen/languages/javascript/lang_pack.py index 453d7506..199830ea 100644 --- a/cimgen/languages/javascript/lang_pack.py +++ b/cimgen/languages/javascript/lang_pack.py @@ -4,13 +4,13 @@ from importlib.resources import files -def location(version): - return "BaseClass.hpp" +def location(version): # NOSONAR + return "" # Setup called only once: make output directory, create base class, create profile class, etc. # This function makes sure we have somewhere to write the classes. -# cgmes_profile_details contains index, names und uris for each profile. +# cgmes_profile_details contains index, names and uris for each profile. # We use that to create the header data for the profiles. def setup(output_path: str, cgmes_profile_details: list, cim_namespace: str): if not os.path.exists(output_path): @@ -32,38 +32,8 @@ def setup(output_path: str, cgmes_profile_details: list, cim_namespace: str): partials = {} -# We need to keep track of which class types are secretly float -# primitives. We will use a different template to create the class -# definitions, and we will read them from the file directly into -# an attribute instead of creating a class. -float_classes = {} - - -def set_float_classes(new_float_classes): - for new_class in new_float_classes: - float_classes[new_class] = new_float_classes[new_class] - - -def is_a_float_class(name): - if name in float_classes: - return float_classes[name] - - -enum_classes = {} - - -def set_enum_classes(new_enum_classes): - for new_class in new_enum_classes: - enum_classes[new_class] = new_enum_classes[new_class] - - -def is_an_enum_class(name): - if name in enum_classes: - return enum_classes[name] - - -def get_class_location(class_name, class_map, version): - pass +def get_class_location(class_name, class_map, version): # NOSONAR + return "" aggregateRenderer = { @@ -103,27 +73,28 @@ def get_class_location(class_name, class_map, version): } -def selectPrimitiveRenderFunction(primitive): +def selectPrimitiveRenderFunction(class_details): + class_name = class_details["class_name"] render = "" - if is_a_float_class(primitive): + if class_details["is_a_float_class"]: render = aggregateRenderer["renderFloat"] - elif primitive == "String": + elif class_name == "String": render = aggregateRenderer["renderString"] - elif primitive == "Boolean": + elif class_name == "Boolean": render = aggregateRenderer["renderBoolean"] - elif primitive == "Date": + elif class_name == "Date": # TODO: Implementation Required! render = aggregateRenderer["renderString"] - elif primitive == "DateTime": + elif class_name == "DateTime": # TODO: Implementation Required! render = aggregateRenderer["renderString"] - elif primitive == "Decimal": + elif class_name == "Decimal": # TODO: Implementation Required! render = aggregateRenderer["renderString"] - elif primitive == "Integer": + elif class_name == "Integer": # TODO: Implementation Required! render = aggregateRenderer["renderString"] - elif primitive == "MonthDay": + elif class_name == "MonthDay": # TODO: Implementation Required! render = aggregateRenderer["renderString"] return render @@ -137,11 +108,11 @@ def run_template(output_path, class_details): del class_details["attributes"][index] renderAttribute = "" - attrType = attribute_type(class_details) - if attrType == "enum": + class_type = _get_class_type(class_details) + if class_type == "enum": renderAttribute = aggregateRenderer["renderInstance"] - elif attrType == "primitive": - renderAttribute = selectPrimitiveRenderFunction(class_details["class_name"]) + elif class_type == "primitive": + renderAttribute = selectPrimitiveRenderFunction(class_details) else: renderAttribute = aggregateRenderer["renderClass"] if renderAttribute == "": @@ -184,11 +155,11 @@ def _create_cgmes_profile(output_path: str, profile_details: list, cim_namespace _write_templated_file(class_file, class_details, template_info["filename"]) -def attribute_type(class_details): +def _get_class_type(class_details): class_name = class_details["class_name"] - if is_a_float_class(class_name) or class_name == "String" or class_name == "Boolean" or class_name == "Integer": + if class_details["is_a_float_class"] or class_name in ("String", "Boolean", "Integer"): return "primitive" - if is_an_enum_class(class_name): + if class_details["is_an_enum_class"]: return "enum" return "class" diff --git a/cimgen/languages/modernpython/lang_pack.py b/cimgen/languages/modernpython/lang_pack.py index a5b96e6b..c4f9e836 100644 --- a/cimgen/languages/modernpython/lang_pack.py +++ b/cimgen/languages/modernpython/lang_pack.py @@ -28,7 +28,7 @@ def setup(output_path: str, cgmes_profile_details: list, cim_namespace: str): _create_cgmes_profile(output_path, cgmes_profile_details) -def location(version): +def location(version): # NOSONAR return "..utils.base" @@ -40,7 +40,7 @@ def location(version): profile_template_files = [{"filename": "cimpy_cgmesProfile_template.mustache", "ext": ".py"}] -def get_class_location(class_name, class_map, version): +def get_class_location(class_name, class_map, version): # NOSONAR return f".{class_map[class_name].superClass()}" @@ -81,14 +81,6 @@ def _get_type_and_default(text, renderer) -> tuple[str, str]: return ("float", "default=0.0") -def set_enum_classes(new_enum_classes): - return - - -def set_float_classes(new_float_classes): - return - - def run_template(output_path, class_details): for template_info in template_files: resource_file = Path( diff --git a/cimgen/languages/python/lang_pack.py b/cimgen/languages/python/lang_pack.py index 5a674e4d..08167e01 100644 --- a/cimgen/languages/python/lang_pack.py +++ b/cimgen/languages/python/lang_pack.py @@ -73,14 +73,6 @@ def _set_default(text, render): return "0.0" -def set_enum_classes(new_enum_classes): - return - - -def set_float_classes(new_float_classes): - return - - def run_template(output_path, class_details): for template_info in template_files: class_file = os.path.join(output_path, class_details["class_name"] + template_info["ext"]) From 5b233a6412c0f69d789e454d21c25a5d9ae3633e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCnther?= Date: Sat, 28 Sep 2024 19:00:17 +0200 Subject: [PATCH 07/14] Improve modernpython template: add "is_used", "is_class_attribute", "is_enum_attribute", "is_list_attribute", "is_primitive_attribute" to json_schema_extra of each attribute MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Günther --- .../templates/cimpy_class_template.mustache | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/cimgen/languages/modernpython/templates/cimpy_class_template.mustache b/cimgen/languages/modernpython/templates/cimpy_class_template.mustache index b58bb082..a211a2bc 100644 --- a/cimgen/languages/modernpython/templates/cimpy_class_template.mustache +++ b/cimgen/languages/modernpython/templates/cimpy_class_template.mustache @@ -23,7 +23,6 @@ class {{class_name}}({{sub_class_of}}): """ {{#attributes}} - {{#is_used}} {{label}}: {{#setType}}{{dataType}}{{/setType}} = Field( {{#setDefault}}{{dataType}}{{/setDefault}}, json_schema_extra={ @@ -31,15 +30,14 @@ class {{class_name}}({{sub_class_of}}): {{#attr_origin}} Profile.{{origin}}, {{/attr_origin}} - ] + ], + "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_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}}, }, ) - {{/is_used}} - {{^is_used}} - # *Association not used* - # Type {{dataType}} in CIM - # {{label}}: {{#setType}}{{dataType}}{{/setType}} = Field({{#setDefault}}{{dataType}}{{/setDefault}}, json_schema_extra={"in_profiles": [{{#attr_origin}}Profile.{{origin}}, {{/attr_origin}}]}) # noqa: E501 - {{/is_used}} {{/attributes}} {{^attributes}} From 1d7b6da24b7f49c33be71bbacb16bdb5745fc106 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCnther?= Date: Sat, 28 Sep 2024 19:12:52 +0200 Subject: [PATCH 08/14] Adapt base.py of modernpython: cgmes_attribute_names_in_profile and cgmes_attributes_in_profile return only attributes with property is_used=True as before (now all attributes are in the generated sources, but with property is_used) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Günther --- cimgen/languages/modernpython/utils/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cimgen/languages/modernpython/utils/base.py b/cimgen/languages/modernpython/utils/base.py index 2ee29b7c..f839a993 100644 --- a/cimgen/languages/modernpython/utils/base.py +++ b/cimgen/languages/modernpython/utils/base.py @@ -96,6 +96,7 @@ def cgmes_attribute_names_in_profile(self, profile: BaseProfile | None) -> set[F or (profile in f.default.json_schema_extra["in_profiles"]) # pyright: ignore[reportGeneralTypeIssues] ) if f.name != "mRID" + if f.default.json_schema_extra["is_used"] } def cgmes_attributes_in_profile(self, profile: BaseProfile | None) -> dict[str, "CgmesAttribute"]: From 6f53673977927693bb7ff96207c47b56dc92a44a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCnther?= Date: Sat, 28 Sep 2024 19:16:09 +0200 Subject: [PATCH 09/14] Add CIM writer to modernpython (to export CIM data into several files based on CGMES profiles) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Günther --- .../cimpy_cgmesProfile_template.mustache | 15 ++ cimgen/languages/modernpython/utils/base.py | 26 +- .../utils/export_template.mustache | 46 ++++ cimgen/languages/modernpython/utils/writer.py | 225 ++++++++++++++++++ 4 files changed, 305 insertions(+), 7 deletions(-) create mode 100644 cimgen/languages/modernpython/utils/export_template.mustache create mode 100644 cimgen/languages/modernpython/utils/writer.py diff --git a/cimgen/languages/modernpython/templates/cimpy_cgmesProfile_template.mustache b/cimgen/languages/modernpython/templates/cimpy_cgmesProfile_template.mustache index 95bec43a..0c0db152 100644 --- a/cimgen/languages/modernpython/templates/cimpy_cgmesProfile_template.mustache +++ b/cimgen/languages/modernpython/templates/cimpy_cgmesProfile_template.mustache @@ -25,6 +25,21 @@ class BaseProfile(str, Enum): """Return the list of uris of the profile.""" raise NotImplementedError("Method has to be implemented in subclass.") + def __lt__(self, other): + if not isinstance(other, self.__class__): + return str(self.__class__) < str(other.__class__) + order = list(self.__class__) + return order.index(self) < order.index(other) + + def __gt__(self, other): + return other < self + + def __le__(self, other): + return self == other or self < other + + def __ge__(self, other): + return self == other or self > other + class Profile(BaseProfile): """ diff --git a/cimgen/languages/modernpython/utils/base.py b/cimgen/languages/modernpython/utils/base.py index f839a993..67e8395b 100644 --- a/cimgen/languages/modernpython/utils/base.py +++ b/cimgen/languages/modernpython/utils/base.py @@ -21,8 +21,19 @@ class Base: @cached_property def possible_profiles(self) -> set[BaseProfile]: + """ + A resource can be used by multiple profiles. This is the set of profiles + where this element can be found. + """ raise NotImplementedError("Method not implemented because not relevant in Base.") + @cached_property + def possible_attribute_profiles(self) -> dict[str, list[BaseProfile]]: + """ + Mapping of attribute to the list of possible profiles. + """ + return {f.name: Base.get_extra_prop(f, "in_profiles") for f in fields(self)} + @staticmethod def parse_json_as(attrs: dict[str, Any]) -> "Base": """ @@ -89,14 +100,9 @@ def cgmes_attribute_names_in_profile(self, profile: BaseProfile | None) -> set[F return { f for f in fields(self) - # The field is defined as a pydantic. Field, not a dataclass.field, - # so access to metadata is a tad different. Furthermore, pyright is confused by extra. - if ( - profile is None - or (profile in f.default.json_schema_extra["in_profiles"]) # pyright: ignore[reportGeneralTypeIssues] - ) + if profile is None or (profile in Base.get_extra_prop(f, "in_profiles")) if f.name != "mRID" - if f.default.json_schema_extra["is_used"] + if Base.get_extra_prop(f, "is_used") } def cgmes_attributes_in_profile(self, profile: BaseProfile | None) -> dict[str, "CgmesAttribute"]: @@ -151,6 +157,12 @@ def __str__(self) -> str: """Returns the string representation of this resource.""" return "\n".join([f"{k}={v}" for k, v in sorted(self.to_dict().items())]) + @staticmethod + def get_extra_prop(field: Field, prop: str) -> Any: + # The field is defined as a pydantic field, not a dataclass field, + # so access to metadata is a tad different. Furthermore, pyright is confused by extra. + return field.default.json_schema_extra[prop] # pyright: ignore[reportAttributeAccessIssue] + CgmesAttributeTypes: TypeAlias = str | int | float | Base | list | None diff --git a/cimgen/languages/modernpython/utils/export_template.mustache b/cimgen/languages/modernpython/utils/export_template.mustache new file mode 100644 index 00000000..4055d9ef --- /dev/null +++ b/cimgen/languages/modernpython/utils/export_template.mustache @@ -0,0 +1,46 @@ + + + {{#model}} + + {{#description}} + {{value}} + {{/description}} + + {{/model}} + {{#main}} + + {{#attributes}} + {{#is_class_attribute}} + + {{/is_class_attribute}} + {{#is_enum_attribute}} + + {{/is_enum_attribute}} + {{#is_list_attribute}} + + {{/is_list_attribute}} + {{#is_primitive_attribute}} + {{value}} + {{/is_primitive_attribute}} + {{/attributes}} + + {{/main}} + {{#about}} + + {{#attributes}} + {{#is_class_attribute}} + + {{/is_class_attribute}} + {{#is_enum_attribute}} + + {{/is_enum_attribute}} + {{#is_list_attribute}} + + {{/is_list_attribute}} + {{#is_primitive_attribute}} + {{value}} + {{/is_primitive_attribute}} + {{/attributes}} + + {{/about}} + diff --git a/cimgen/languages/modernpython/utils/writer.py b/cimgen/languages/modernpython/utils/writer.py new file mode 100644 index 00000000..9c9f4bbc --- /dev/null +++ b/cimgen/languages/modernpython/utils/writer.py @@ -0,0 +1,225 @@ +from dataclasses import fields +from pathlib import Path + +import chevron + +from pycgmes.utils.base import Base +from pycgmes.utils.constants import NAMESPACES +from pycgmes.utils.profile import BaseProfile, Profile + + +class Writer: + """Class for writing CIM RDF/XML files.""" + + def __init__(self, objects: dict[str, Base]): + """Constructor. + + :param objects: Mapping of rdfid to CIM object. + """ + self.objects = objects + + def write( + self, outputfile: str, model_id: str, class_profile_map: dict[str, BaseProfile] + ) -> dict[BaseProfile, str]: + """Write CIM RDF/XML files. + + This function writes CIM objects into one or more RDF/XML files separated by profiles. + + Each CIM object will be written to its corresponding profile file depending on class_profile_map. + But some objects to more than one file if some attribute profiles are not the same as the class profile. + + :param outputfile: Stem of the output file, resulting files: _.xml. + :param model_id: Stem of the model IDs, resulting IDs: _. + :param class_profile_map: Mapping of CIM type to profile. + :return: Mapping of profile to outputfile. + """ + profile_list: list[BaseProfile] = list(Profile) + profile_list += {p for p in class_profile_map.values() if p not in profile_list} + profile_file_map: dict[BaseProfile, str] = {} + for profile in profile_list: + profile_name = profile.long_name + full_file_name = outputfile + "_" + profile.long_name + ".xml" + output = self.generate(profile, model_id + "_" + profile_name, class_profile_map) + if output: + with Path.open(Path(full_file_name), "w") as file: + file.write(output) + profile_file_map[profile] = full_file_name + return profile_file_map + + def generate(self, profile: BaseProfile, model_id: str, class_profile_map: dict[str, BaseProfile]) -> str: + """Write CIM objects as RDF/XML data to a string. + + This function writes RDF/XML data corresponding to one profile into a string. + + :param profile: Only data for this profile should be written. + :param model_id: Stem of the model IDs, resulting IDs: _. + :param class_profile_map: Mapping of CIM type to profile. + :return: Mapping of profile to outputfile. + """ + namespaces = [{"key": k, "url": NAMESPACES[k]} for k in ("rdf", "cim", "md")] + model_description = { + "id": model_id, + "description": [{"attr_name": "modelingAuthoritySet", "value": "www.sogno.energy"}], + } + for uri in profile.uris: + model_description["description"].append({"attr_name": "profile", "value": uri}) + main, about = self.sort_attributes_to_profile(profile, class_profile_map) + output = "" + if main or about: + template_path = (Path(__file__).parent / "export_template.mustache").resolve() + with template_path.open() as file: + output = chevron.render( + file, + { + "main": main, + "about": about, + "namespaces": namespaces, + "model": [model_description], + }, + ) + return output + + def sort_attributes_to_profile(self, profile: BaseProfile, class_profile_map: dict[str, BaseProfile]): + """Sort CIM objects and their attributes depending on whether the profile is the main profile of the class. + + This function sorts a list of objects to two lists: main and about. + An object is sorted to main if the profile is the main profile of the class, otherwise to about. + To each object the attribute infos are stored in the list. + But it contains only attributes that belongs to the profile. + If an attribute has more than one possible profile the main profile of the class is preferred. + + :param profile: Only data for this profile should be taken. + :param class_profile_map: Mapping of CIM type to profile. + :return: main list, about list. + """ + main = [] + 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): + 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(): + value = attr_infos["value"] + if value and attr != "mRID" and Writer.get_attribute_profile(obj, attr, class_profile) == profile: + if isinstance(value, (list, tuple)): + attributes.extend(attr_infos | {"value": v} for v in value) + else: + attributes.append(attr_infos) + + infos = {"id": rdfid, "type": typ, "attributes": attributes} + if main_entry_of_object: + main.append(infos) + elif attributes: + about.append(infos) + return main, about + + @staticmethod + def is_class_matching_profile(obj: Base, profile: BaseProfile) -> bool: + """Check if this profile is a possible profile for this CIM object. + + This function checks if the CIM type of an object contains data for a profile. + The profile could be the main profile of the type, or the type contains attributes for this profile. + + :param obj: CIM object to get the CIM type from. + :param profile: Profile to check. + :return: True/False + """ + if profile in obj.possible_profiles: + return True + return any(profile in profiles for profiles in obj.possible_attribute_profiles.values()) + + @staticmethod + def get_class_profile(obj: Base) -> BaseProfile: + """Get the main profile of this CIM object. + + This function searches for the main profile of the CIM type of an object. + If the type contains attributes for different profiles not all data of the object could be written into one + file. To write the data to as few as possible files the main profile should be that with most of the + attributes. But some types contain a lot of rarely used special attributes, i.e. attributes for a special + profile (e.g. TopologyNode has many attributes for TopologyBoundary, but the main profile should be Topology). + That's why attributes that only belong to one profile are skipped in the search algorithm. + + :param obj: CIM object to get the CIM type from. + :return: Main profile. + """ + class_profiles = list(obj.possible_profiles) + if len(class_profiles) == 1: + return class_profiles[0] + profile_attributes_map: dict[BaseProfile, list(str)] = {} + for attr, profiles in obj.possible_attribute_profiles.items(): + ambiguous_profile = len(profiles) > 1 + for profile in profiles: + if ambiguous_profile and profile in class_profiles: + profile_attributes_map.setdefault(profile, []).append(attr) + count_profile_list: list(tuple[int, BaseProfile]) = [] + for profile, attributes in profile_attributes_map.items(): + count_profile_list.append((-len(attributes), profile)) + return sorted(count_profile_list)[0][1] + + @staticmethod + def get_class_profile_map(obj_list: list[Base]) -> dict[str, BaseProfile]: + """Get the main profiles for a list of CIM objects. + + This function searches for the main profile of each CIM type in the object list + (see getClassProfile for details). + + The result could be used as parameter for the functions: write and generate. + But it is also possible to optimize the mapping manually for some CIM types before calling these functions. + + :param obj_list: List of CIM objects. + :return: Mapping of CIM type to profile. + """ + profile_map: dict[str, BaseProfile] = {} + for obj in obj_list: + typ = obj.apparent_name() + if typ not in profile_map: + profile_map[typ] = Writer.get_class_profile(obj) + return profile_map + + @staticmethod + def get_attribute_profile(obj: Base, attr: str, class_profile: BaseProfile) -> BaseProfile | None: + """Get the profile for this attribute of the CIM object. + + This function searches for the profile of an attribute for the CIM type of an object. + If the main profile of the type is a possible profile of the attribute it should be choosen. + Otherwise, the first profile in the list of possible profiles ordered by profile number. + + :param obj: CIM object to get the CIM type from. + :param attr: Attribute to check + :param class_profile: Main profile of the CIM type + :return: Attribute profile. + """ + attr_profiles_map = obj.possible_attribute_profiles + profiles = attr_profiles_map.get(attr, []) + if class_profile in profiles: + return class_profile + if profiles: + return sorted(profiles)[0] + return None + + @staticmethod + def get_attribute_infos(obj: Base) -> dict[str, dict[str, object]]: + # the class of this object and the parent classes (excluding class "object"). + class_and_parent_classes = obj.__class__.__mro__[:-1] + attr_infos_map: dict[str, dict[str, object]] = {} + for cls in reversed(class_and_parent_classes): + for field in fields(cls): + attr = field.name + if attr not in attr_infos_map: + attr_name = cls.apparent_name() + "." + attr + extra = getattr(field.default, "json_schema_extra", {}) + if extra.get("is_used"): + infos = { + "attr_name": attr_name, + "namespace": extra.get("namespace", obj.namespace), + "value": getattr(obj, attr), + "is_class_attribute": extra.get("is_class_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"), + } + attr_infos_map[attr] = infos + return attr_infos_map From 8810da3be56acdd9ad54a0ffae3497cb10e49023 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCnther?= Date: Thu, 3 Oct 2024 18:31:08 +0200 Subject: [PATCH 10/14] Add recommended class profile to all generated classes of modernpython MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Günther --- .../modernpython/templates/cimpy_class_template.mustache | 8 ++++++++ cimgen/languages/modernpython/utils/base.py | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/cimgen/languages/modernpython/templates/cimpy_class_template.mustache b/cimgen/languages/modernpython/templates/cimpy_class_template.mustache index a211a2bc..08e125d9 100644 --- a/cimgen/languages/modernpython/templates/cimpy_class_template.mustache +++ b/cimgen/languages/modernpython/templates/cimpy_class_template.mustache @@ -55,3 +55,11 @@ class {{class_name}}({{sub_class_of}}): Profile.{{origin}}, {{/class_origin}} } + + @cached_property + def recommended_profile(self) -> BaseProfile: + """ + This is the profile with most of the attributes. + It should be used to write the data to as few as possible files. + """ + return Profile.{{recommended_class_profile}} diff --git a/cimgen/languages/modernpython/utils/base.py b/cimgen/languages/modernpython/utils/base.py index 67e8395b..e44b78ba 100644 --- a/cimgen/languages/modernpython/utils/base.py +++ b/cimgen/languages/modernpython/utils/base.py @@ -27,6 +27,14 @@ def possible_profiles(self) -> set[BaseProfile]: """ raise NotImplementedError("Method not implemented because not relevant in Base.") + @cached_property + def recommended_profile(self) -> BaseProfile: + """ + This is the profile with most of the attributes. + It should be used to write the data to as few as possible files. + """ + raise NotImplementedError("Method not implemented because not relevant in Base.") + @cached_property def possible_attribute_profiles(self) -> dict[str, list[BaseProfile]]: """ From c612de9ec0460b5ce8d1bf7a0ca1f7735f22b9f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCnther?= Date: Thu, 3 Oct 2024 20:32:14 +0200 Subject: [PATCH 11/14] Improve CIM writer of modernpython: use recommended profile of each class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Günther --- cimgen/cimgen.py | 1 + cimgen/languages/modernpython/utils/writer.py | 31 ++----------------- 2 files changed, 3 insertions(+), 29 deletions(-) diff --git a/cimgen/cimgen.py b/cimgen/cimgen.py index e5214011..fe1a29b8 100644 --- a/cimgen/cimgen.py +++ b/cimgen/cimgen.py @@ -810,6 +810,7 @@ def _get_recommended_class_profiles(elem_dict): profiles = [origin["origin"] for origin in attribute["attr_origin"]] ambiguous_profile = len(profiles) > 1 for profile in profiles: + # Use condition attribute["is_used"]? For CGMES 2.4.13/2.4.15/3.0.0 the results wouldn't change! if ambiguous_profile and profile in class_profiles: profile_count_map.setdefault(profile, []).append(attribute["label"]) name = elem_dict[name].superClass() diff --git a/cimgen/languages/modernpython/utils/writer.py b/cimgen/languages/modernpython/utils/writer.py index 9c9f4bbc..b4334945 100644 --- a/cimgen/languages/modernpython/utils/writer.py +++ b/cimgen/languages/modernpython/utils/writer.py @@ -135,49 +135,22 @@ def is_class_matching_profile(obj: Base, profile: BaseProfile) -> bool: def get_class_profile(obj: Base) -> BaseProfile: """Get the main profile of this CIM object. - This function searches for the main profile of the CIM type of an object. - If the type contains attributes for different profiles not all data of the object could be written into one - file. To write the data to as few as possible files the main profile should be that with most of the - attributes. But some types contain a lot of rarely used special attributes, i.e. attributes for a special - profile (e.g. TopologyNode has many attributes for TopologyBoundary, but the main profile should be Topology). - That's why attributes that only belong to one profile are skipped in the search algorithm. - :param obj: CIM object to get the CIM type from. :return: Main profile. """ - class_profiles = list(obj.possible_profiles) - if len(class_profiles) == 1: - return class_profiles[0] - profile_attributes_map: dict[BaseProfile, list(str)] = {} - for attr, profiles in obj.possible_attribute_profiles.items(): - ambiguous_profile = len(profiles) > 1 - for profile in profiles: - if ambiguous_profile and profile in class_profiles: - profile_attributes_map.setdefault(profile, []).append(attr) - count_profile_list: list(tuple[int, BaseProfile]) = [] - for profile, attributes in profile_attributes_map.items(): - count_profile_list.append((-len(attributes), profile)) - return sorted(count_profile_list)[0][1] + return obj.recommended_profile @staticmethod def get_class_profile_map(obj_list: list[Base]) -> dict[str, BaseProfile]: """Get the main profiles for a list of CIM objects. - This function searches for the main profile of each CIM type in the object list - (see getClassProfile for details). - The result could be used as parameter for the functions: write and generate. But it is also possible to optimize the mapping manually for some CIM types before calling these functions. :param obj_list: List of CIM objects. :return: Mapping of CIM type to profile. """ - profile_map: dict[str, BaseProfile] = {} - for obj in obj_list: - typ = obj.apparent_name() - if typ not in profile_map: - profile_map[typ] = Writer.get_class_profile(obj) - return profile_map + return {obj.apparent_name(): Writer.get_class_profile(obj) for obj in obj_list} @staticmethod def get_attribute_profile(obj: Base, attr: str, class_profile: BaseProfile) -> BaseProfile | None: From 7b893fb0442d99da30250eef485b9605a93cc7e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCnther?= Date: Sat, 5 Oct 2024 15:22:35 +0200 Subject: [PATCH 12/14] Update modernpython/README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Günther --- README.md | 2 +- cimgen/languages/modernpython/README.md | 382 +++++++++++++----------- 2 files changed, 204 insertions(+), 180 deletions(-) diff --git a/README.md b/README.md index eb52133b..601b8da6 100644 --- a/README.md +++ b/README.md @@ -59,4 +59,4 @@ your publications: Dinkelbach, J., Razik, L., Mirz, M., Benigni, A., Monti, A.: Template-based generation of programming language specific code for smart grid modelling compliant with CIM and CGMES. -J. Eng. 2023, 1–13 (2022). [https://doi.org/10.1049/tje2.12208](https://doi.org/10.1049/tje2.12208) +J. Eng. 2023, 1-13 (2022). [https://doi.org/10.1049/tje2.12208](https://doi.org/10.1049/tje2.12208) diff --git a/cimgen/languages/modernpython/README.md b/cimgen/languages/modernpython/README.md index 3b4e0c73..d567d629 100644 --- a/cimgen/languages/modernpython/README.md +++ b/cimgen/languages/modernpython/README.md @@ -6,12 +6,11 @@ - [Examples](#examples) - [Python](#python) - [Modern Python](#modern-python) - - [Modern Python after black (formatting)](#modern-python-after-black-formatting) ## Description It mostly uses dataclasses (pydantic one, to get data checks) and adds some attribute walking -directly into the class. See for instance `cgmes_attributes_in_profile` in [Base.py](./Base.py). +directly into the class. See for instance `cgmes_attributes_in_profile` in [Base.py](utils/Base.py). ## Warning @@ -21,83 +20,94 @@ other code using these classes) much simpler. ## Examples -Example of ACLineSegment. +Example of ACLineSegment (generated with --cgmes_version cgmes_v3_0_0). ### Python ```python from .Conductor import Conductor +from .CGMESProfile import Profile class ACLineSegment(Conductor): - ''' - A wire or combination of wires, with consistent electrical characteristics, building a single electrical system, used to carry alternating current between points in the power system. For symmetrical, transposed 3ph lines, it is sufficient to use attributes of the line segment, which describe impedances and admittances for the entire length of the segment. Additionally impedances can be computed by using length and associated per length impedances. The BaseVoltage at the two ends of ACLineSegments in a Line shall have the same BaseVoltage.nominalVoltage. However, boundary lines may have slightly different BaseVoltage.nominalVoltages and variation is allowed. Larger voltage difference in general requires use of an equivalent branch. - - :b0ch: Zero sequence shunt (charging) susceptance, uniformly distributed, of the entire line section. Default: 0.0 - :bch: Positive sequence shunt (charging) susceptance, uniformly distributed, of the entire line section. This value represents the full charging over the full length of the line. Default: 0.0 - :g0ch: Zero sequence shunt (charging) conductance, uniformly distributed, of the entire line section. Default: 0.0 - :gch: Positive sequence shunt (charging) conductance, uniformly distributed, of the entire line section. Default: 0.0 - :r: Positive sequence series resistance of the entire line section. Default: 0.0 - :r0: Zero sequence series resistance of the entire line section. Default: 0.0 - :shortCircuitEndTemperature: Maximum permitted temperature at the end of SC for the calculation of minimum short-circuit currents. Used for short circuit data exchange according to IEC 60909 Default: 0.0 - :x: Positive sequence series reactance of the entire line section. Default: 0.0 - :x0: Zero sequence series reactance of the entire line section. Default: 0.0 - ''' - - cgmesProfile = Conductor.cgmesProfile - - possibleProfileList = {'class': [cgmesProfile.EQ.value, ], - 'b0ch': [cgmesProfile.EQ.value, ], - 'bch': [cgmesProfile.EQ.value, ], - 'g0ch': [cgmesProfile.EQ.value, ], - 'gch': [cgmesProfile.EQ.value, ], - 'r': [cgmesProfile.EQ.value, ], - 'r0': [cgmesProfile.EQ.value, ], - 'shortCircuitEndTemperature': [cgmesProfile.EQ.value, ], - 'x': [cgmesProfile.EQ.value, ], - 'x0': [cgmesProfile.EQ.value, ], - } - - serializationProfile = {} - - __doc__ += '\n Documentation of parent class Conductor: \n' + Conductor.__doc__ - - def __init__(self, b0ch = 0.0, bch = 0.0, g0ch = 0.0, gch = 0.0, r = 0.0, r0 = 0.0, shortCircuitEndTemperature = 0.0, x = 0.0, x0 = 0.0, *args, **kw_args): - super().__init__(*args, **kw_args) - - self.b0ch = b0ch - self.bch = bch - self.g0ch = g0ch - self.gch = gch - self.r = r - self.r0 = r0 - self.shortCircuitEndTemperature = shortCircuitEndTemperature - self.x = x - self.x0 = x0 - - def __str__(self): - str = 'class=ACLineSegment\n' - attributes = self.__dict__ - for key in attributes.keys(): - str = str + key + '={}\n'.format(attributes[key]) - return str + """ + A wire or combination of wires, with consistent electrical characteristics, building a single electrical system, used to carry alternating current between points in the power system. For symmetrical, transposed three phase lines, it is sufficient to use attributes of the line segment, which describe impedances and admittances for the entire length of the segment. Additionally impedances can be computed by using length and associated per length impedances. The BaseVoltage at the two ends of ACLineSegments in a Line shall have the same BaseVoltage.nominalVoltage. However, boundary lines may have slightly different BaseVoltage.nominalVoltages and variation is allowed. Larger voltage difference in general requires use of an equivalent branch. + + :Clamp: The clamps connected to the line segment. Default: "list" + :Cut: Cuts applied to the line segment. Default: "list" + :b0ch: Zero sequence shunt (charging) susceptance, uniformly distributed, of the entire line section. Default: 0.0 + :bch: Positive sequence shunt (charging) susceptance, uniformly distributed, of the entire line section. This value represents the full charging over the full length of the line. Default: 0.0 + :g0ch: Zero sequence shunt (charging) conductance, uniformly distributed, of the entire line section. Default: 0.0 + :gch: Positive sequence shunt (charging) conductance, uniformly distributed, of the entire line section. Default: 0.0 + :r: Positive sequence series resistance of the entire line section. Default: 0.0 + :r0: Zero sequence series resistance of the entire line section. Default: 0.0 + :shortCircuitEndTemperature: Maximum permitted temperature at the end of SC for the calculation of minimum short-circuit currents. Used for short circuit data exchange according to IEC 60909. Default: 0.0 + :x: Positive sequence series reactance of the entire line section. Default: 0.0 + :x0: Zero sequence series reactance of the entire line section. Default: 0.0 + """ + + possibleProfileList = { + "class": [Profile.EQ.value, Profile.SC.value, ], + "Clamp": [Profile.EQ.value, ], + "Cut": [Profile.EQ.value, ], + "b0ch": [Profile.SC.value, ], + "bch": [Profile.EQ.value, ], + "g0ch": [Profile.SC.value, ], + "gch": [Profile.EQ.value, ], + "r": [Profile.EQ.value, ], + "r0": [Profile.SC.value, ], + "shortCircuitEndTemperature": [Profile.SC.value, ], + "x": [Profile.EQ.value, ], + "x0": [Profile.SC.value, ], + } + + serializationProfile = {} + + recommendedClassProfile = Profile.EQ.value + + __doc__ += "\nDocumentation of parent class Conductor:\n" + Conductor.__doc__ + + def __init__(self, Clamp = "list", Cut = "list", b0ch = 0.0, bch = 0.0, g0ch = 0.0, gch = 0.0, r = 0.0, r0 = 0.0, shortCircuitEndTemperature = 0.0, x = 0.0, x0 = 0.0, *args, **kw_args): + super().__init__(*args, **kw_args) + + self.Clamp = Clamp + self.Cut = Cut + self.b0ch = b0ch + self.bch = bch + self.g0ch = g0ch + self.gch = gch + self.r = r + self.r0 = r0 + self.shortCircuitEndTemperature = shortCircuitEndTemperature + self.x = x + self.x0 = x0 + + def __str__(self): + str = "class=ACLineSegment\n" + attributes = self.__dict__ + for key in attributes.keys(): + str = str + key + "={}\n".format(attributes[key]) + return str ``` ### Modern Python ```python """ -Generated from the CGMES 3 files via cimgen: https://github.com/sogno-platform/cimgen +Generated from the CGMES files via cimgen: https://github.com/sogno-platform/cimgen """ from functools import cached_property from typing import Optional + from pydantic import Field from pydantic.dataclasses import dataclass -from .Base import DataclassConfig, Profile + +from ..utils.profile import BaseProfile, Profile from .Conductor import Conductor -@dataclass(config=DataclassConfig) + +@dataclass class ACLineSegment(Conductor): """ A wire or combination of wires, with consistent electrical characteristics, building a single electrical system, @@ -108,171 +118,177 @@ class ACLineSegment(Conductor): BaseVoltage.nominalVoltage. However, boundary lines may have slightly different BaseVoltage.nominalVoltages and variation is allowed. Larger voltage difference in general requires use of an equivalent branch. - bch: Positive sequence shunt (charging) susceptance, uniformly distributed, of the entire line section. This value - represents the full charging over the full length of the line. - gch: Positive sequence shunt (charging) conductance, uniformly distributed, of the entire line section. - r: Positive sequence series resistance of the entire line section. - x: Positive sequence series reactance of the entire line section. Clamp: The clamps connected to the line segment. Cut: Cuts applied to the line segment. b0ch: Zero sequence shunt (charging) susceptance, uniformly distributed, of the entire line section. - g0ch: Zero sequence shunt (charging) conductance, uniformly distributed, of the entire line section. - r0: Zero sequence series resistance of the entire line section. - shortCircuitEndTemperature: Maximum permitted temperature at the end of SC for the calculation of minimum short- - circuit currents. Used for short circuit data exchange according to IEC 60909. - x0: Zero sequence series reactance of the entire line section. - """ - - bch : float = Field(default=0.0, in_profiles = [Profile.EQ, ]) - - gch : float = Field(default=0.0, in_profiles = [Profile.EQ, ]) - - r : float = Field(default=0.0, in_profiles = [Profile.EQ, ]) - - x : float = Field(default=0.0, in_profiles = [Profile.EQ, ]) - - # *Association not used* - # Type M:0..n in CIM # pylint: disable-next=line-too-long - # Clamp : list = Field(default_factory=list, in_profiles = [Profile.EQ, ]) # noqa: E501 - - # *Association not used* - # Type M:0..n in CIM # pylint: disable-next=line-too-long - # Cut : list = Field(default_factory=list, in_profiles = [Profile.EQ, ]) # noqa: E501 - - b0ch : float = Field(default=0.0, in_profiles = [Profile.SC, ]) - - g0ch : float = Field(default=0.0, in_profiles = [Profile.SC, ]) - - r0 : float = Field(default=0.0, in_profiles = [Profile.SC, ]) - - shortCircuitEndTemperature : float = Field(default=0.0, in_profiles = [Profile.SC, ]) - - x0 : float = Field(default=0.0, in_profiles = [Profile.SC, ]) - - - - @cached_property - def possible_profiles(self)->set[Profile]: - """ - A resource can be used by multiple profiles. This is the set of profiles - where this element can be found. - """ - return { Profile.EQ, Profile.SC, } -``` - -### Modern Python after black (formatting) - -```python -""" -Generated from the CGMES 3 files via cimgen: https://github.com/sogno-platform/cimgen -""" - -from functools import cached_property -from pydantic import Field -from pydantic.dataclasses import dataclass -from .Base import DataclassConfig, Profile -from .Conductor import Conductor - - -@dataclass(config=DataclassConfig) -class ACLineSegment(Conductor): - """ - A wire or combination of wires, with consistent electrical characteristics, building a single electrical system, - used to carry alternating current between points in the power system. For symmetrical, transposed three phase - lines, it is sufficient to use attributes of the line segment, which describe impedances and admittances for - the entire length of the segment. Additionally impedances can be computed by using length and associated per - length impedances. The BaseVoltage at the two ends of ACLineSegments in a Line shall have the same - BaseVoltage.nominalVoltage. However, boundary lines may have slightly different BaseVoltage.nominalVoltages - and variation is allowed. Larger voltage difference in general requires use of an equivalent branch. - bch: Positive sequence shunt (charging) susceptance, uniformly distributed, of the entire line section. This value represents the full charging over the full length of the line. + g0ch: Zero sequence shunt (charging) conductance, uniformly distributed, of the entire line section. gch: Positive sequence shunt (charging) conductance, uniformly distributed, of the entire line section. r: Positive sequence series resistance of the entire line section. - x: Positive sequence series reactance of the entire line section. - Clamp: The clamps connected to the line segment. - Cut: Cuts applied to the line segment. - b0ch: Zero sequence shunt (charging) susceptance, uniformly distributed, of the entire line section. - g0ch: Zero sequence shunt (charging) conductance, uniformly distributed, of the entire line section. r0: Zero sequence series resistance of the entire line section. shortCircuitEndTemperature: Maximum permitted temperature at the end of SC for the calculation of minimum short- circuit currents. Used for short circuit data exchange according to IEC 60909. + x: Positive sequence series reactance of the entire line section. x0: Zero sequence series reactance of the entire line section. """ - bch: float = Field( - default=0.0, - in_profiles=[ - Profile.EQ, - ], + Clamp: list = Field( + default_factory=list, + json_schema_extra={ + "in_profiles": [ + Profile.EQ, + ], + "is_used": False, + "is_class_attribute": False, + "is_enum_attribute": False, + "is_list_attribute": True, + "is_primitive_attribute": False, + }, ) - gch: float = Field( - default=0.0, - in_profiles=[ - Profile.EQ, - ], + Cut: list = Field( + default_factory=list, + json_schema_extra={ + "in_profiles": [ + Profile.EQ, + ], + "is_used": False, + "is_class_attribute": False, + "is_enum_attribute": False, + "is_list_attribute": True, + "is_primitive_attribute": False, + }, ) - r: float = Field( + b0ch: float = Field( default=0.0, - in_profiles=[ - Profile.EQ, - ], + json_schema_extra={ + "in_profiles": [ + Profile.SC, + ], + "is_used": True, + "is_class_attribute": False, + "is_enum_attribute": False, + "is_list_attribute": False, + "is_primitive_attribute": True, + }, ) - x: float = Field( + bch: float = Field( default=0.0, - in_profiles=[ - Profile.EQ, - ], + json_schema_extra={ + "in_profiles": [ + Profile.EQ, + ], + "is_used": True, + "is_class_attribute": False, + "is_enum_attribute": False, + "is_list_attribute": False, + "is_primitive_attribute": True, + }, ) - # *Association not used* - # Type M:0..n in CIM # pylint: disable-next=line-too-long - # Clamp : list = Field(default_factory=list, in_profiles = [Profile.EQ, ]) - - # *Association not used* - # Type M:0..n in CIM # pylint: disable-next=line-too-long - # Cut : list = Field(default_factory=list, in_profiles = [Profile.EQ, ]) + g0ch: float = Field( + default=0.0, + json_schema_extra={ + "in_profiles": [ + Profile.SC, + ], + "is_used": True, + "is_class_attribute": False, + "is_enum_attribute": False, + "is_list_attribute": False, + "is_primitive_attribute": True, + }, + ) - b0ch: float = Field( + gch: float = Field( default=0.0, - in_profiles=[ - Profile.SC, - ], + json_schema_extra={ + "in_profiles": [ + Profile.EQ, + ], + "is_used": True, + "is_class_attribute": False, + "is_enum_attribute": False, + "is_list_attribute": False, + "is_primitive_attribute": True, + }, ) - g0ch: float = Field( + r: float = Field( default=0.0, - in_profiles=[ - Profile.SC, - ], + json_schema_extra={ + "in_profiles": [ + Profile.EQ, + ], + "is_used": True, + "is_class_attribute": False, + "is_enum_attribute": False, + "is_list_attribute": False, + "is_primitive_attribute": True, + }, ) r0: float = Field( default=0.0, - in_profiles=[ - Profile.SC, - ], + json_schema_extra={ + "in_profiles": [ + Profile.SC, + ], + "is_used": True, + "is_class_attribute": False, + "is_enum_attribute": False, + "is_list_attribute": False, + "is_primitive_attribute": True, + }, ) shortCircuitEndTemperature: float = Field( default=0.0, - in_profiles=[ - Profile.SC, - ], + json_schema_extra={ + "in_profiles": [ + Profile.SC, + ], + "is_used": True, + "is_class_attribute": False, + "is_enum_attribute": False, + "is_list_attribute": False, + "is_primitive_attribute": True, + }, + ) + + x: float = Field( + default=0.0, + json_schema_extra={ + "in_profiles": [ + Profile.EQ, + ], + "is_used": True, + "is_class_attribute": False, + "is_enum_attribute": False, + "is_list_attribute": False, + "is_primitive_attribute": True, + }, ) x0: float = Field( default=0.0, - in_profiles=[ - Profile.SC, - ], + json_schema_extra={ + "in_profiles": [ + Profile.SC, + ], + "is_used": True, + "is_class_attribute": False, + "is_enum_attribute": False, + "is_list_attribute": False, + "is_primitive_attribute": True, + }, ) @cached_property - def possible_profiles(self) -> set[Profile]: + def possible_profiles(self) -> set[BaseProfile]: """ A resource can be used by multiple profiles. This is the set of profiles where this element can be found. @@ -281,4 +297,12 @@ class ACLineSegment(Conductor): Profile.EQ, Profile.SC, } + + @cached_property + def recommended_profile(self) -> BaseProfile: + """ + This is the profile with most of the attributes. + It should be used to write the data to as few as possible files. + """ + return Profile.EQ ``` From 1da207dd79e59ac53c0165ec63e0f5006b7c7075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCnther?= Date: Sun, 6 Oct 2024 23:05:37 +0200 Subject: [PATCH 13/14] Fix sonar issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Günther --- cimgen/cimgen.py | 12 ++++++------ cimgen/languages/java/lang_pack.py | 2 +- cimgen/languages/javascript/lang_pack.py | 4 ++-- cimgen/languages/modernpython/utils/writer.py | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cimgen/cimgen.py b/cimgen/cimgen.py index fe1a29b8..d124bf41 100644 --- a/cimgen/cimgen.py +++ b/cimgen/cimgen.py @@ -16,7 +16,7 @@ def __init__(self, jsonObject): self.jsonDefinition = jsonObject return - def asJson(self, lang_pack): + def asJson(self): jsonObject = {} if self.about() is not None: jsonObject["about"] = self.about() @@ -223,7 +223,7 @@ def is_an_enum_class(self): def enum_instances(self): return self.enum_instance_list - def addEnumInstance(self, instance): + def add_enum_instance(self, instance): instance["index"] = len(self.enum_instance_list) self.enum_instance_list.append(instance) @@ -398,7 +398,7 @@ def _add_profile_to_packages(profile_name, short_profile_name, profile_uri_list) package_listed_by_short_name[short_profile_name].extend(profile_uri_list) -def _parse_rdf(input_dic, version, lang_pack): +def _parse_rdf(input_dic, version): classes_map = {} profile_name = "" profile_uri_list = [] @@ -415,7 +415,7 @@ def _parse_rdf(input_dic, version, lang_pack): # Iterate over list elements for list_elem in descriptions: rdfsEntry = RDFSEntry(list_elem) - object_dic = rdfsEntry.asJson(lang_pack) + object_dic = rdfsEntry.asJson() rdfs_entry_types = _rdfs_entry_types(rdfsEntry, version) if "class" in rdfs_entry_types: @@ -452,7 +452,7 @@ def _parse_rdf(input_dic, version, lang_pack): for instance in enum_instances: clarse = _get_rid_of_hash(instance["type"]) if clarse and clarse in classes_map: - classes_map[clarse].addEnumInstance(instance) + classes_map[clarse].add_enum_instance(instance) else: logger.info("Class {} for enum instance {} not found.".format(clarse, instance)) @@ -703,7 +703,7 @@ def cim_generate(directory, output_path, version, lang_pack): # parse RDF files and create a dictionary from the RDF file parse_result = xmltodict.parse(xmlstring, attr_prefix="$", cdata_key="_", dict_constructor=dict) - parsed = _parse_rdf(parse_result, version, lang_pack) + parsed = _parse_rdf(parse_result, version) profiles_array.append(parsed) # merge multiple profile definitions into one profile diff --git a/cimgen/languages/java/lang_pack.py b/cimgen/languages/java/lang_pack.py index 66ac565d..fd671f0e 100644 --- a/cimgen/languages/java/lang_pack.py +++ b/cimgen/languages/java/lang_pack.py @@ -97,7 +97,7 @@ def label(text, render): # These create_ functions are used to generate the implementations for # the entries in the dynamic_switch maps referenced in assignments.cpp and Task.cpp -def create_class_assign(text, render): +def create_class_assign(text, render): # NOSONAR # TODO REMOVE: return "" diff --git a/cimgen/languages/javascript/lang_pack.py b/cimgen/languages/javascript/lang_pack.py index 199830ea..f216816a 100644 --- a/cimgen/languages/javascript/lang_pack.py +++ b/cimgen/languages/javascript/lang_pack.py @@ -73,7 +73,7 @@ def get_class_location(class_name, class_map, version): # NOSONAR } -def selectPrimitiveRenderFunction(class_details): +def select_primitive_render_function(class_details): class_name = class_details["class_name"] render = "" if class_details["is_a_float_class"]: @@ -112,7 +112,7 @@ def run_template(output_path, class_details): if class_type == "enum": renderAttribute = aggregateRenderer["renderInstance"] elif class_type == "primitive": - renderAttribute = selectPrimitiveRenderFunction(class_details) + renderAttribute = select_primitive_render_function(class_details) else: renderAttribute = aggregateRenderer["renderClass"] if renderAttribute == "": diff --git a/cimgen/languages/modernpython/utils/writer.py b/cimgen/languages/modernpython/utils/writer.py index b4334945..89abb2e7 100644 --- a/cimgen/languages/modernpython/utils/writer.py +++ b/cimgen/languages/modernpython/utils/writer.py @@ -79,7 +79,7 @@ def generate(self, profile: BaseProfile, model_id: str, class_profile_map: dict[ ) return output - def sort_attributes_to_profile(self, profile: BaseProfile, class_profile_map: dict[str, BaseProfile]): + def sort_attributes_to_profile(self, profile: BaseProfile, class_profile_map: dict[str, BaseProfile]): # NOSONAR """Sort CIM objects and their attributes depending on whether the profile is the main profile of the class. This function sorts a list of objects to two lists: main and about. From 6e17f1090175a6c321b56135a0802145bfc8baa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCnther?= Date: Sun, 6 Oct 2024 23:19:49 +0200 Subject: [PATCH 14/14] Fix sonar issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Günther --- cimgen/cimgen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cimgen/cimgen.py b/cimgen/cimgen.py index d124bf41..67813dd1 100644 --- a/cimgen/cimgen.py +++ b/cimgen/cimgen.py @@ -398,7 +398,7 @@ def _add_profile_to_packages(profile_name, short_profile_name, profile_uri_list) package_listed_by_short_name[short_profile_name].extend(profile_uri_list) -def _parse_rdf(input_dic, version): +def _parse_rdf(input_dic, version): # NOSONAR classes_map = {} profile_name = "" profile_uri_list = []