Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions server/python/src/metaobjects/codegen/generators/entity_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,10 @@ def _field_line(field: MetaField, imports: set[str], config: GenConfig) -> tuple
if field.sub_type == fc.FIELD_SUBTYPE_OBJECT:
ref = field.attr(fc.FIELD_ATTR_OBJECT_REF)
if ref:
imports.add(f"from .{ref} import {ref}")
# @objectRef is FQN-expanded at load time; the generated VOs live
# flat in one package, so import by the bare class name.
ref_name = str(ref).split("::")[-1]
imports.add(f"from .{ref_name} import {ref_name}")
required = field.attr(fc.FIELD_ATTR_REQUIRED) is True
default_raw = field.attr(fc.FIELD_ATTR_DEFAULT)
has_default = default_raw is not None
Expand Down Expand Up @@ -152,14 +155,12 @@ def _field_line(field: MetaField, imports: set[str], config: GenConfig) -> tuple


def _effective_fqn(entity: MetaObject) -> str:
"""`package::name`, resolving the package from the nearest ancestor that carries
one (objects inherit the file/root package). Falls back to the bare name."""
pkg = entity.package
parent = entity.parent
while pkg is None and parent is not None:
pkg = parent.package
parent = parent.parent
return f"{pkg}{PACKAGE_SEP}{entity.name}" if pkg else entity.name
"""`package::name` via the canonical :meth:`MetaData.resolution_key` — own
package, else the file-default package (captured at parse), else the nearest
ancestor's. The ``file_default_package`` step is load-bearing after a multi-file
merge: every node hangs under one package-less merged root, so the ancestor walk
alone would fold each object onto the alphabetically-first file's package."""
return entity.resolution_key()


class EntityModelGenerator:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,10 @@


def _effective_fqn(entity: MetaObject) -> str:
"""``package::name``, resolving package from the nearest ancestor that carries
one. Mirror of the router generator's helper of the same name."""
pkg = entity.package
parent = entity.parent
while pkg is None and parent is not None:
pkg = parent.package
parent = parent.parent
return f"{pkg}{PACKAGE_SEP}{entity.name}" if pkg else entity.name
"""``package::name`` via the canonical :meth:`MetaData.resolution_key` (own
package, else file-default, else ancestor) — multi-file-merge safe. Mirror of
the router generator's helper of the same name."""
return entity.resolution_key()


def _primary_source_rdb(entity: MetaObject) -> MetaSource | None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -571,19 +571,15 @@ def render_payload_vo(


def _effective_fqn_for(template: MetaTemplate, payload: MetaObject) -> str:
"""``package::name`` for the doc-header. Prefer the template's own package
chain; fall back to the payload's; finally the bare template name."""

def _walk(node: MetaData) -> str | None:
pkg = node.package
parent = node.parent
while pkg is None and parent is not None:
pkg = parent.package
parent = parent.parent
return pkg

pkg = _walk(template) or _walk(payload)
return f"{pkg}{PACKAGE_SEP}{template.name}" if pkg else template.name
"""``package::name`` for the doc-header, via the canonical
:meth:`MetaData.resolution_key` (own package, else the file-default captured at
parse, else the nearest ancestor) — multi-file-merge safe. Falls back to the
payload's package only if the template resolves to a bare (package-less) name."""
key = template.resolution_key()
if PACKAGE_SEP in key:
return key
payload_pkg = payload.package or payload.file_default_package
return f"{payload_pkg}{PACKAGE_SEP}{template.name}" if payload_pkg else template.name


# ---------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,10 @@


def _effective_fqn(entity: MetaObject) -> str:
"""``package::name``, resolving package from the nearest ancestor that carries
one. Mirrors the entity-model generator's helper of the same name."""
pkg = entity.package
parent = entity.parent
while pkg is None and parent is not None:
pkg = parent.package
parent = parent.parent
return f"{pkg}{PACKAGE_SEP}{entity.name}" if pkg else entity.name
"""``package::name`` via the canonical :meth:`MetaData.resolution_key` (own
package, else file-default, else ancestor) — multi-file-merge safe. Mirrors the
entity-model generator's helper of the same name."""
return entity.resolution_key()


def _primary_source_rdb(entity: MetaObject) -> MetaSource | None:
Expand Down
5 changes: 4 additions & 1 deletion server/python/src/metaobjects/codegen/type_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ def py_type_for(field: MetaField) -> PyType:
``str``."""
if field.sub_type == fc.FIELD_SUBTYPE_OBJECT:
ref = field.attr(fc.FIELD_ATTR_OBJECT_REF)
base = PyType(str(ref)) if ref else PyType("object")
# @objectRef is expanded to a package-qualified FQN at load time
# (e.g. ``app::pkg::Thing``); the emitted VOs all live flat in one
# generated package, so type by the bare class name.
base = PyType(str(ref).split("::")[-1]) if ref else PyType("object")
elif field.sub_type == fc.FIELD_SUBTYPE_ENUM:
values = effective_enum_values(field)
if values:
Expand Down
32 changes: 32 additions & 0 deletions server/python/tests/codegen/test_entity_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,38 @@ def test_nested_object_array_imports_ref() -> None:
assert "posts: list[PostBrief] | None = None" in out


def test_header_fqn_folds_file_default_package_after_merge() -> None:
"""After a multi-file merge an object hangs under one package-less merged
root, so the naive nearest-ancestor walk would fold every object onto the
alphabetically-first file's package. The header FQN must instead use the
canonical ``resolution_key`` (own → file-default → ancestor), so an object
keeps its true source-file package regardless of merge order."""
from metaobjects.codegen.generators.entity_model import _effective_fqn

obj = _entity("ClaimComplexity", [_f("confidence", fc.FIELD_SUBTYPE_DOUBLE)])
# Post-merge shape: no own package, captured file-default from its real file,
# parent is the merged super-root carrying a DIFFERENT (first-file) package.
obj.file_default_package = "app::reasoning"
merged_root = _entity("_merged", [], package="app::orchestration")
merged_root.add_child(obj)

assert _effective_fqn(obj) == "app::reasoning::ClaimComplexity"


def test_object_ref_fqn_is_shortened_to_class_name() -> None:
"""@objectRef is FQN-expanded at load time (``app::pkg::Thing``); the type
annotation and the relative import must use the bare class name, not the
raw FQN (which is not valid Python). Regression: object.value VOs emitted
``list[app::pkg::Thing]`` and ``from .app::pkg::Thing import ...``."""
e = _entity("Catalog", [
_f("tools", fc.FIELD_SUBTYPE_OBJECT, object_ref="myapp::orch::ToolDescriptor", is_array=True),
])
out = render_entity_model(e)
assert "from .ToolDescriptor import ToolDescriptor" in out
assert "tools: list[ToolDescriptor] | None = None" in out
assert "::" not in out


def test_field_default_is_emitted_as_literal() -> None:
"""@default renders a Python-literal default and makes the field non-Optional."""
flag = MetaField(TYPE_FIELD, fc.FIELD_SUBTYPE_BOOLEAN, "flag")
Expand Down
Loading