Skip to content

Commit 53e828a

Browse files
authored
Merge pull request #573 from BESSER-PEARL/development
release: v7.11.2 — B-UML export keeps every object model in multi-object projects
2 parents 9a8044c + c01dda1 commit 53e828a

6 files changed

Lines changed: 265 additions & 97 deletions

File tree

besser/utilities/buml_code_builder/domain_model_builder.py

Lines changed: 137 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -561,91 +561,150 @@ def _emit_constraint_decl(constraint, var_name: str) -> None:
561561

562562
# Generate object model code if provided
563563
if objectmodel:
564-
f.write("\n################\n")
565-
f.write("# OBJECT MODEL #\n")
566-
f.write("################\n")
567-
568-
# Write object instances using fluent API
569-
for obj in sorted(objectmodel.objects, key=lambda x: x.name_):
570-
obj_var_name = f"{obj.name_.lower()}_obj"
571-
classifier_var_name = safe_class_name(obj.classifier.name)
572-
573-
# Start the fluent API call using the proper syntax: Class("name")
574-
f.write(f"{obj_var_name} = {classifier_var_name}(\"{_escape_python_string(obj.name_)}\")")
575-
576-
# Add attributes if the object has slots
577-
if obj.slots:
578-
attributes_dict = {}
579-
for slot in obj.slots:
580-
attr_name = slot.attribute.name
581-
582-
# Format the value based on type
583-
if isinstance(slot.value.value, str):
584-
value_str = f'"{_escape_python_string(slot.value.value)}"'
585-
elif hasattr(slot.value.value, 'isoformat'): # datetime objects
586-
value_str = f'datetime.datetime.fromisoformat("{slot.value.value.isoformat()}")'
587-
elif hasattr(slot.value.value, 'owner') and hasattr(slot.value.value.owner, 'name'):
588-
# This is an enumeration literal - generate the proper reference
589-
enum_name = slot.value.value.owner.name
590-
literal_name = slot.value.value.name
591-
value_str = f"{enum_name}.{literal_name}"
592-
else:
593-
value_str = str(slot.value.value)
564+
_write_object_model_section(f, objectmodel, object_model_var_name)
594565

595-
attributes_dict[attr_name] = value_str
566+
print(f"BUML model saved to {file_path}")
596567

597-
# Add attributes to the fluent API call
598-
if attributes_dict:
599-
attr_pairs = [f"{k}={v}" for k, v in attributes_dict.items()]
600-
f.write(f".attributes({', '.join(attr_pairs)})")
601568

602-
# Complete the fluent API call
603-
f.write(".build()\n")
569+
def _write_object_model_section(f, objectmodel: ObjectModel, object_model_var_name: str = "object_model"):
570+
"""Write the object-model portion of the builder output to an open file handle.
604571
605-
f.write("\n")
572+
Emits the ``# OBJECT MODEL #`` banner, the object instances (via the fluent
573+
API), the links between objects, and the final ``ObjectModel(...)`` binding.
574+
This section assumes the referenced domain classes and enumerations are
575+
already defined earlier in the same (concatenated) file.
606576
607-
# Add links after objects are created (avoiding forward reference issues)
608-
if hasattr(objectmodel, 'links') and objectmodel.links:
609-
610-
# Group links by (source_obj_var, end_name)
611-
grouped_links = {}
612-
for link in objectmodel.links:
613-
if len(link.connections) == 2:
614-
end1, end2 = link.connections
615-
obj1_var = f"{end1.object.name_.lower()}_obj"
616-
obj2_var = f"{end2.object.name_.lower()}_obj"
617-
end2_name = end2.association_end.name
618-
619-
key = (obj1_var, end2_name)
620-
grouped_links.setdefault(key, set()).add(obj2_var)
621-
622-
# Write assignments for each group
623-
for (obj_var, end_name), targets in grouped_links.items():
624-
if len(targets) == 1:
625-
[single_target] = targets
626-
f.write(f"{obj_var}.{end_name} = {single_target}\n")
627-
else:
628-
target_str = ", ".join(sorted(targets)) # sorted for consistency
629-
f.write(f"{obj_var}.{end_name} = {{{target_str}}}\n")
577+
Parameters:
578+
f: An already-open, writable file handle.
579+
objectmodel (ObjectModel): The B-UML object model to serialize.
580+
object_model_var_name (str): Name of the ObjectModel variable to bind.
581+
"""
582+
object_model_var_name = object_model_var_name or "object_model"
630583

631-
f.write("\n")
584+
f.write("\n################\n")
585+
f.write("# OBJECT MODEL #\n")
586+
f.write("################\n")
587+
588+
# Write object instances using fluent API
589+
for obj in sorted(objectmodel.objects, key=lambda x: x.name_):
590+
obj_var_name = f"{obj.name_.lower()}_obj"
591+
classifier_var_name = safe_class_name(obj.classifier.name)
592+
593+
# Start the fluent API call using the proper syntax: Class("name")
594+
f.write(f"{obj_var_name} = {classifier_var_name}(\"{_escape_python_string(obj.name_)}\")")
595+
596+
# Add attributes if the object has slots
597+
if obj.slots:
598+
attributes_dict = {}
599+
for slot in obj.slots:
600+
attr_name = slot.attribute.name
601+
602+
# Format the value based on type
603+
if isinstance(slot.value.value, str):
604+
value_str = f'"{_escape_python_string(slot.value.value)}"'
605+
elif hasattr(slot.value.value, 'isoformat'): # datetime objects
606+
value_str = f'datetime.datetime.fromisoformat("{slot.value.value.isoformat()}")'
607+
elif hasattr(slot.value.value, 'owner') and hasattr(slot.value.value.owner, 'name'):
608+
# This is an enumeration literal - generate the proper reference
609+
enum_name = slot.value.value.owner.name
610+
literal_name = slot.value.value.name
611+
value_str = f"{enum_name}.{literal_name}"
612+
else:
613+
value_str = str(slot.value.value)
632614

633-
# Create the object model instance
634-
f.write("# Object Model instance\n")
635-
objects_str = ", ".join([f"{obj.name_.lower()}_obj" for obj in sorted(objectmodel.objects, key=lambda x: x.name_)])
636-
f.write(f"{object_model_var_name}: ObjectModel = ObjectModel(\n")
637-
f.write(f" name=\"{_escape_python_string(objectmodel.name)}\",\n")
638-
f.write(f" objects={{{objects_str}}}")
615+
attributes_dict[attr_name] = value_str
639616

640-
# Add metadata if it exists
641-
if hasattr(objectmodel, 'metadata') and objectmodel.metadata:
642-
if objectmodel.metadata.description:
643-
f.write(",\n")
644-
f.write(f' metadata=Metadata(description="{_escape_python_string(objectmodel.metadata.description)}")\n')
645-
else:
646-
f.write("\n")
617+
# Add attributes to the fluent API call
618+
if attributes_dict:
619+
attr_pairs = [f"{k}={v}" for k, v in attributes_dict.items()]
620+
f.write(f".attributes({', '.join(attr_pairs)})")
621+
622+
# Complete the fluent API call
623+
f.write(".build()\n")
624+
625+
f.write("\n")
626+
627+
# Add links after objects are created (avoiding forward reference issues)
628+
if hasattr(objectmodel, 'links') and objectmodel.links:
629+
630+
# Group links by (source_obj_var, end_name)
631+
grouped_links = {}
632+
for link in objectmodel.links:
633+
if len(link.connections) == 2:
634+
end1, end2 = link.connections
635+
obj1_var = f"{end1.object.name_.lower()}_obj"
636+
obj2_var = f"{end2.object.name_.lower()}_obj"
637+
end2_name = end2.association_end.name
638+
639+
key = (obj1_var, end2_name)
640+
grouped_links.setdefault(key, set()).add(obj2_var)
641+
642+
# Write assignments for each group
643+
for (obj_var, end_name), targets in grouped_links.items():
644+
if len(targets) == 1:
645+
[single_target] = targets
646+
f.write(f"{obj_var}.{end_name} = {single_target}\n")
647647
else:
648-
f.write("\n")
649-
f.write(")\n")
648+
target_str = ", ".join(sorted(targets)) # sorted for consistency
649+
f.write(f"{obj_var}.{end_name} = {{{target_str}}}\n")
650650

651-
print(f"BUML model saved to {file_path}")
651+
f.write("\n")
652+
653+
# Create the object model instance
654+
f.write("# Object Model instance\n")
655+
objects_str = ", ".join([f"{obj.name_.lower()}_obj" for obj in sorted(objectmodel.objects, key=lambda x: x.name_)])
656+
f.write(f"{object_model_var_name}: ObjectModel = ObjectModel(\n")
657+
f.write(f" name=\"{_escape_python_string(objectmodel.name)}\",\n")
658+
f.write(f" objects={{{objects_str}}}")
659+
660+
# Add metadata if it exists
661+
if hasattr(objectmodel, 'metadata') and objectmodel.metadata:
662+
if objectmodel.metadata.description:
663+
f.write(",\n")
664+
f.write(f' metadata=Metadata(description="{_escape_python_string(objectmodel.metadata.description)}")\n')
665+
else:
666+
f.write("\n")
667+
else:
668+
f.write("\n")
669+
f.write(")\n")
670+
671+
672+
def object_model_to_code(
673+
objectmodel: ObjectModel,
674+
file_path: str,
675+
object_model_var_name: str = "object_model",
676+
):
677+
"""Generate Python code for a standalone B-UML object model.
678+
679+
The generated file is self-contained apart from the domain classes and
680+
enumerations it references: it only imports ``ObjectModel`` and ``datetime``
681+
and then emits the object-model section. It is meant to be concatenated
682+
*after* the corresponding domain model (whose classes/enums it references)
683+
so the combined file executes cleanly.
684+
685+
Parameters:
686+
objectmodel (ObjectModel): The B-UML object model to serialize.
687+
file_path (str): The path where the generated code will be saved.
688+
object_model_var_name (str): Name of the ObjectModel variable in the
689+
generated code. Defaults to "object_model".
690+
691+
Outputs:
692+
- A Python file containing the object-model instances and their links.
693+
"""
694+
output_dir = os.path.dirname(file_path)
695+
if output_dir and not os.path.exists(output_dir):
696+
os.makedirs(output_dir)
697+
if not file_path.endswith('.py'):
698+
file_path += '.py'
699+
700+
object_model_var_name = object_model_var_name or "object_model"
701+
702+
with open(file_path, 'w', encoding='utf-8') as f:
703+
# Object-model imports. The domain classes/enums are assumed to be
704+
# defined earlier in the concatenated output.
705+
f.write("from besser.BUML.metamodel.object import ObjectModel\n")
706+
f.write("import datetime\n")
707+
708+
_write_object_model_section(f, objectmodel, object_model_var_name)
709+
710+
print(f"BUML object model saved to {file_path}")

besser/utilities/buml_code_builder/project_builder.py

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from besser.utilities.buml_code_builder.common import _comment_safe, _escape_python_string
1717
from besser.utilities.buml_code_builder.domain_model_builder import (
1818
domain_model_to_code,
19+
object_model_to_code,
1920
contains_user_class,
2021
is_user_object_model,
2122
)
@@ -222,28 +223,28 @@ def project_to_code(project: Project, file_path: str, sm: str = ""):
222223
_write_temp_to_output(tmp_path, f, section_header=section)
223224
model_vars.append(var_name)
224225

225-
# Standalone object models (when not paired 1:1 with domain models)
226+
# Standalone object models (when not paired 1:1 with domain models).
227+
# The domain model these objects reference is already written above
228+
# (in the domain_pairs loop). Since the whole project is concatenated
229+
# into ONE file, each object-only section can reference the class and
230+
# enum variables defined earlier, so we emit just the object portion.
226231
if not (len(domain_models) == 1 and len(object_models) == 1):
227232
n_standalone_obj = len(object_models)
228233
for idx, om in enumerate(object_models, start=1):
229234
obj_var_name = _suffixed_name("object_model", idx, n_standalone_obj)
230-
# Object models need a domain_model to reference; pass None and
231-
# generate just the object portion via domain_model_to_code.
232-
# For standalone objects without a paired domain model we find
233-
# the domain_model attribute on the ObjectModel itself.
234-
paired_dm = getattr(om, "domain_model", None)
235-
if paired_dm:
236-
dm_var = _suffixed_name("object_domain_model", idx, n_standalone_obj)
237-
tmp_path = os.path.join(temp_dir, f"object_model_{idx}.py")
238-
domain_model_to_code(
239-
model=paired_dm,
240-
file_path=tmp_path,
241-
objectmodel=om,
242-
model_var_name=dm_var,
243-
object_model_var_name=obj_var_name,
244-
)
245-
_write_temp_to_output(tmp_path, f)
246-
model_vars.append(obj_var_name)
235+
236+
section = ""
237+
if n_standalone_obj > 1:
238+
section = f"# OBJECT MODEL {idx} #\n\n"
239+
240+
tmp_path = os.path.join(temp_dir, f"object_model_{idx}.py")
241+
object_model_to_code(
242+
objectmodel=om,
243+
file_path=tmp_path,
244+
object_model_var_name=obj_var_name,
245+
)
246+
_write_temp_to_output(tmp_path, f, section_header=section)
247+
model_vars.append(obj_var_name)
247248

248249
# ---------------------------------------------------------- #
249250
# USER DOMAIN MODELS #

docs/source/releases/v7.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Version 7
44
.. toctree::
55
:maxdepth: 1
66

7+
v7/v7.11.2
78
v7/v7.11.1
89
v7/v7.11.0
910
v7/v7.10.0
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
Version 7.11.2
2+
==============
3+
4+
Patch release: **B-UML export now keeps every object model in multi-object
5+
projects**. A fix to the Python code builder so that exporting a class model
6+
together with more than one object model no longer silently drops the object
7+
models. No metamodel or API contract changes.
8+
9+
Highlights
10+
----------
11+
12+
- **Multiple object models are no longer lost on B-UML** (``.py``) **export**:
13+
exporting a project that combined a domain (class) model with two or more
14+
object models produced a Python file that contained none of the object models,
15+
while exporting the same project to JSON was unaffected (`web editor issue #161
16+
<https://github.com/BESSER-PEARL/BESSER-Web-Modeling-Editor/issues/161>`_).
17+
The root cause was in ``project_to_code``
18+
(``besser/utilities/buml_code_builder/project_builder.py``): the
19+
standalone-object loop only emitted a model when
20+
``getattr(om, "domain_model", None)`` was truthy, but ``ObjectModel`` has no
21+
``domain_model`` attribute, so the branch never ran — and the models were also
22+
omitted from the generated ``Project(models=[...])`` list. A single object
23+
model still worked because it took a separate one-to-one pairing path.
24+
- **The fix** extracts the object-writing logic from ``domain_model_to_code``
25+
into a reusable ``object_model_to_code`` helper (``domain_model_to_code``
26+
output is byte-for-byte unchanged), and ``project_to_code`` now emits every
27+
standalone object model as an object-only section that references the domain
28+
classes already written earlier in the same file. A new regression test
29+
exports one domain model plus two object models and executes the generated
30+
file back into two ``ObjectModel`` instances.
31+
32+
No metamodel or API contract changes.
33+
34+
Thanks to `@rdegiovanni <https://github.com/rdegiovanni>`_ for the report
35+
(`web editor issue #161
36+
<https://github.com/BESSER-PEARL/BESSER-Web-Modeling-Editor/issues/161>`_).

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = besser
3-
version = 7.11.1
3+
version = 7.11.2
44
author = Luxembourg Institute of Science and Technology
55
description = BESSER
66
long_description = file: README.md

0 commit comments

Comments
 (0)