Skip to content

Commit

Permalink
support single file generation for enum and schema
Browse files Browse the repository at this point in the history
Signed-off-by: Federico M. Facca <federico.facca@zaphiro.ch>
  • Loading branch information
chicco785 committed Oct 17, 2023
1 parent a1ccc79 commit 430e349
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 50 deletions.
17 changes: 16 additions & 1 deletion modernpython/Base.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,26 @@ class DataclassConfig: # pylint: disable=too-few-public-methods

# By default with pydantic extra arguments given to a dataclass are silently ignored.
# This matches the default behaviour by failing noisily.
extra = "ignore"
extra = "forbid"
populate_by_name = True
defer_build = True
from_attributes = True

class GeoDataclassConfig: # pylint: disable=too-few-public-methods
"""
Used to configure pydantic dataclasses.
See doc at
https://docs.pydantic.dev/latest/usage/model_config/#options
"""

# By default with pydantic extra arguments given to a dataclass are silently ignored.
# This matches the default behaviour by failing noisily.
extra = "ignore"
populate_by_name = True
defer_build = True
from_attributes = True
arbitrary_types_allowed=True

# Default namespaces used by CGMES.
NAMESPACES = {
Expand Down
2 changes: 2 additions & 0 deletions modernpython/enum_header.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from enum import Enum, IntEnum

81 changes: 51 additions & 30 deletions modernpython/langPack.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ def location(version):
template_files = [{"filename": "cimpy_class_template.mustache", "ext": ".py"}]
enum_template_files = [{"filename": "pydantic_enum_template.mustache", "ext": ".py"}]

required_profiles = ["EQ", "GL"] #temporary

def get_class_location(class_name, class_map, version):
# Check if the current class has a parent class
if class_map[class_name].superClass():
Expand All @@ -53,7 +55,7 @@ def _compute_data_type(attribute):

if "range" in attribute:
# return "'"+attribute["range"].split("#")[1]+"'"
return "'"+attribute["range"].split("#")[1]+"'"
return attribute["range"].split("#")[1]
if "dataType" in attribute and "class_name" in attribute:
# for whatever weird reason String is not created as class from CIMgen
if is_primitive_class(attribute["class_name"]) or attribute["class_name"] == "String":
Expand Down Expand Up @@ -84,7 +86,7 @@ def _compute_data_type(attribute):
if is_cim_data_type_class(attribute["class_name"]):
return "float"
# this is for example the case for 'StreetAddress.streetDetail'
return "'"+attribute["dataType"].split("#")[1]+"'"
return attribute["dataType"].split("#")[1]

def _ends_with_s(attribute_name):
return attribute_name.endswith("s")
Expand Down Expand Up @@ -252,6 +254,12 @@ def has_unit_attribute(attributes):
return True
return False

def is_required_profile(class_origin):
for origin in class_origin:
if origin["origin"] in required_profiles:
return True
return False

def run_template(version_path, class_details):
if (
# Primitives are never used in the in memory representation but only for
Expand All @@ -260,6 +268,7 @@ def run_template(version_path, class_details):
# Datatypes based on primitives are never used in the in memory
# representation but only for the schema
or class_details["is_a_cim_data_type"] == True
or class_details["class_name"] == 'PositionPoint'
):
return
elif class_details["has_instances"] == True:
Expand All @@ -269,47 +278,59 @@ def run_template(version_path, class_details):

def run_template_enum(version_path, class_details, templates):
for template_info in templates:
class_file = os.path.join(version_path, class_details["class_name"] + template_info["ext"])
class_file = os.path.join(version_path, "enum" + template_info["ext"])
if not os.path.exists(class_file):
with open(class_file, "w", encoding="utf-8") as file:
template_path = os.path.join(os.getcwd(), "modernpython/templates", template_info["filename"])
class_details["setInstances"] = _set_instances
with open(template_path, encoding="utf-8") as f:
args = {
"data": class_details,
"template": f,
"partials_dict": partials,
}
output = chevron.render(**args)
file.write(output)
header_file_path = os.path.join(
os.getcwd(), "modernpython", "enum_header.py"
)
header_file = open(header_file_path, "r")
file.write(header_file.read())
with open(class_file, "a", encoding="utf-8") as file:
template_path = os.path.join(os.getcwd(), "modernpython/templates", template_info["filename"])
class_details["setInstances"] = _set_instances
with open(template_path, encoding="utf-8") as f:
args = {
"data": class_details,
"template": f,
"partials_dict": partials,
}
output = chevron.render(**args)
file.write(output)

def run_template_schema(version_path, class_details, templates):
for template_info in templates:
class_file = os.path.join(version_path, class_details["class_name"] + template_info["ext"])
class_file = os.path.join(version_path, "schema" + template_info["ext"])
if not os.path.exists(class_file):
with open(class_file, "w", encoding="utf-8") as file:
template_path = os.path.join(os.getcwd(), "modernpython/templates", template_info["filename"])
class_details["setDefault"] = _set_default
class_details["setType"] = _set_type
class_details["setImports"] = _set_imports
class_details["setValidator"] = _set_validator
class_details["setNormalizedName"] = _set_normalized_name
with open(template_path, encoding="utf-8") as f:
args = {
"data": class_details,
"template": f,
"partials_dict": partials,
}
output = chevron.render(**args)
file.write(output)
schema_file_path = os.path.join(
os.getcwd(), "modernpython", "schema_header.py"
)
schema_file = open(schema_file_path, "r")
file.write(schema_file.read())
with open(class_file, "a", encoding="utf-8") as file:
template_path = os.path.join(os.getcwd(), "modernpython/templates", template_info["filename"])
class_details["setDefault"] = _set_default
class_details["setType"] = _set_type
class_details["setImports"] = _set_imports
class_details["setValidator"] = _set_validator
class_details["setNormalizedName"] = _set_normalized_name
with open(template_path, encoding="utf-8") as f:
args = {
"data": class_details,
"template": f,
"partials_dict": partials,
}
output = chevron.render(**args)
file.write(output)


def _create_init(path):
init_file = path + "/__init__.py"

with open(init_file, "w", encoding="utf-8") as init:
init.write("# pylint: disable=too-many-lines,missing-module-docstring\n")

#init.write("# pylint: disable=too-many-lines,missing-module-docstring\n")
pass

# creates the Base class file, all classes inherit from this class
def _create_base(path):
Expand Down
67 changes: 67 additions & 0 deletions modernpython/schema_header.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from __future__ import annotations
import uuid
from functools import cached_property
from pydantic import ConfigDict, Field, field_validator, computed_field
from geoalchemy2.shape import to_shape
from geoalchemy2.elements import WKBElement
from shapely.geometry import Point
from datetime import date, datetime, time
from typing import Optional, Iterator, List
from pydantic.dataclasses import dataclass
from .Base import DataclassConfig, GeoDataclassConfig, Profile, Base
from .util import cyclic_references_validator
from .enum import *

@dataclass(config=GeoDataclassConfig)
class PositionPoint(Base):
"""
Set of spatial coordinates that determine a point, defined in the coordinate system specified in 'Location.CoordinateSystem'. Use a single position point instance to desribe a point-oriented location. Use a sequence of position points to describe a line-oriented object (physical location of non-point oriented objects like cables or lines), or area of an object (like a substation or a geographical zone - in this case, have first and last position point with the same values).
:Location: Location described by this position point.
:sequenceNumber: Zero-relative sequence number of this point within a series of points.
:xPosition: X axis position.
:yPosition: Y axis position.
:zPosition: (if applicable) Z axis position.
"""

location: "Location" = Field(alias="Location", in_profiles = [Profile.GL, ])
sequenceNumber: Optional[int] = Field(default=None, in_profiles = [Profile.GL, ])
point: Point = Field(
repr=False, in_profiles = [Profile.GL, ]
) # we introduce this field compared to CIM definition because we want to store a proper geometry "point" in the database

@computed_field
@property
def xPosition(self) -> str:
return str(self.point.x)

@computed_field
@property
def yPosition(self) -> str:
return str(self.point.y)

@computed_field
@property
def zPosition(self) -> str:
return str(self.point.z)

@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.GL, }

# Pydantic needs help to map GeoAlchemy classes to Shapely
@field_validator("point", mode="before")
def validate_point_format(cls, v):
if isinstance(v, Point):
return v
elif isinstance(v, WKBElement):
point = to_shape(v)
if point.geom_type != "Point":
raise ValueError("must be a Point")
return Point(point)
else:
raise ValueError("must be a Point or a WKBElement")
18 changes: 1 addition & 17 deletions modernpython/templates/cimpy_class_template.mustache
Original file line number Diff line number Diff line change
@@ -1,18 +1,8 @@

"""
Generated from the CGMES 3 files via cimgen: https://github.com/sogno-platform/cimgen
"""

from __future__ import annotations
from functools import cached_property
from typing import Optional, List
from pydantic import Field, field_validator
import uuid
from datetime import date, datetime, time
from pydantic.dataclasses import dataclass, rebuild_dataclass
from .Base import DataclassConfig, Profile
from .util import cyclic_references_validator
from .{{sub_class_of}} import {{sub_class_of}}

@dataclass(config=DataclassConfig)
class {{class_name}}({{sub_class_of}}):
"""
Expand All @@ -37,16 +27,10 @@ class {{class_name}}({{sub_class_of}}):
{{^attributes}}
# No attributes defined for this class.
{{/attributes}}


@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 { {{#class_origin}}Profile.{{origin}}, {{/class_origin}} }

{{#setImports}}{{attributes}}{{/setImports}}

rebuild_dataclass({{class_name}})
3 changes: 1 addition & 2 deletions modernpython/templates/pydantic_enum_template.mustache
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@

"""
Generated from the CGMES 3 files via cimgen: https://github.com/sogno-platform/cimgen
"""

from enum import Enum

class {{class_name}}(str,Enum):

'''
Expand Down

0 comments on commit 430e349

Please sign in to comment.