Skip to content

Commit bea2830

Browse files
authored
[core] add attribute_list (#41571)
1 parent 7337018 commit bea2830

File tree

4 files changed

+126
-2
lines changed

4 files changed

+126
-2
lines changed

sdk/core/azure-core/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- Added a `context` keyword argument to the `start_span` and `start_as_current_span` methods in the `OpenTelemetryTracer` class. This allows users to specify a custom parent context for created spans. #41511
99
- Added method `as_attribute_dict` to `azure.core.serialization` for backcompat migration purposes. Will return a generated model as a dictionary where the keys are in attribute syntax.
1010
- Added `is_generated_model` method to `azure.core.serialization`. Returns whether a given input is a model from one of our generated sdks. #41445
11+
- Added `attribute_list` method to `azure.core.serialization`. Returns all of the attributes of a given model from one of our generated sdks. #41571
1112

1213
### Breaking Changes
1314

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

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
# --------------------------------------------------------------------------
77
import base64
88
from json import JSONEncoder
9-
from typing import Dict, Optional, Union, cast, Any
9+
from typing import Dict, List, Optional, Union, cast, Any
1010
from datetime import datetime, date, time, timedelta
1111
from datetime import timezone
1212

@@ -166,6 +166,12 @@ def _as_attribute_dict_value(v: Any, *, exclude_readonly: bool = False) -> Any:
166166

167167

168168
def _get_flattened_attribute(obj: Any) -> Optional[str]:
169+
"""Get the name of the flattened attribute in a generated TypeSpec model if one exists.
170+
171+
:param any obj: The object to check.
172+
:return: The name of the flattened attribute if it exists, otherwise None.
173+
:rtype: Optional[str]
174+
"""
169175
flattened_items = None
170176
try:
171177
flattened_items = getattr(obj, next(a for a in dir(obj) if "__flattened_items" in a), None)
@@ -187,6 +193,29 @@ def _get_flattened_attribute(obj: Any) -> Optional[str]:
187193
return None
188194

189195

196+
def attribute_list(obj: Any) -> List[str]:
197+
"""Get a list of attribute names for a generated SDK model.
198+
199+
:param obj: The object to get attributes from.
200+
:type obj: any
201+
:return: A list of attribute names.
202+
:rtype: List[str]
203+
"""
204+
if not is_generated_model(obj):
205+
raise TypeError("Object is not a generated SDK model.")
206+
if hasattr(obj, "_attribute_map"):
207+
# msrest model
208+
return list(obj._attribute_map.keys()) # pylint: disable=protected-access
209+
flattened_attribute = _get_flattened_attribute(obj)
210+
retval: List[str] = []
211+
for attr_name, rest_field in obj._attr_to_rest_field.items(): # pylint: disable=protected-access
212+
if flattened_attribute == attr_name:
213+
retval.extend(attribute_list(rest_field._class_type)) # pylint: disable=protected-access
214+
else:
215+
retval.append(attr_name)
216+
return retval
217+
218+
190219
def as_attribute_dict(obj: Any, *, exclude_readonly: bool = False) -> Dict[str, Any]:
191220
"""Convert an object to a dictionary of its attributes.
192221

sdk/core/azure-core/tests/specs_sdk/modeltypes/modeltypes/models/_patch.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,29 @@ def __init__(self, *, additional_properties: Optional[Dict[str, Any]], name: Opt
370370
self.name = name
371371

372372

373+
class MsrestClientNameAndJsonEncodedNameModel(MsrestModel):
374+
_attribute_map = {
375+
"client_name": {"key": "wireName", "type": "str"},
376+
}
377+
378+
def __init__(self, *, client_name: str, **kwargs) -> None:
379+
super().__init__(**kwargs)
380+
self.client_name = client_name
381+
382+
383+
class MsrestReadonlyModel(MsrestModel):
384+
_validation = {
385+
"id": {"readonly": True},
386+
}
387+
_attribute_map = {
388+
"id": {"key": "readonlyProp", "type": "str"},
389+
}
390+
391+
def __init__(self, **kwargs) -> None:
392+
super().__init__(**kwargs)
393+
self.id: Optional[str] = None
394+
395+
373396
__all__: List[str] = [
374397
"HybridPet",
375398
"HybridDog",
@@ -401,6 +424,8 @@ def __init__(self, *, additional_properties: Optional[Dict[str, Any]], name: Opt
401424
"MsrestFlattenModel",
402425
"HybridPetAPTrue",
403426
"MsrestPetAPTrue",
427+
"MsrestClientNameAndJsonEncodedNameModel",
428+
"MsrestReadonlyModel",
404429
] # Add all objects you want publicly available to users at this package level
405430

406431

sdk/core/azure-core/tests/test_serialization.py

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from typing import Any, Dict, List, Optional
1111
from io import BytesIO
1212

13-
from azure.core.serialization import AzureJSONEncoder, NULL, as_attribute_dict, is_generated_model
13+
from azure.core.serialization import AzureJSONEncoder, NULL, as_attribute_dict, is_generated_model, attribute_list
1414
import pytest
1515
from modeltypes._utils.model_base import Model as HybridModel, rest_field
1616
from modeltypes._utils.serialization import Model as MsrestModel
@@ -547,6 +547,75 @@ def __init__(self):
547547
assert not is_generated_model(Model())
548548

549549

550+
def test_attribute_list_non_model():
551+
with pytest.raises(TypeError):
552+
attribute_list({})
553+
554+
with pytest.raises(TypeError):
555+
attribute_list([])
556+
557+
with pytest.raises(TypeError):
558+
attribute_list("string")
559+
560+
with pytest.raises(TypeError):
561+
attribute_list(42)
562+
563+
with pytest.raises(TypeError):
564+
attribute_list(None)
565+
566+
with pytest.raises(TypeError):
567+
attribute_list(object)
568+
569+
class RandomModel:
570+
def __init__(self):
571+
self.attr = "value"
572+
573+
with pytest.raises(TypeError):
574+
attribute_list(RandomModel())
575+
576+
577+
def test_attribute_list_scratch_model():
578+
model = models.HybridPet(name="wall-e", species="dog")
579+
assert attribute_list(model) == ["name", "species"]
580+
msrest_model = models.MsrestPet(name="wall-e", species="dog")
581+
assert attribute_list(msrest_model) == ["name", "species"]
582+
583+
584+
def test_attribute_list_client_named_property_model():
585+
model = models.ClientNameAndJsonEncodedNameModel(client_name="wall-e")
586+
assert attribute_list(model) == ["client_name"]
587+
msrest_model = models.MsrestClientNameAndJsonEncodedNameModel(client_name="wall-e")
588+
assert attribute_list(msrest_model) == ["client_name"]
589+
590+
591+
def test_attribute_list_flattened_model():
592+
model = models.FlattenModel(name="wall-e", description="a dog", age=2)
593+
assert attribute_list(model) == ["name", "description", "age"]
594+
msrest_model = models.MsrestFlattenModel(name="wall-e", description="a dog", age=2)
595+
assert attribute_list(msrest_model) == ["name", "description", "age"]
596+
597+
598+
def test_attribute_list_readonly_model():
599+
model = models.ReadonlyModel({"id": 1})
600+
assert attribute_list(model) == ["id"]
601+
msrest_model = models.MsrestReadonlyModel(id=1)
602+
assert attribute_list(msrest_model) == ["id"]
603+
604+
605+
def test_attribute_list_additional_properties_hybrid():
606+
hybrid_model = models.HybridPetAPTrue(
607+
{"birthdate": "2017-12-13T02:29:51Z", "complexProperty": {"color": "Red"}, "name": "Buddy"}
608+
)
609+
assert attribute_list(hybrid_model) == ["name"]
610+
611+
612+
def test_attribute_list_additional_properties_msrest():
613+
msrest_model = models.MsrestPetAPTrue(
614+
additional_properties={"birthdate": "2017-12-13T02:29:51Z", "complexProperty": {"color": "Red"}}, name="Buddy"
615+
)
616+
assert attribute_list(msrest_model) == ["additional_properties", "name"]
617+
618+
550619
def test_as_attribute_dict_client_name():
551620
model = models.ClientNameAndJsonEncodedNameModel(client_name="wall-e")
552621
assert model.as_dict() == {"wireName": "wall-e"}

0 commit comments

Comments
 (0)