diff --git a/.gitignore b/.gitignore index d8c0846c..56dba38b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ __pycache__/ .vscode/* */.DS_Store .DS_Store +tests/ diff --git a/cimgen/languages/modernpython/lang_pack.py b/cimgen/languages/modernpython/lang_pack.py index 99685bf8..64cb4104 100644 --- a/cimgen/languages/modernpython/lang_pack.py +++ b/cimgen/languages/modernpython/lang_pack.py @@ -1,5 +1,4 @@ import logging -import os import re from distutils.dir_util import copy_tree from pathlib import Path @@ -35,9 +34,12 @@ def location(version): # NOSONAR base = {"base_class": "Base", "class_location": location} # 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"}] +class_template_file = {"filename": "class_template.mustache", "ext": ".py"} +constants_template_file = {"filename": "constants_template.mustache", "ext": ".py"} +profile_template_file = {"filename": "profile_template.mustache", "ext": ".py"} +enum_template_file = {"filename": "enum_template.mustache", "ext": ".py"} +primitive_template_file = {"filename": "primitive_template.mustache", "ext": ".py"} +datatype_template_file = {"filename": "datatype_template.mustache", "ext": ".py"} def get_class_location(class_name, class_map, version): # NOSONAR @@ -76,23 +78,73 @@ def _get_type_and_default(text, render) -> tuple[str, str]: return ("str", 'default=""') +def _get_python_type(datatype): + if datatype.lower() == "integer": + return "int" + if datatype.lower() == "boolean": + return "bool" + if datatype.lower() == "datetime": + return "datetime" + if datatype.lower() == "date": + return "date" + if datatype.lower() == "time": + return "time" + if datatype.lower() == "monthday": + return "str" # TO BE FIXED? I could not find a datatype in python that holds only month and day. + if datatype.lower() == "string": + return "str" + else: + # everything else should be a float + return "float" + + +def _set_imports(attributes): + import_set = set() + for attribute in attributes: + if attribute["is_datatype_attribute"] or attribute["is_primitive_attribute"]: + import_set.add(attribute["attribute_class"]) + return sorted(import_set) + + +def _set_datatype_attributes(attributes) -> dict: + datatype_attributes = {} + datatype_attributes["python_type"] = "None" + import_set = set() + for attribute in attributes: + if "value" in attribute.get("about", "") and "attribute_class" in attribute: + datatype_attributes["python_type"] = _get_python_type(attribute["attribute_class"]) + if "isFixed" in attribute: + import_set.add(attribute["attribute_class"]) + datatype_attributes["isFixed_imports"] = sorted(import_set) + return datatype_attributes + + def run_template(output_path, class_details): - if class_details["is_a_primitive_class"] or class_details["is_a_datatype_class"]: - return - for template_info in template_files: - resource_file = Path( - os.path.join( - output_path, - "resources", - class_details["class_name"] + template_info["ext"], - ) - ) - if not resource_file.exists(): - if not (parent := resource_file.parent).exists(): - parent.mkdir() + if class_details["is_a_primitive_class"]: + # Primitives are never used in the in memory representation but only for + # the schema + template = primitive_template_file + class_details["python_type"] = _get_python_type(class_details["class_name"]) + elif class_details["is_a_datatype_class"]: + # Datatypes based on primitives are never used in the in memory + # representation but only for the schema + template = datatype_template_file + class_details.update(_set_datatype_attributes(class_details["attributes"])) + elif class_details["is_an_enum_class"]: + template = enum_template_file + else: + template = class_template_file class_details["setDefault"] = _set_default class_details["setType"] = _set_type - _write_templated_file(resource_file, class_details, template_info["filename"]) + class_details["imports"] = _set_imports(class_details["attributes"]) + resource_file = _create_file(output_path, class_details, template) + _write_templated_file(resource_file, class_details, template["filename"]) + + +def _create_file(output_path, class_details, template) -> str: + resource_file = Path(output_path) / "resources" / (class_details["class_name"] + template["ext"]) + resource_file.parent.mkdir(exist_ok=True) + return str(resource_file) def _write_templated_file(class_file, class_details, template_filename): @@ -109,17 +161,15 @@ def _write_templated_file(class_file, class_details, template_filename): 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"]) + class_file = Path(output_path) / "utils" / ("constants" + constants_template_file["ext"]) + class_details = {"cim_namespace": cim_namespace} + _write_templated_file(class_file, class_details, constants_template_file["filename"]) 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"]) + class_file = Path(output_path) / "utils" / ("profile" + profile_template_file["ext"]) + class_details = {"profiles": profile_details} + _write_templated_file(class_file, class_details, profile_template_file["filename"]) def resolve_headers(path: str, version: str): diff --git a/cimgen/languages/modernpython/templates/cimpy_class_template.mustache b/cimgen/languages/modernpython/templates/class_template.mustache similarity index 89% rename from cimgen/languages/modernpython/templates/cimpy_class_template.mustache rename to cimgen/languages/modernpython/templates/class_template.mustache index 58ba391d..59561703 100644 --- a/cimgen/languages/modernpython/templates/cimpy_class_template.mustache +++ b/cimgen/languages/modernpython/templates/class_template.mustache @@ -10,6 +10,9 @@ from pydantic.dataclasses import dataclass from ..utils.profile import BaseProfile, Profile from {{class_location}} import {{sub_class_of}} +{{#imports}} +from .{{.}} import {{.}} +{{/imports}} @dataclass @@ -37,6 +40,12 @@ class {{class_name}}({{sub_class_of}}): "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_datatype_attribute}} + "attribute_class": {{attribute_class}}, +{{/is_datatype_attribute}} +{{#is_primitive_attribute}} + "attribute_class": {{attribute_class}}, +{{/is_primitive_attribute}} }, ) diff --git a/cimgen/languages/modernpython/templates/cimpy_constants_template.mustache b/cimgen/languages/modernpython/templates/constants_template.mustache similarity index 100% rename from cimgen/languages/modernpython/templates/cimpy_constants_template.mustache rename to cimgen/languages/modernpython/templates/constants_template.mustache diff --git a/cimgen/languages/modernpython/templates/datatype_template.mustache b/cimgen/languages/modernpython/templates/datatype_template.mustache new file mode 100644 index 00000000..6d690217 --- /dev/null +++ b/cimgen/languages/modernpython/templates/datatype_template.mustache @@ -0,0 +1,27 @@ +""" +Generated from the CGMES files via cimgen: https://github.com/sogno-platform/cimgen +""" + +from ..utils.datatypes import CIMDatatype +from ..utils.profile import Profile +{{#isFixed_imports}} +from .{{.}} import {{.}} +{{/isFixed_imports}} + + +{{class_name}} = CIMDatatype( + name="{{class_name}}", + type={{python_type}}, +{{#attributes}} +{{#isFixed}} + {{label}}={{attribute_class}}.{{isFixed}}, +{{/isFixed}} +{{/attributes}} + profiles=[{{#class_origin}} + Profile.{{origin}},{{/class_origin}} + ], +) + +""" +{{{wrapped_class_comment}}} +""" diff --git a/cimgen/languages/modernpython/templates/enum_template.mustache b/cimgen/languages/modernpython/templates/enum_template.mustache new file mode 100644 index 00000000..d0b3826c --- /dev/null +++ b/cimgen/languages/modernpython/templates/enum_template.mustache @@ -0,0 +1,15 @@ +""" +Generated from the CGMES files via cimgen: https://github.com/sogno-platform/cimgen +""" + +from enum import Enum + + +class {{class_name}}(str, Enum): + """ + {{{class_comment}}} # noqa: E501 + """ + + {{#enum_instances}} + {{label}} = "{{label}}"{{#comment}} # {{comment}}{{/comment}} # noqa: E501 + {{/enum_instances}} diff --git a/cimgen/languages/modernpython/templates/primitive_template.mustache b/cimgen/languages/modernpython/templates/primitive_template.mustache new file mode 100644 index 00000000..be44ab00 --- /dev/null +++ b/cimgen/languages/modernpython/templates/primitive_template.mustache @@ -0,0 +1,19 @@ +""" +Generated from the CGMES files via cimgen: https://github.com/sogno-platform/cimgen +""" + +from datetime import date, datetime, time +from ..utils.datatypes import Primitive +from ..utils.profile import Profile + +{{class_name}} = Primitive( + name="{{class_name}}", + type={{python_type}}, + profiles=[{{#class_origin}} + Profile.{{origin}},{{/class_origin}} + ], +) + +""" +{{{wrapped_class_comment}}} +""" diff --git a/cimgen/languages/modernpython/templates/cimpy_cgmesProfile_template.mustache b/cimgen/languages/modernpython/templates/profile_template.mustache similarity index 95% rename from cimgen/languages/modernpython/templates/cimpy_cgmesProfile_template.mustache rename to cimgen/languages/modernpython/templates/profile_template.mustache index 0c0db152..0ecc6799 100644 --- a/cimgen/languages/modernpython/templates/cimpy_cgmesProfile_template.mustache +++ b/cimgen/languages/modernpython/templates/profile_template.mustache @@ -10,7 +10,7 @@ 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 + All CGMES 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. """ diff --git a/cimgen/languages/modernpython/utils/base.py b/cimgen/languages/modernpython/utils/base.py index e44b78ba..f6f75703 100644 --- a/cimgen/languages/modernpython/utils/base.py +++ b/cimgen/languages/modernpython/utils/base.py @@ -6,9 +6,9 @@ from pydantic.dataclasses import dataclass -from pycgmes.utils.constants import NAMESPACES +from .constants import NAMESPACES -from ..utils.config import cgmes_resource_config +from .config import cgmes_resource_config from .profile import BaseProfile diff --git a/cimgen/languages/modernpython/utils/chevron_writer.py b/cimgen/languages/modernpython/utils/chevron_writer.py index acd7b48f..103d9175 100644 --- a/cimgen/languages/modernpython/utils/chevron_writer.py +++ b/cimgen/languages/modernpython/utils/chevron_writer.py @@ -3,9 +3,9 @@ import chevron -from pycgmes.utils.base import Base -from pycgmes.utils.constants import NAMESPACES -from pycgmes.utils.profile import BaseProfile, Profile +from .base import Base +from .constants import NAMESPACES +from .profile import BaseProfile, Profile class ChevronWriter: diff --git a/cimgen/languages/modernpython/utils/datatypes.py b/cimgen/languages/modernpython/utils/datatypes.py new file mode 100644 index 00000000..db3d6ba6 --- /dev/null +++ b/cimgen/languages/modernpython/utils/datatypes.py @@ -0,0 +1,29 @@ +from pydantic import Field +from typing import List, Optional, Union + +from .constants import NAMESPACES +from pydantic.dataclasses import dataclass + +from .config import cgmes_resource_config +from .profile import BaseProfile +from ..resources.UnitMultiplier import UnitMultiplier +from ..resources.UnitSymbol import UnitSymbol +from ..resources.Currency import Currency + + +@dataclass(config=cgmes_resource_config) +class Primitive: + + name: str = Field(frozen=True) + type: object = Field(frozen=True) + namespace: str = Field(frozen=True, default=NAMESPACES["cim"]) + profiles: List[BaseProfile] = Field(frozen=True) + + +@dataclass(config=cgmes_resource_config) +class CIMDatatype(Primitive): + + multiplier: Optional[UnitMultiplier] = Field(default=None, frozen=True) + unit: Optional[Union[UnitSymbol, Currency]] = Field(default=None, frozen=True) + denominatorMultiplier: Optional[UnitMultiplier] = Field(default=None, frozen=True) + denominatorUnit: Optional[UnitSymbol] = Field(default=None, frozen=True)