diff --git a/src/shacl2code/lang/common.py b/src/shacl2code/lang/common.py index c7486d5b..1c49fa75 100644 --- a/src/shacl2code/lang/common.py +++ b/src/shacl2code/lang/common.py @@ -22,6 +22,11 @@ THIS_DIR = Path(__file__).parent +def prop_is_list(prop): + """Whether a property's max_count allows more than one value.""" + return prop.max_count is None or prop.max_count != 1 + + class OutputFile(object): def __init__(self, path): self.path = path @@ -148,6 +153,7 @@ def get_all_named_individuals(cls): "get_all_derived": get_all_derived, "get_all_named_individuals": get_all_named_individuals, "include_file": include_file, + "prop_is_list": prop_is_list, **self.get_extra_env(), } diff --git a/src/shacl2code/lang/cpp.py b/src/shacl2code/lang/cpp.py index d44955ee..2b311ce7 100644 --- a/src/shacl2code/lang/cpp.py +++ b/src/shacl2code/lang/cpp.py @@ -37,10 +37,6 @@ def parent_cpp_classes(cls, classes): return parents -def prop_is_list(prop): - return prop.max_count is None or prop.max_count != 1 - - def id_str(s): return re.sub(r"[^a-zA-Z0-9_]", "_", s) @@ -182,7 +178,6 @@ def suffix(s): def get_extra_env(self): return { "varname": varname, - "prop_is_list": prop_is_list, "parent_cpp_classes": parent_cpp_classes, "macro_prefix": self.macro_prefix, "api_def_begin": comment_wrap(textwrap.dedent(f"""\ diff --git a/src/shacl2code/lang/golang.py b/src/shacl2code/lang/golang.py index a10d11ec..89965ef4 100644 --- a/src/shacl2code/lang/golang.py +++ b/src/shacl2code/lang/golang.py @@ -8,7 +8,7 @@ import textwrap from pathlib import Path -from .common import JinjaTemplateRender +from .common import JinjaTemplateRender, prop_is_list from .lang import TEMPLATE_DIR, language GO_KEYWORDS = ( @@ -75,10 +75,6 @@ def prop_name(prop): return varname(prop.varname, public=False) -def prop_is_list(prop): - return prop.max_count is None or prop.max_count != 1 - - def prop_go_type(prop, classes): if prop.enum_values: return "string" @@ -276,7 +272,6 @@ def get_extra_env(self): "interface_name": interface_name, "class_type_var": class_type_var, "prop_name": prop_name, - "prop_is_list": prop_is_list, "prop_go_type": prop_go_type, "prop_ctx_name": prop_ctx_name, "prop_decode_func": prop_decode_func, diff --git a/src/shacl2code/lang/python.py b/src/shacl2code/lang/python.py index 29521e23..cad9f41d 100644 --- a/src/shacl2code/lang/python.py +++ b/src/shacl2code/lang/python.py @@ -75,6 +75,19 @@ def varname(*name): return name +def prop_element_pytype(prop, classes): + """Python type of a single element of prop, ignoring container shape. + + Object-reference properties resolve to ``Union[str, 'ClassName']``, since + they may be set from either an id string or the referenced object. + """ + if prop.enum_values: + return "str" + if prop.class_id: + return "Union[str, '" + varname(*classes.get(prop.class_id).clsname) + "']" + return DATATYPE_PYTHON_TYPES[prop.datatype] + + @language("python") class PythonRender(JinjaTemplateRender): """Render Python Language Bindings.""" @@ -144,6 +157,7 @@ def get_file(name): def get_extra_env(self): return { "varname": varname, + "prop_element_pytype": prop_element_pytype, "DATATYPE_CLASSES": DATATYPE_CLASSES, "DATATYPE_PYTHON_TYPES": DATATYPE_PYTHON_TYPES, } diff --git a/src/shacl2code/lang/rust.py b/src/shacl2code/lang/rust.py index c3e46c7d..4aa5e3e6 100644 --- a/src/shacl2code/lang/rust.py +++ b/src/shacl2code/lang/rust.py @@ -7,7 +7,7 @@ import re from pathlib import Path -from .common import JinjaTemplateRender +from .common import JinjaTemplateRender, prop_is_list from .lang import TEMPLATE_DIR, language RUST_KEYWORDS = ( @@ -108,11 +108,6 @@ def prop_field_name(prop): return varname(prop.varname) -def prop_is_list(prop): - """Check if a property is a list.""" - return prop.max_count is None or prop.max_count != 1 - - def prop_rust_type(prop, classes): """Get the Rust type for a property's data type.""" if prop.enum_values: @@ -268,7 +263,6 @@ def get_extra_env(self): "type_name": type_name, "struct_name": struct_name, "prop_field_name": prop_field_name, - "prop_is_list": prop_is_list, "prop_rust_type": prop_rust_type, "prop_full_type": prop_full_type, "const_name": const_name, diff --git a/src/shacl2code/lang/templates/python/_macros.j2 b/src/shacl2code/lang/templates/python/_macros.j2 new file mode 100644 index 00000000..e6fd9691 --- /dev/null +++ b/src/shacl2code/lang/templates/python/_macros.j2 @@ -0,0 +1,13 @@ +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2026 Joshua Watt +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: MIT +# +# Shared Jinja macros for the Python templates. +{% macro class_docstring(comment) %} + {{ '"' }}{{ '"' }}{{ '"' }} + {%- for l in comment.split("\n") %} + {{ l.rstrip() }} + {%- endfor %} + {{ '"' }}{{ '"' }}{{ '"' }} +{%- endmacro %} diff --git a/src/shacl2code/lang/templates/python/model.py.j2 b/src/shacl2code/lang/templates/python/model.py.j2 index ea45b3f9..17cae1a3 100644 --- a/src/shacl2code/lang/templates/python/model.py.j2 +++ b/src/shacl2code/lang/templates/python/model.py.j2 @@ -1,5 +1,5 @@ #! /usr/bin/env python3 -# +# {%- import "_macros.j2" as pymacros %} # {{ disclaimer }} # # SPDX-License-Identifier: {{ spdx_license }} @@ -3042,12 +3042,7 @@ class {{ varname(*class.clsname) }}( {%- else -%} SHACLObject {%- endif -%}): -{%- if class.comment %} - {{ '"' }}{{ '"' }}{{ '"' }} - {%- for l in class.comment.split("\n") %} - {{ l.rstrip() }} - {%- endfor %} - {{ '"' }}{{ '"' }}{{ '"' }} +{%- if class.comment %}{{ pymacros.class_docstring(class.comment) }} {% endif %} TYPE: ClassVar[str] = "{{ class._id }}" {%- if context.compact_iri(class._id) != class._id %} @@ -3080,7 +3075,7 @@ class {{ varname(*class.clsname) }}( {%- if class.properties %} PROPERTIES: ClassVar[List[ClassProp]] = [ {%- for prop in class.properties %} - {%- set is_list = prop.max_count is none or prop.max_count != 1 %} + {%- set is_list = prop_is_list(prop) %} {%- if prop.comment %} {%- for l in prop.comment.split("\n") %} #{{ (" " + l).rstrip() }} diff --git a/src/shacl2code/lang/templates/python/model.pyi.j2 b/src/shacl2code/lang/templates/python/model.pyi.j2 index d257007f..365f41b5 100644 --- a/src/shacl2code/lang/templates/python/model.pyi.j2 +++ b/src/shacl2code/lang/templates/python/model.pyi.j2 @@ -1,5 +1,5 @@ # {{ disclaimer }} -# +# {%- import "_macros.j2" as pymacros %} # SPDX-License-Identifier: {{ spdx_license }} """Generated stub for the generated Python bindings.""" @@ -412,12 +412,7 @@ class {{ varname(*class.clsname) }}( {%- else -%} SHACLObject {%- endif -%}): -{%- if class.comment %} - {{ '"' }}{{ '"' }}{{ '"' }} - {%- for l in class.comment.split("\n") %} - {{ l.rstrip() }} - {%- endfor %} - {{ '"' }}{{ '"' }}{{ '"' }} +{%- if class.comment %}{{ pymacros.class_docstring(class.comment) }} {% endif %} TYPE: ClassVar[str] = "{{ class._id }}" {%- if context.compact_iri(class._id) != class._id %} @@ -458,14 +453,8 @@ class {{ varname(*class.clsname) }}( {{ class.id_property }}: Optional[str] = None, {%- endif %} {%- for prop in class.properties %} - {%- set is_list = prop.max_count is none or prop.max_count != 1 %} - {%- if prop.enum_values %} - {%- set ptype = "str" %} - {%- elif prop.class_id %} - {%- set ptype = "Union[str, '" ~ varname(*classes.get(prop.class_id).clsname) ~ "']" %} - {%- else %} - {%- set ptype = DATATYPE_PYTHON_TYPES[prop.datatype] %} - {%- endif %} + {%- set is_list = prop_is_list(prop) %} + {%- set ptype = prop_element_pytype(prop, classes) %} {%- if is_list %} {{ varname(prop.varname) }}: Optional[Iterable[{{ ptype }}]] = None, {%- else %} @@ -480,14 +469,8 @@ class {{ varname(*class.clsname) }}( {{ class.id_property }}: Optional[str] {%- endif %} {%- for prop in class.properties %} - {%- set is_list = prop.max_count is none or prop.max_count != 1 %} - {%- if prop.enum_values %} - {%- set ptype = "str" %} - {%- elif prop.class_id %} - {%- set ptype = "Union[str, '" ~ varname(*classes.get(prop.class_id).clsname) ~ "']" %} - {%- else %} - {%- set ptype = DATATYPE_PYTHON_TYPES[prop.datatype] %} - {%- endif %} + {%- set is_list = prop_is_list(prop) %} + {%- set ptype = prop_element_pytype(prop, classes) %} {%- if is_list %} @property def {{ varname(prop.varname) }}(self) -> ListProxy[{{ ptype }}]: ...