Skip to content
6 changes: 2 additions & 4 deletions sdk/core/azure-core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
# Release History

## 1.36.1 (Unreleased)
## 1.37.0 (2025-12-08)

### Features Added

### Breaking Changes

### Bugs Fixed
- Added `get_backcompat_attr_name` to `azure.core.serialization`. `get_backcompat_attr_name` gets the backcompat name of an attribute using backcompat attribute access. #44084

### Other Changes

Expand Down
2 changes: 1 addition & 1 deletion sdk/core/azure-core/azure/core/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@
# regenerated.
# --------------------------------------------------------------------------

VERSION = "1.36.1"
VERSION = "1.37.0"
79 changes: 62 additions & 17 deletions sdk/core/azure-core/azure/core/serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# Licensed under the MIT License. See License.txt in the project root for
# license information.
# --------------------------------------------------------------------------
# pylint: disable=protected-access
import base64
from functools import partial
from json import JSONEncoder
Expand All @@ -19,6 +20,7 @@
"as_attribute_dict",
"attribute_list",
"TypeHandlerRegistry",
"get_backcompat_attr_name",
]
TZ_UTC = timezone.utc

Expand Down Expand Up @@ -317,7 +319,7 @@ def _is_readonly(p: Any) -> bool:
:rtype: bool
"""
try:
return p._visibility == ["read"] # pylint: disable=protected-access
return p._visibility == ["read"]
except AttributeError:
return False

Expand All @@ -332,6 +334,20 @@ def _as_attribute_dict_value(v: Any, *, exclude_readonly: bool = False) -> Any:
return as_attribute_dict(v, exclude_readonly=exclude_readonly) if is_generated_model(v) else v


def _get_backcompat_name(rest_field: Any, default_attr_name: str) -> str:
"""Get the backcompat name for an attribute.

:param any rest_field: The rest field to get the backcompat name from.
:param str default_attr_name: The default attribute name to use if no backcompat name
:return: The backcompat name.
:rtype: str
"""
try:
return rest_field._original_tsp_name or default_attr_name
except AttributeError:
return default_attr_name


def _get_flattened_attribute(obj: Any) -> Optional[str]:
"""Get the name of the flattened attribute in a generated TypeSpec model if one exists.

Expand All @@ -348,11 +364,9 @@ def _get_flattened_attribute(obj: Any) -> Optional[str]:
if flattened_items is None:
return None

for k, v in obj._attr_to_rest_field.items(): # pylint: disable=protected-access
for k, v in obj._attr_to_rest_field.items():
try:
if set(v._class_type._attr_to_rest_field.keys()).intersection( # pylint: disable=protected-access
set(flattened_items)
):
if set(v._class_type._attr_to_rest_field.keys()).intersection(set(flattened_items)):
return k
except AttributeError:
# if the attribute does not have _class_type, it is not a typespec generated model
Expand All @@ -372,12 +386,12 @@ def attribute_list(obj: Any) -> List[str]:
raise TypeError("Object is not a generated SDK model.")
if hasattr(obj, "_attribute_map"):
# msrest model
return list(obj._attribute_map.keys()) # pylint: disable=protected-access
return list(obj._attribute_map.keys())
flattened_attribute = _get_flattened_attribute(obj)
retval: List[str] = []
for attr_name, rest_field in obj._attr_to_rest_field.items(): # pylint: disable=protected-access
for attr_name, rest_field in obj._attr_to_rest_field.items():
if flattened_attribute == attr_name:
retval.extend(attribute_list(rest_field._class_type)) # pylint: disable=protected-access
retval.extend(attribute_list(rest_field._class_type))
else:
retval.append(attr_name)
return retval
Expand Down Expand Up @@ -410,16 +424,16 @@ def as_attribute_dict(obj: Any, *, exclude_readonly: bool = False) -> Dict[str,
# create a reverse mapping from rest field name to attribute name
rest_to_attr = {}
flattened_attribute = _get_flattened_attribute(obj)
for attr_name, rest_field in obj._attr_to_rest_field.items(): # pylint: disable=protected-access
for attr_name, rest_field in obj._attr_to_rest_field.items():

if exclude_readonly and _is_readonly(rest_field):
# if we're excluding readonly properties, we need to track them
readonly_props.add(rest_field._rest_name) # pylint: disable=protected-access
readonly_props.add(rest_field._rest_name)
if flattened_attribute == attr_name:
for fk, fv in rest_field._class_type._attr_to_rest_field.items(): # pylint: disable=protected-access
rest_to_attr[fv._rest_name] = fk # pylint: disable=protected-access
for fk, fv in rest_field._class_type._attr_to_rest_field.items():
rest_to_attr[fv._rest_name] = fk
else:
rest_to_attr[rest_field._rest_name] = attr_name # pylint: disable=protected-access
rest_to_attr[rest_field._rest_name] = attr_name
for k, v in obj.items():
if exclude_readonly and k in readonly_props: # pyright: ignore
continue
Expand All @@ -429,10 +443,8 @@ def as_attribute_dict(obj: Any, *, exclude_readonly: bool = False) -> Dict[str,
else:
is_multipart_file_input = False
try:
is_multipart_file_input = next( # pylint: disable=protected-access
rf
for rf in obj._attr_to_rest_field.values() # pylint: disable=protected-access
if rf._rest_name == k # pylint: disable=protected-access
is_multipart_file_input = next(
rf for rf in obj._attr_to_rest_field.values() if rf._rest_name == k
)._is_multipart_file_input
except StopIteration:
pass
Expand All @@ -444,3 +456,36 @@ def as_attribute_dict(obj: Any, *, exclude_readonly: bool = False) -> Dict[str,
except AttributeError as exc:
# not a typespec generated model
raise TypeError("Object must be a generated model instance.") from exc


def get_backcompat_attr_name(model: Any, attr_name: str) -> str:
"""Get the backcompat attribute name for a given attribute.

This function takes an attribute name and returns the backcompat name (original TSP name)
if one exists, otherwise returns the attribute name itself.

:param any model: The model instance.
:param str attr_name: The attribute name to get the backcompat name for.
:return: The backcompat attribute name (original TSP name) or the attribute name itself.
:rtype: str
"""
if not is_generated_model(model):
raise TypeError("Object must be a generated model instance.")

# Check if attr_name exists in the model's attributes
flattened_attribute = _get_flattened_attribute(model)
for field_attr_name, rest_field in model._attr_to_rest_field.items():
# Check if this is the attribute we're looking for
if field_attr_name == attr_name:
# Return the original TSP name if it exists, otherwise the attribute name
return _get_backcompat_name(rest_field, attr_name)

# If this is a flattened attribute, check inside it
if flattened_attribute == field_attr_name:
for fk, fv in rest_field._class_type._attr_to_rest_field.items():
if fk == attr_name:
# Return the original TSP name for this flattened property
return _get_backcompat_name(fv, fk)

# If not found in the model, just return the attribute name as-is
return attr_name
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,10 @@ def __new__(cls, *args: typing.Any, **kwargs: typing.Any) -> Self:
if not rf._rest_name_input:
rf._rest_name_input = attr
cls._attr_to_rest_field: typing.Dict[str, _RestField] = dict(attr_to_rest_field.items())
cls._backcompat_attr_to_rest_field: typing.Dict[str, _RestField] = {
Model._get_backcompat_attribute_name(cls._attr_to_rest_field, attr): rf
for attr, rf in cls._attr_to_rest_field.items()
}
cls._calculated.add(f"{cls.__module__}.{cls.__qualname__}")

return super().__new__(cls)
Expand All @@ -663,6 +667,16 @@ def __init_subclass__(cls, discriminator: typing.Optional[str] = None) -> None:
if hasattr(base, "__mapping__"):
base.__mapping__[discriminator or cls.__name__] = cls # type: ignore

@classmethod
def _get_backcompat_attribute_name(cls, _attr_to_rest_field: typing.Dict[str, "_RestField"], attr_name: str) -> str:
rest_field = _attr_to_rest_field.get(attr_name) # pylint: disable=protected-access
if rest_field is None:
return attr_name
original_tsp_name = getattr(rest_field, "_original_tsp_name", None) # pylint: disable=protected-access
if original_tsp_name:
return original_tsp_name
return attr_name

@classmethod
def _get_discriminator(cls, exist_discriminators) -> typing.Optional["_RestField"]:
for v in cls.__dict__.values():
Expand Down Expand Up @@ -998,6 +1012,7 @@ def __init__(
format: typing.Optional[str] = None,
is_multipart_file_input: bool = False,
xml: typing.Optional[typing.Dict[str, typing.Any]] = None,
original_tsp_name: typing.Optional[str] = None,
):
self._type = type
self._rest_name_input = name
Expand All @@ -1009,6 +1024,7 @@ def __init__(
self._format = format
self._is_multipart_file_input = is_multipart_file_input
self._xml = xml if xml is not None else {}
self._original_tsp_name = original_tsp_name

@property
def _class_type(self) -> typing.Any:
Expand Down Expand Up @@ -1060,6 +1076,7 @@ def rest_field(
format: typing.Optional[str] = None,
is_multipart_file_input: bool = False,
xml: typing.Optional[typing.Dict[str, typing.Any]] = None,
original_tsp_name: typing.Optional[str] = None,
) -> typing.Any:
return _RestField(
name=name,
Expand All @@ -1069,6 +1086,7 @@ def rest_field(
format=format,
is_multipart_file_input=is_multipart_file_input,
xml=xml,
original_tsp_name=original_tsp_name,
)


Expand Down
Loading
Loading