Skip to content

Commit cbb1db6

Browse files
iscai-msftiscai-msftmsyyc
authored
[core] add tests and fix backcompat functions (#44084)
* add tests and fix backcompat functions * add changelog * lint and run black * run black * add get_old_attribute * switch to get_backcompat_attr_name * fix ci * format * update changelog * Update sdk/core/azure-core/CHANGELOG.md * fix changelog analyze --------- Co-authored-by: iscai-msft <[email protected]> Co-authored-by: Yuchao Yan <[email protected]>
1 parent 09f5b04 commit cbb1db6

File tree

5 files changed

+551
-24
lines changed

5 files changed

+551
-24
lines changed

sdk/core/azure-core/CHANGELOG.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
# Release History
22

3-
## 1.36.1 (Unreleased)
3+
## 1.37.0 (2025-12-08)
44

55
### Features Added
66

7-
### Breaking Changes
8-
9-
### Bugs Fixed
7+
- 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
108

119
### Other Changes
1210

sdk/core/azure-core/azure/core/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@
99
# regenerated.
1010
# --------------------------------------------------------------------------
1111

12-
VERSION = "1.36.1"
12+
VERSION = "1.37.0"

sdk/core/azure-core/azure/core/serialization.py

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# Licensed under the MIT License. See License.txt in the project root for
55
# license information.
66
# --------------------------------------------------------------------------
7+
# pylint: disable=protected-access
78
import base64
89
from functools import partial
910
from json import JSONEncoder
@@ -19,6 +20,7 @@
1920
"as_attribute_dict",
2021
"attribute_list",
2122
"TypeHandlerRegistry",
23+
"get_backcompat_attr_name",
2224
]
2325
TZ_UTC = timezone.utc
2426

@@ -317,7 +319,7 @@ def _is_readonly(p: Any) -> bool:
317319
:rtype: bool
318320
"""
319321
try:
320-
return p._visibility == ["read"] # pylint: disable=protected-access
322+
return p._visibility == ["read"]
321323
except AttributeError:
322324
return False
323325

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

334336

337+
def _get_backcompat_name(rest_field: Any, default_attr_name: str) -> str:
338+
"""Get the backcompat name for an attribute.
339+
340+
:param any rest_field: The rest field to get the backcompat name from.
341+
:param str default_attr_name: The default attribute name to use if no backcompat name
342+
:return: The backcompat name.
343+
:rtype: str
344+
"""
345+
try:
346+
return rest_field._original_tsp_name or default_attr_name
347+
except AttributeError:
348+
return default_attr_name
349+
350+
335351
def _get_flattened_attribute(obj: Any) -> Optional[str]:
336352
"""Get the name of the flattened attribute in a generated TypeSpec model if one exists.
337353
@@ -348,11 +364,9 @@ def _get_flattened_attribute(obj: Any) -> Optional[str]:
348364
if flattened_items is None:
349365
return None
350366

351-
for k, v in obj._attr_to_rest_field.items(): # pylint: disable=protected-access
367+
for k, v in obj._attr_to_rest_field.items():
352368
try:
353-
if set(v._class_type._attr_to_rest_field.keys()).intersection( # pylint: disable=protected-access
354-
set(flattened_items)
355-
):
369+
if set(v._class_type._attr_to_rest_field.keys()).intersection(set(flattened_items)):
356370
return k
357371
except AttributeError:
358372
# if the attribute does not have _class_type, it is not a typespec generated model
@@ -372,12 +386,12 @@ def attribute_list(obj: Any) -> List[str]:
372386
raise TypeError("Object is not a generated SDK model.")
373387
if hasattr(obj, "_attribute_map"):
374388
# msrest model
375-
return list(obj._attribute_map.keys()) # pylint: disable=protected-access
389+
return list(obj._attribute_map.keys())
376390
flattened_attribute = _get_flattened_attribute(obj)
377391
retval: List[str] = []
378-
for attr_name, rest_field in obj._attr_to_rest_field.items(): # pylint: disable=protected-access
392+
for attr_name, rest_field in obj._attr_to_rest_field.items():
379393
if flattened_attribute == attr_name:
380-
retval.extend(attribute_list(rest_field._class_type)) # pylint: disable=protected-access
394+
retval.extend(attribute_list(rest_field._class_type))
381395
else:
382396
retval.append(attr_name)
383397
return retval
@@ -410,16 +424,16 @@ def as_attribute_dict(obj: Any, *, exclude_readonly: bool = False) -> Dict[str,
410424
# create a reverse mapping from rest field name to attribute name
411425
rest_to_attr = {}
412426
flattened_attribute = _get_flattened_attribute(obj)
413-
for attr_name, rest_field in obj._attr_to_rest_field.items(): # pylint: disable=protected-access
427+
for attr_name, rest_field in obj._attr_to_rest_field.items():
414428

415429
if exclude_readonly and _is_readonly(rest_field):
416430
# if we're excluding readonly properties, we need to track them
417-
readonly_props.add(rest_field._rest_name) # pylint: disable=protected-access
431+
readonly_props.add(rest_field._rest_name)
418432
if flattened_attribute == attr_name:
419-
for fk, fv in rest_field._class_type._attr_to_rest_field.items(): # pylint: disable=protected-access
420-
rest_to_attr[fv._rest_name] = fk # pylint: disable=protected-access
433+
for fk, fv in rest_field._class_type._attr_to_rest_field.items():
434+
rest_to_attr[fv._rest_name] = fk
421435
else:
422-
rest_to_attr[rest_field._rest_name] = attr_name # pylint: disable=protected-access
436+
rest_to_attr[rest_field._rest_name] = attr_name
423437
for k, v in obj.items():
424438
if exclude_readonly and k in readonly_props: # pyright: ignore
425439
continue
@@ -429,10 +443,8 @@ def as_attribute_dict(obj: Any, *, exclude_readonly: bool = False) -> Dict[str,
429443
else:
430444
is_multipart_file_input = False
431445
try:
432-
is_multipart_file_input = next( # pylint: disable=protected-access
433-
rf
434-
for rf in obj._attr_to_rest_field.values() # pylint: disable=protected-access
435-
if rf._rest_name == k # pylint: disable=protected-access
446+
is_multipart_file_input = next(
447+
rf for rf in obj._attr_to_rest_field.values() if rf._rest_name == k
436448
)._is_multipart_file_input
437449
except StopIteration:
438450
pass
@@ -444,3 +456,36 @@ def as_attribute_dict(obj: Any, *, exclude_readonly: bool = False) -> Dict[str,
444456
except AttributeError as exc:
445457
# not a typespec generated model
446458
raise TypeError("Object must be a generated model instance.") from exc
459+
460+
461+
def get_backcompat_attr_name(model: Any, attr_name: str) -> str:
462+
"""Get the backcompat attribute name for a given attribute.
463+
464+
This function takes an attribute name and returns the backcompat name (original TSP name)
465+
if one exists, otherwise returns the attribute name itself.
466+
467+
:param any model: The model instance.
468+
:param str attr_name: The attribute name to get the backcompat name for.
469+
:return: The backcompat attribute name (original TSP name) or the attribute name itself.
470+
:rtype: str
471+
"""
472+
if not is_generated_model(model):
473+
raise TypeError("Object must be a generated model instance.")
474+
475+
# Check if attr_name exists in the model's attributes
476+
flattened_attribute = _get_flattened_attribute(model)
477+
for field_attr_name, rest_field in model._attr_to_rest_field.items():
478+
# Check if this is the attribute we're looking for
479+
if field_attr_name == attr_name:
480+
# Return the original TSP name if it exists, otherwise the attribute name
481+
return _get_backcompat_name(rest_field, attr_name)
482+
483+
# If this is a flattened attribute, check inside it
484+
if flattened_attribute == field_attr_name:
485+
for fk, fv in rest_field._class_type._attr_to_rest_field.items():
486+
if fk == attr_name:
487+
# Return the original TSP name for this flattened property
488+
return _get_backcompat_name(fv, fk)
489+
490+
# If not found in the model, just return the attribute name as-is
491+
return attr_name

sdk/core/azure-core/tests/specs_sdk/modeltypes/modeltypes/_utils/model_base.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,10 @@ def __new__(cls, *args: typing.Any, **kwargs: typing.Any) -> Self:
654654
if not rf._rest_name_input:
655655
rf._rest_name_input = attr
656656
cls._attr_to_rest_field: typing.Dict[str, _RestField] = dict(attr_to_rest_field.items())
657+
cls._backcompat_attr_to_rest_field: typing.Dict[str, _RestField] = {
658+
Model._get_backcompat_attribute_name(cls._attr_to_rest_field, attr): rf
659+
for attr, rf in cls._attr_to_rest_field.items()
660+
}
657661
cls._calculated.add(f"{cls.__module__}.{cls.__qualname__}")
658662

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

670+
@classmethod
671+
def _get_backcompat_attribute_name(cls, _attr_to_rest_field: typing.Dict[str, "_RestField"], attr_name: str) -> str:
672+
rest_field = _attr_to_rest_field.get(attr_name) # pylint: disable=protected-access
673+
if rest_field is None:
674+
return attr_name
675+
original_tsp_name = getattr(rest_field, "_original_tsp_name", None) # pylint: disable=protected-access
676+
if original_tsp_name:
677+
return original_tsp_name
678+
return attr_name
679+
666680
@classmethod
667681
def _get_discriminator(cls, exist_discriminators) -> typing.Optional["_RestField"]:
668682
for v in cls.__dict__.values():
@@ -998,6 +1012,7 @@ def __init__(
9981012
format: typing.Optional[str] = None,
9991013
is_multipart_file_input: bool = False,
10001014
xml: typing.Optional[typing.Dict[str, typing.Any]] = None,
1015+
original_tsp_name: typing.Optional[str] = None,
10011016
):
10021017
self._type = type
10031018
self._rest_name_input = name
@@ -1009,6 +1024,7 @@ def __init__(
10091024
self._format = format
10101025
self._is_multipart_file_input = is_multipart_file_input
10111026
self._xml = xml if xml is not None else {}
1027+
self._original_tsp_name = original_tsp_name
10121028

10131029
@property
10141030
def _class_type(self) -> typing.Any:
@@ -1060,6 +1076,7 @@ def rest_field(
10601076
format: typing.Optional[str] = None,
10611077
is_multipart_file_input: bool = False,
10621078
xml: typing.Optional[typing.Dict[str, typing.Any]] = None,
1079+
original_tsp_name: typing.Optional[str] = None,
10631080
) -> typing.Any:
10641081
return _RestField(
10651082
name=name,
@@ -1069,6 +1086,7 @@ def rest_field(
10691086
format=format,
10701087
is_multipart_file_input=is_multipart_file_input,
10711088
xml=xml,
1089+
original_tsp_name=original_tsp_name,
10721090
)
10731091

10741092

0 commit comments

Comments
 (0)