This introduces an uglt regression.
This commit is contained in:
parent
ce7d092be4
commit
bd2daf921a
5 changed files with 136 additions and 128 deletions
|
|
@ -1,22 +1,11 @@
|
|||
from functools import lru_cache
|
||||
from typing import Any, Dict, Iterable, List, Optional, Tuple, Set, cast
|
||||
from sqlalchemy import Column, Integer, DateTime, Boolean, String, JSON, func, inspect
|
||||
from sqlalchemy.orm import declarative_mixin, declarative_base, NO_VALUE, RelationshipProperty, Mapper
|
||||
from sqlalchemy import Column, Integer, DateTime, String, JSON, func, inspect
|
||||
from sqlalchemy.orm import declarative_mixin, declarative_base, NO_VALUE, Mapper
|
||||
from sqlalchemy.orm.state import InstanceState
|
||||
|
||||
Base = declarative_base()
|
||||
from crudkit.core.meta import column_names_for_model
|
||||
|
||||
@lru_cache(maxsize=512)
|
||||
def _column_names_for_model(cls: type) -> tuple[str, ...]:
|
||||
try:
|
||||
mapper = inspect(cls)
|
||||
return tuple(prop.key for prop in mapper.column_attrs)
|
||||
except Exception:
|
||||
names: list[str] = []
|
||||
for c in cls.__mro__:
|
||||
if hasattr(c, "__table__"):
|
||||
names.extend(col.name for col in c.__table__.columns)
|
||||
return tuple(dict.fromkeys(names))
|
||||
Base = declarative_base()
|
||||
|
||||
def _sa_state(obj: Any) -> Optional[InstanceState[Any]]:
|
||||
"""Safely get SQLAlchemy InstanceState (or None)."""
|
||||
|
|
@ -60,7 +49,16 @@ def _safe_get_loaded_attr(obj, name):
|
|||
if attr is not None:
|
||||
val = attr.loaded_value
|
||||
return None if val is NO_VALUE else val
|
||||
|
||||
try:
|
||||
# In rare cases, state.dict may be stale; reject descriptors
|
||||
got = getattr(obj, name, None)
|
||||
from sqlalchemy.orm.attributes import InstrumentedAttribute as _Instr
|
||||
if got is not None and not isinstance(got, _Instr):
|
||||
# Do not trigger load; only return if it was already present in __dict__
|
||||
if hasattr(obj, "__dict__") and name in obj.__dict__:
|
||||
return got
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
|
@ -72,16 +70,10 @@ def _identity_key(obj) -> Tuple[type, Any]:
|
|||
except Exception:
|
||||
return (type(obj), id(obj))
|
||||
|
||||
def _is_collection_rel(prop: RelationshipProperty) -> bool:
|
||||
try:
|
||||
return prop.uselist is True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _serialize_simple_obj(obj) -> Dict[str, Any]:
|
||||
"""Columns only (no relationships)."""
|
||||
out: Dict[str, Any] = {}
|
||||
for name in _column_names_for_model(type(obj)):
|
||||
for name in column_names_for_model(type(obj)):
|
||||
try:
|
||||
out[name] = getattr(obj, name)
|
||||
except Exception:
|
||||
|
|
@ -153,16 +145,16 @@ def _split_field_tokens(fields: Iterable[str]) -> Tuple[List[str], Dict[str, Lis
|
|||
def _deep_get_loaded(obj: Any, dotted: str) -> Any:
|
||||
"""
|
||||
Deep get with no lazy loads:
|
||||
- For all but the final hop, use _safe_get_loaded_attr (mapped-only, no getattr).
|
||||
- For the final hop, try _safe_get_loaded_attr first; if None, fall back to getattr()
|
||||
to allow computed properties/hybrids that rely on already-loaded columns.
|
||||
- Walk intermediate hops via _safe_get_loaded_attr only.
|
||||
- Final hop: prefer _safe_get_loaded_attr; if that returns an ORM object or a collection of ORM objects,
|
||||
serialize to simple dicts; else return the plain value.
|
||||
"""
|
||||
parts = dotted.split(".")
|
||||
if not parts:
|
||||
return None
|
||||
|
||||
cur = obj
|
||||
# Traverse up to the parent of the last token safely
|
||||
# Traverse up to parent of last token without lazy-loads
|
||||
for part in parts[:-1]:
|
||||
if cur is None:
|
||||
return None
|
||||
|
|
@ -171,30 +163,45 @@ def _deep_get_loaded(obj: Any, dotted: str) -> Any:
|
|||
return None
|
||||
|
||||
last = parts[-1]
|
||||
# Try safe fetch on the last hop first
|
||||
val = _safe_get_loaded_attr(cur, last)
|
||||
if val is not None:
|
||||
return val
|
||||
# Fall back to getattr for computed/hybrid attributes on an already-loaded object
|
||||
try:
|
||||
return getattr(cur, last, None)
|
||||
except Exception:
|
||||
if val is None:
|
||||
# Do NOT lazy load. If it isn't loaded, treat as absent.
|
||||
return None
|
||||
|
||||
# If the final hop is an ORM object or collection, serialize it
|
||||
if _sa_mapper(val) is not None:
|
||||
return _serialize_simple_obj(val)
|
||||
if isinstance(val, (list, tuple)):
|
||||
out = []
|
||||
for v in val:
|
||||
if _sa_mapper(v) is not None:
|
||||
out.append(_serialize_simple_obj(v))
|
||||
else:
|
||||
out.append(v)
|
||||
return out
|
||||
|
||||
# Plain scalar/computed value
|
||||
return val
|
||||
|
||||
def _serialize_leaf(obj: Any) -> Any:
|
||||
"""
|
||||
Lead serialization for values we put into as_dict():
|
||||
- If object has as_dict(), call as_dict() with no args (caller controls field shapes).
|
||||
- Else return value as-is (Flask/JSON encoder will handle datetimes, etc., via app config).
|
||||
Leaf serialization for non-dotted scalar fields:
|
||||
- If it's an ORM object with as_dict(), use it.
|
||||
- Else if it's an ORM object, serialize columns only.
|
||||
- Else return the value as-is.
|
||||
"""
|
||||
if obj is None:
|
||||
return None
|
||||
ad = getattr(obj, "as_dict", None)
|
||||
if callable(ad):
|
||||
try:
|
||||
return ad(None)
|
||||
except Exception:
|
||||
return str(obj)
|
||||
|
||||
if _sa_mapper(obj) is not None:
|
||||
ad = getattr(obj, "as_dict", None)
|
||||
if callable(ad):
|
||||
try:
|
||||
return ad() # no args, your default handles fields selection
|
||||
except Exception:
|
||||
pass
|
||||
return _serialize_simple_obj(obj)
|
||||
|
||||
return obj
|
||||
|
||||
def _serialize_collection(items: Iterable[Any], requested_tails: List[str]) -> List[Dict[str, Any]]:
|
||||
|
|
@ -319,8 +326,6 @@ class CRUDMixin:
|
|||
if mapper is not None:
|
||||
out[name] = _serialize_simple_obj(val)
|
||||
continue
|
||||
|
||||
# If it's a collection and no subfields were requested, emit a light list
|
||||
if isinstance(val, (list, tuple)):
|
||||
out[name] = [_serialize_leaf(v) for v in val]
|
||||
else:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue