Skip to content

Commit 3700490

Browse files
committed
Merge branch 'feat/own-fix-py' into feat/own-fix-reference
2 parents beafb58 + 61dee8e commit 3700490

10 files changed

Lines changed: 212 additions & 40 deletions

File tree

server/python/src/metaobjects/codegen/fr010_field_mapping.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
from metaobjects.meta.meta_data import MetaData
2323
from metaobjects.meta.template.template_constants import TEMPLATE_ATTR_XML_TEXT
2424
from metaobjects.shared.base_types import TYPE_FIELD
25-
from metaobjects.shared.structural import KEY_IS_ARRAY
2625

2726

2827
def fields(vo: MetaData) -> list[MetaData]:
@@ -32,9 +31,10 @@ def fields(vo: MetaData) -> list[MetaData]:
3231

3332

3433
def is_array(field: MetaData) -> bool:
35-
"""Array-ness from either form: the node property (programmatic build) or the
36-
``@isArray`` attr (how metadata loads from JSON)."""
37-
return bool(field.is_array) or field.attrs().get(KEY_IS_ARRAY) is True
34+
"""Effective array-ness (ADR-0039 resolving): the native ``is_array`` flag OR the
35+
``@isArray`` attr, resolved through the ``extends`` super chain so a concrete field
36+
inheriting array-ness from an abstract parent is honored."""
37+
return field.resolved_is_array()
3838

3939

4040
def is_required(field: MetaData) -> bool:
@@ -63,31 +63,36 @@ def enum_values(field: MetaData) -> list[str]:
6363
return []
6464

6565

66-
def _own_attr_string(node: MetaData, name: str) -> str | None:
67-
"""The own (locally declared) string value of attr *name*, or ``None``."""
68-
v = node.attr(name)
66+
def _attr_string(node: MetaData, name: str) -> str | None:
67+
"""The RESOLVING string value of attr *name* (own + inherited via ``extends``),
68+
or ``None``.
69+
70+
ADR-0039 — effective read: a concrete enum field extending an abstract enum may
71+
inherit ``@coerceDefault``/``@default``/``@normalize`` from the abstract parent,
72+
so these resolve through the super chain (``get_meta_attr``), never own-only."""
73+
v = node.get_meta_attr(name)
6974
return v if isinstance(v, str) else None
7075

7176

7277
def coerce_default(field: MetaData) -> str | None:
73-
"""FR-011: the enum field's own ``@coerceDefault`` member, or ``None``."""
74-
return _own_attr_string(field, fc.FIELD_ATTR_COERCE_DEFAULT)
78+
"""FR-011: the enum field's effective ``@coerceDefault`` member, or ``None``."""
79+
return _attr_string(field, fc.FIELD_ATTR_COERCE_DEFAULT)
7580

7681

7782
def default_value(field: MetaData) -> str | None:
78-
"""FR-011: the enum field's own ``@default`` absent-fill member, or ``None``."""
79-
return _own_attr_string(field, fc.FIELD_ATTR_DEFAULT)
83+
"""FR-011: the enum field's effective ``@default`` absent-fill member, or ``None``."""
84+
return _attr_string(field, fc.FIELD_ATTR_DEFAULT)
8085

8186

8287
def resolve_normalize(field: MetaData, owner: MetaData | None) -> str:
8388
"""FR-011: resolve the enum normalization mode — field-level ``@normalize``, else the
8489
owning ``object.value``'s ``@normalize`` (the per-object default), else the global
8590
default (``"strip"``). Mirrors the Java/Kotlin/C#/TS ``resolveNormalize``."""
86-
field_mode = _own_attr_string(field, fc.FIELD_ATTR_NORMALIZE)
91+
field_mode = _attr_string(field, fc.FIELD_ATTR_NORMALIZE)
8792
if field_mode is not None:
8893
return field_mode
8994
if owner is not None:
90-
owner_mode = _own_attr_string(owner, fc.FIELD_ATTR_NORMALIZE)
95+
owner_mode = _attr_string(owner, fc.FIELD_ATTR_NORMALIZE)
9196
if owner_mode is not None:
9297
return owner_mode
9398
return fc.NORMALIZE_DEFAULT

server/python/src/metaobjects/codegen/generators/fr019_shared_enum.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,12 @@ def resolve_shared_enum_decl(field: MetaField) -> MetaData | None:
6666

6767

6868
def is_provided(decl: MetaData) -> bool:
69-
"""Own ``@provided`` truth of an enum declaration."""
70-
return decl.attr(fc.FIELD_ATTR_PROVIDED) is True
69+
"""Effective ``@provided`` truth of an enum declaration.
70+
71+
ADR-0039 — resolves through ``extends`` (``get_meta_attr``): a concrete enum
72+
extending an abstract ``@provided`` enum inherits the flag, so an own-only read
73+
would misclassify it."""
74+
return decl.get_meta_attr(fc.FIELD_ATTR_PROVIDED) is True
7175

7276

7377
def _meta_package_of(decl: MetaData) -> str:

server/python/src/metaobjects/codegen/template_codegen/template_data.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ def is_concrete(o: Any) -> bool:
5252

5353

5454
def _is_required(f: Any) -> bool:
55-
if f.own_attrs().get(_VALIDATOR_REQUIRED) is True:
55+
# ADR-0039 resolving: @required / a validator.required child may be inherited via extends.
56+
if f.attrs().get(_VALIDATOR_REQUIRED) is True:
5657
return True
5758
return any(
5859
c.type == TYPE_VALIDATOR and c.sub_type == _VALIDATOR_REQUIRED
@@ -65,11 +66,12 @@ def _field_data(f: Any) -> dict[str, Any]:
6566
"name": f.name,
6667
"type": f.sub_type,
6768
"required": _is_required(f),
68-
"isArray": bool(f.is_array),
69+
# ADR-0039 resolving: a concrete field may inherit isArray/@maxLength from an abstract parent.
70+
"isArray": f.resolved_is_array(),
6971
}
70-
own = f.own_attrs()
71-
if "maxLength" in own:
72-
d["maxLength"] = int(own["maxLength"])
72+
eff = f.attrs()
73+
if "maxLength" in eff:
74+
d["maxLength"] = int(eff["maxLength"])
7375
if f.sub_type == _SUBTYPE_ENUM:
7476
values = f.attrs().get("values")
7577
if isinstance(values, list):

server/python/src/metaobjects/codegen/type_map.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from metaobjects.meta.core.field.meta_field import MetaField
77
from metaobjects.meta.core.field import field_constants as fc
88
from metaobjects.meta.persistence.db import db_constants as dbc
9-
from metaobjects.shared.structural import KEY_IS_ARRAY
109

1110

1211
@dataclass(frozen=True)
@@ -43,9 +42,11 @@ class PyType:
4342

4443

4544
def field_is_array(field: MetaField) -> bool:
46-
"""Array-ness from either form: the node property (programmatic build) or the
47-
`@isArray` attr (how metadata loads from JSON — the conformance-fixture form)."""
48-
return field.is_array or field.attrs().get(KEY_IS_ARRAY) is True
45+
"""Effective array-ness (ADR-0039 resolving): the native ``is_array`` flag OR
46+
the ``@isArray`` attr, resolved through the ``extends`` super chain — a concrete
47+
field inheriting ``isArray:true`` from an abstract parent (whose flag lives on the
48+
parent as a native property, not an attr) must generate a ``list[...]``."""
49+
return field.resolved_is_array()
4950

5051

5152
def _py_str_literal(value: str) -> str:

server/python/src/metaobjects/loader/validation_passes.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2108,7 +2108,10 @@ def _validate_field_object_storage(root: MetaData, errors: list[MetaError]) -> N
21082108
for node in _walk(root):
21092109
if node.type != TYPE_FIELD or node.sub_type != FIELD_SUBTYPE_OBJECT:
21102110
continue
2111-
object_ref = node.attr(FIELD_ATTR_OBJECT_REF)
2111+
# ADR-0039 resolving: a concrete field.object may inherit @objectRef from an
2112+
# abstract parent field via extends — read the effective value, not own-only,
2113+
# or a valid inherited target falsely trips ERR_OBJECT_FIELD_WITHOUT_OBJECT_REF.
2114+
object_ref = node.get_meta_attr(FIELD_ATTR_OBJECT_REF)
21122115
if not (isinstance(object_ref, str) and object_ref):
21132116
errors.append(MetaError(
21142117
code=ErrorCode.ERR_OBJECT_FIELD_WITHOUT_OBJECT_REF,
@@ -2120,10 +2123,10 @@ def _validate_field_object_storage(root: MetaData, errors: list[MetaError]) -> N
21202123
envelope=node.source,
21212124
))
21222125
continue
2123-
storage = node.attr(FIELD_ATTR_STORAGE)
2126+
storage = node.get_meta_attr(FIELD_ATTR_STORAGE) # ADR-0039 resolving
21242127
if storage is None:
21252128
continue
2126-
if storage == "flattened" and getattr(node, "is_array", False):
2129+
if storage == "flattened" and node.resolved_is_array(): # ADR-0039 resolving isArray
21272130
errors.append(MetaError(
21282131
code=ErrorCode.ERR_STORAGE_FLATTENED_ARRAY,
21292132
message=(
@@ -2163,14 +2166,17 @@ def _validate_field_object_storage(root: MetaData, errors: list[MetaError]) -> N
21632166

21642167

21652168
def _validate_field_map(root: MetaData, errors: list[MetaError]) -> None:
2166-
for obj in (c for c in root.own_children() if c.type == TYPE_OBJECT):
2167-
for field in (c for c in obj.own_children() if c.type == TYPE_FIELD):
2169+
# ADR-0039: iterate EFFECTIVE members (children()) so an object inheriting a
2170+
# field.map through extends is still validated; and read @valueType/@objectRef
2171+
# via the resolving get_meta_attr (they may be inherited from an abstract parent).
2172+
for obj in (c for c in root.children() if c.type == TYPE_OBJECT):
2173+
for field in (c for c in obj.children() if c.type == TYPE_FIELD):
21682174
if field.sub_type != FIELD_SUBTYPE_MAP:
21692175
continue
21702176

2171-
value_type = field.attr(FIELD_ATTR_VALUE_TYPE)
2177+
value_type = field.get_meta_attr(FIELD_ATTR_VALUE_TYPE)
21722178
has_value_type = isinstance(value_type, str) and bool(value_type)
2173-
object_ref = field.attr(FIELD_ATTR_OBJECT_REF)
2179+
object_ref = field.get_meta_attr(FIELD_ATTR_OBJECT_REF)
21742180
has_object_ref = isinstance(object_ref, str) and bool(object_ref)
21752181

21762182
if has_value_type == has_object_ref:

server/python/src/metaobjects/meta/core/field/meta_field.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,13 @@ def object_ref(self) -> str | None:
4444
4545
The value is the package-folded form (e.g. ``com::example::om::Address``)
4646
— matching a target object's ``resolution_key()``.
47+
48+
ADR-0039 — RESOLVING (``get_meta_attr``): ``@objectRef`` is an effective
49+
property, so a concrete ``field.object`` extending an abstract one inherits
50+
its target. Unifies with the codegen ``attrs().get(@objectRef)`` path (both
51+
now resolve through the ``extends`` super chain).
4752
"""
48-
v = self.attr(fc.FIELD_ATTR_OBJECT_REF)
53+
v = self.get_meta_attr(fc.FIELD_ATTR_OBJECT_REF)
4954
return str(v) if v is not None else None
5055

5156
def get_value(self, obj: object, name: str | None = None) -> object:

server/python/src/metaobjects/meta/meta_data.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from ..attr_class_map import attr_class_for
77
from ..shared.base_types import SUBTYPE_BASE, TYPE_ATTR
88
from ..shared.separators import PACKAGE_SEP
9+
from ..shared.structural import KEY_IS_ARRAY
910
from ..source import CodeSource, ErrorSource
1011

1112
T = TypeVar("T")
@@ -121,10 +122,54 @@ def own_meta_attrs(self) -> list[MetaData]:
121122
return list(self._attr_nodes.values())
122123

123124
def attr(self, name: str) -> object:
124-
"""Own attr value for *name*, or ``None``."""
125+
"""OWN-ONLY attr value for *name*, or ``None``.
126+
127+
ADR-0039 — NAMING INVERSION (Python vs TS/Java): in Python ``attr()`` is
128+
OWN-ONLY (reads only this node's locally-declared attr, never the super
129+
chain), whereas TS/Java ``attr()`` RESOLVES. **Do not use ``attr()`` to
130+
read an effective/semantic property** (``@objectRef``/``@column``/
131+
``@default``/``@maxLength``/``@precision``/``@storage``/``@required``/…) —
132+
a value inherited through this node's ``extends`` chain would be silently
133+
dropped. Use the resolving ``get_meta_attr(name)`` (or ``attrs().get(name)``)
134+
instead. ``attr()`` is legitimate ONLY for: own-mode canonical
135+
serialization, overlay/merge + super-resolution walks, validating the
136+
*own* declaration, and the deliberately-never-inherited ``@dbColumnType``.
137+
Every such call MUST carry a one-line comment naming the sanctioned case.
138+
"""
125139
node = self._attr_nodes.get(name)
126140
return getattr(node, "value", None) if node is not None else None
127141

142+
def get_meta_attr(self, name: str) -> object:
143+
"""RESOLVING attr value for *name* — own + inherited via the ``extends``
144+
super chain (own wins on conflict), or ``None``.
145+
146+
ADR-0039 — this is the default/effective read. Prefer it over ``attr()``
147+
(which is own-only in Python; see the inversion note there) for any
148+
semantic property that may be inherited from an abstract parent.
149+
"""
150+
return self.attrs().get(name)
151+
152+
def resolved_is_array(self) -> bool:
153+
"""RESOLVING array-ness — the effective ``isArray`` for this node.
154+
155+
ADR-0039 — mirrors the TS ``MetaData.resolvedIsArray()``. ``is_array`` is a
156+
native boolean property (not an attr), so it is resolved two ways: the
157+
native flag on this node OR any super in the chain has it set, OR the
158+
loaded-from-JSON ``@isArray`` attr resolves true through ``attrs()``.
159+
Reading the own ``is_array`` flag alone silently drops an array-ness
160+
inherited from an abstract parent field.
161+
"""
162+
if self.is_array:
163+
return True
164+
node = self.super_data
165+
seen: set[MetaData] = {self}
166+
while node is not None and node not in seen:
167+
if node.is_array:
168+
return True
169+
seen.add(node)
170+
node = node.super_data
171+
return self.attrs().get(KEY_IS_ARRAY) is True
172+
128173
def own_attrs(self) -> dict[str, object]:
129174
"""Own attr value map — excludes inherited attrs."""
130175
return {

server/python/src/metaobjects/runtime/object_manager.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
from ..meta.persistence.db import db_constants as dbc
4040
from ..meta.persistence.source.meta_source import MetaSource
4141
from ..meta.persistence.source import source_constants as sc
42-
from ..shared.structural import KEY_IS_ARRAY
4342
from .n2m_resolver import (
4443
N2mDescriptor,
4544
collect_column_ids,
@@ -563,7 +562,7 @@ def _primary_pk_field(self, entity: MetaObject) -> str:
563562
pi = entity.primary_identity()
564563
if pi is None:
565564
raise ValueError(f"Entity '{entity.name}' has no primary identity")
566-
raw = pi.attr(ic.IDENTITY_ATTR_FIELDS)
565+
raw = pi.get_meta_attr(ic.IDENTITY_ATTR_FIELDS) # ADR-0039 resolving: identity @fields may be inherited
567566
if isinstance(raw, str):
568567
return raw
569568
if isinstance(raw, (list, tuple)) and raw:
@@ -645,14 +644,18 @@ def _op_clause(col: str, op: str, value: Any) -> tuple[str, list[Any]]:
645644

646645

647646
def _field_is_array(field: MetaField) -> bool:
648-
"""Array-ness from either form: the node property or the ``@isArray`` attr."""
649-
return bool(field.is_array) or field.attr(KEY_IS_ARRAY) is True
647+
"""Effective array-ness (ADR-0039 resolving): the native ``is_array`` flag OR
648+
the ``@isArray`` attr, resolved through the ``extends`` super chain so a concrete
649+
field inheriting array-ness from an abstract parent is honored at write time."""
650+
return field.resolved_is_array()
650651

651652

652653
def _coerce_write_value(field: MetaField, value: Any) -> Any:
653654
if value is None:
654655
return None
655656
sub = field.sub_type
657+
# ADR-0039 own-only: @dbColumnType is a physical column-type override, never
658+
# inherited via extends (the one deliberately own-only attribute).
656659
col_type = field.attr(dbc.FIELD_ATTR_DB_COLUMN_TYPE)
657660

658661
# decimal / currency: a decimal authored as a string → Decimal (exact; never
@@ -684,7 +687,7 @@ def _coerce_write_value(field: MetaField, value: Any) -> Any:
684687
if sub == fc.FIELD_SUBTYPE_TIMESTAMP:
685688
if isinstance(value, _dt.datetime):
686689
return value
687-
is_tz = field.attr(dbc.FIELD_ATTR_LOCAL_TIME) is not True
690+
is_tz = field.get_meta_attr(dbc.FIELD_ATTR_LOCAL_TIME) is not True # ADR-0039 resolving: @localTime may be inherited
688691
return _parse_datetime(str(value), tz_aware=is_tz)
689692

690693
# field.object (jsonb storage): a dict/list passes through — pg8000 binds it
@@ -718,7 +721,7 @@ def _parse_datetime(text: str, *, tz_aware: bool) -> _dt.datetime:
718721
def _column_of(field: MetaField | None) -> str:
719722
if field is None:
720723
return ""
721-
col = field.attr(fc.FIELD_ATTR_COLUMN)
724+
col = field.get_meta_attr(fc.FIELD_ATTR_COLUMN) # ADR-0039 resolving: @column may be inherited
722725
return col if isinstance(col, str) and col else field.name
723726

724727

server/python/src/metaobjects/serializer_json.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,11 @@ def _body(node: MetaData, effective: bool = False) -> dict[str, object]:
6666
body[KEY_EXTENDS] = node.super_ref
6767
if node.is_abstract:
6868
body[KEY_ABSTRACT] = True
69-
if node.is_array:
69+
# ADR-0039 — isArray is a native property; in EFFECTIVE mode it must resolve
70+
# through the extends super chain (a concrete field inheriting isArray:true
71+
# from an abstract parent), whereas own-mode emits only the locally-declared
72+
# flag (round-trips the authored form).
73+
if node.resolved_is_array() if effective else node.is_array:
7074
body[KEY_IS_ARRAY] = True
7175

7276
# In effective mode use attrs()/children() (own + inherited via the super

0 commit comments

Comments
 (0)