From be4b075b3261d14e27847bc5157602bbe004a320 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Wed, 26 Aug 2026 15:05:46 +0700 Subject: [PATCH 1/4] Unify prop_is_list function prop_is_list functions are defined in cpp, golang, python, rust. Move it to common and share. Signed-off-by: Arthit Suriyawongkul --- src/shacl2code/lang/common.py | 6 ++++ src/shacl2code/lang/cpp.py | 5 ---- src/shacl2code/lang/golang.py | 7 +---- src/shacl2code/lang/python.py | 14 ++++++++++ src/shacl2code/lang/rust.py | 8 +----- .../lang/templates/python/_macros.j2 | 14 ++++++++++ .../lang/templates/python/model.py.j2 | 10 ++----- .../lang/templates/python/model.pyi.j2 | 28 ++++--------------- 8 files changed, 45 insertions(+), 47 deletions(-) create mode 100644 src/shacl2code/lang/templates/python/_macros.j2 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..4ff7a6a6 --- /dev/null +++ b/src/shacl2code/lang/templates/python/_macros.j2 @@ -0,0 +1,14 @@ +{# +Copyright (c) 2026 Joshua Watt + +SPDX-License-Identifier: MIT + +Shared Jinja macros for the Python model.py and model.pyi 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..c7f0f2bf 100644 --- a/src/shacl2code/lang/templates/python/model.py.j2 +++ b/src/shacl2code/lang/templates/python/model.py.j2 @@ -1,3 +1,4 @@ +{%- import "_macros.j2" as pymacros -%} #! /usr/bin/env python3 # # {{ disclaimer }} @@ -3042,12 +3043,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 +3076,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..418bbc20 100644 --- a/src/shacl2code/lang/templates/python/model.pyi.j2 +++ b/src/shacl2code/lang/templates/python/model.pyi.j2 @@ -1,3 +1,4 @@ +{%- import "_macros.j2" as pymacros -%} # {{ disclaimer }} # # SPDX-License-Identifier: {{ spdx_license }} @@ -412,12 +413,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 +454,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 +470,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 }}]: ... From 699898f7b8da029c3c6833f8f8861ad4c5d1723a Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Wed, 26 Aug 2026 15:15:24 +0700 Subject: [PATCH 2/4] Fix formatting Signed-off-by: Arthit Suriyawongkul --- src/shacl2code/lang/templates/python/model.py.j2 | 3 +-- src/shacl2code/lang/templates/python/model.pyi.j2 | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/shacl2code/lang/templates/python/model.py.j2 b/src/shacl2code/lang/templates/python/model.py.j2 index c7f0f2bf..17cae1a3 100644 --- a/src/shacl2code/lang/templates/python/model.py.j2 +++ b/src/shacl2code/lang/templates/python/model.py.j2 @@ -1,6 +1,5 @@ -{%- import "_macros.j2" as pymacros -%} #! /usr/bin/env python3 -# +# {%- import "_macros.j2" as pymacros %} # {{ disclaimer }} # # SPDX-License-Identifier: {{ spdx_license }} diff --git a/src/shacl2code/lang/templates/python/model.pyi.j2 b/src/shacl2code/lang/templates/python/model.pyi.j2 index 418bbc20..365f41b5 100644 --- a/src/shacl2code/lang/templates/python/model.pyi.j2 +++ b/src/shacl2code/lang/templates/python/model.pyi.j2 @@ -1,6 +1,5 @@ -{%- import "_macros.j2" as pymacros -%} # {{ disclaimer }} -# +# {%- import "_macros.j2" as pymacros %} # SPDX-License-Identifier: {{ spdx_license }} """Generated stub for the generated Python bindings.""" From be0d1fed6f1a8b3eb80ab28b9f7ecfcd8c027d7c Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Thu, 27 Aug 2026 01:10:09 +0700 Subject: [PATCH 3/4] Exclude macro comments from the generated output Signed-off-by: Arthit Suriyawongkul --- src/shacl2code/lang/templates/python/_macros.j2 | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/shacl2code/lang/templates/python/_macros.j2 b/src/shacl2code/lang/templates/python/_macros.j2 index 4ff7a6a6..3b5a0e27 100644 --- a/src/shacl2code/lang/templates/python/_macros.j2 +++ b/src/shacl2code/lang/templates/python/_macros.j2 @@ -1,10 +1,8 @@ -{# -Copyright (c) 2026 Joshua Watt - -SPDX-License-Identifier: MIT - -Shared Jinja macros for the Python model.py and model.pyi templates. -#} +# Copyright (c) 2026 Joshua Watt +# +# SPDX-License-Identifier: MIT +# +# Shared Jinja macros for the Python templates. {% macro class_docstring(comment) %} {{ '"' }}{{ '"' }}{{ '"' }} {%- for l in comment.split("\n") %} From 98007a3fb2d7e46f5deeacbcceb707c3d9fb5ee2 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Thu, 27 Aug 2026 01:27:27 +0700 Subject: [PATCH 4/4] Add SPDX header Signed-off-by: Arthit Suriyawongkul --- src/shacl2code/lang/templates/python/_macros.j2 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/shacl2code/lang/templates/python/_macros.j2 b/src/shacl2code/lang/templates/python/_macros.j2 index 3b5a0e27..e6fd9691 100644 --- a/src/shacl2code/lang/templates/python/_macros.j2 +++ b/src/shacl2code/lang/templates/python/_macros.j2 @@ -1,5 +1,6 @@ -# Copyright (c) 2026 Joshua Watt -# +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2026 Joshua Watt +# SPDX-FileType: SOURCE # SPDX-License-Identifier: MIT # # Shared Jinja macros for the Python templates.