Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sdk/core/azure-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- 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
- 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.
- Added `is_generated_model` method to `azure.core.serialization`. Returns whether a given input is a model from one of our generated sdks. #41445
- Added `attribute_list` method to `azure.core.serialization`. Returns all of the attributes of a given model from one of our generated sdks. #41571

### Breaking Changes

Expand Down
31 changes: 30 additions & 1 deletion sdk/core/azure-core/azure/core/serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# --------------------------------------------------------------------------
import base64
from json import JSONEncoder
from typing import Dict, Optional, Union, cast, Any
from typing import Dict, List, Optional, Union, cast, Any
from datetime import datetime, date, time, timedelta
from datetime import timezone

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


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

:param any obj: The object to check.
:return: The name of the flattened attribute if it exists, otherwise None.
:rtype: Optional[str]
"""
flattened_items = None
try:
flattened_items = getattr(obj, next(a for a in dir(obj) if "__flattened_items" in a), None)
Expand All @@ -187,6 +193,29 @@ def _get_flattened_attribute(obj: Any) -> Optional[str]:
return None


def attribute_list(obj: Any) -> List[str]:
"""Get a list of attribute names for a generated SDK model.

:param obj: The object to get attributes from.
:type obj: any
:return: A list of attribute names.
:rtype: List[str]
"""
if not is_generated_model(obj):
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
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
if flattened_attribute == attr_name:
retval.extend(attribute_list(rest_field._class_type)) # pylint: disable=protected-access
else:
retval.append(attr_name)
return retval


def as_attribute_dict(obj: Any, *, exclude_readonly: bool = False) -> Dict[str, Any]:
"""Convert an object to a dictionary of its attributes.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,29 @@ def __init__(self, *, additional_properties: Optional[Dict[str, Any]], name: Opt
self.name = name


class MsrestClientNameAndJsonEncodedNameModel(MsrestModel):
_attribute_map = {
"client_name": {"key": "wireName", "type": "str"},
}

def __init__(self, *, client_name: str, **kwargs) -> None:
super().__init__(**kwargs)
self.client_name = client_name


class MsrestReadonlyModel(MsrestModel):
_validation = {
"id": {"readonly": True},
}
_attribute_map = {
"id": {"key": "readonlyProp", "type": "str"},
}

def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self.id: Optional[str] = None


__all__: List[str] = [
"HybridPet",
"HybridDog",
Expand Down Expand Up @@ -401,6 +424,8 @@ def __init__(self, *, additional_properties: Optional[Dict[str, Any]], name: Opt
"MsrestFlattenModel",
"HybridPetAPTrue",
"MsrestPetAPTrue",
"MsrestClientNameAndJsonEncodedNameModel",
"MsrestReadonlyModel",
] # Add all objects you want publicly available to users at this package level


Expand Down
71 changes: 70 additions & 1 deletion sdk/core/azure-core/tests/test_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from typing import Any, Dict, List, Optional
from io import BytesIO

from azure.core.serialization import AzureJSONEncoder, NULL, as_attribute_dict, is_generated_model
from azure.core.serialization import AzureJSONEncoder, NULL, as_attribute_dict, is_generated_model, attribute_list
import pytest
from modeltypes._utils.model_base import Model as HybridModel, rest_field
from modeltypes._utils.serialization import Model as MsrestModel
Expand Down Expand Up @@ -547,6 +547,75 @@ def __init__(self):
assert not is_generated_model(Model())


def test_attribute_list_non_model():
with pytest.raises(TypeError):
attribute_list({})

with pytest.raises(TypeError):
attribute_list([])

with pytest.raises(TypeError):
attribute_list("string")

with pytest.raises(TypeError):
attribute_list(42)

with pytest.raises(TypeError):
attribute_list(None)

with pytest.raises(TypeError):
attribute_list(object)

class RandomModel:
def __init__(self):
self.attr = "value"

with pytest.raises(TypeError):
attribute_list(RandomModel())


def test_attribute_list_scratch_model():
model = models.HybridPet(name="wall-e", species="dog")
assert attribute_list(model) == ["name", "species"]
msrest_model = models.MsrestPet(name="wall-e", species="dog")
assert attribute_list(msrest_model) == ["name", "species"]


def test_attribute_list_client_named_property_model():
model = models.ClientNameAndJsonEncodedNameModel(client_name="wall-e")
assert attribute_list(model) == ["client_name"]
msrest_model = models.MsrestClientNameAndJsonEncodedNameModel(client_name="wall-e")
assert attribute_list(msrest_model) == ["client_name"]


def test_attribute_list_flattened_model():
model = models.FlattenModel(name="wall-e", description="a dog", age=2)
assert attribute_list(model) == ["name", "description", "age"]
msrest_model = models.MsrestFlattenModel(name="wall-e", description="a dog", age=2)
assert attribute_list(msrest_model) == ["name", "description", "age"]


def test_attribute_list_readonly_model():
model = models.ReadonlyModel({"id": 1})
assert attribute_list(model) == ["id"]
msrest_model = models.MsrestReadonlyModel(id=1)
assert attribute_list(msrest_model) == ["id"]


def test_attribute_list_additional_properties_hybrid():
hybrid_model = models.HybridPetAPTrue(
{"birthdate": "2017-12-13T02:29:51Z", "complexProperty": {"color": "Red"}, "name": "Buddy"}
)
assert attribute_list(hybrid_model) == ["name"]


def test_attribute_list_additional_properties_msrest():
msrest_model = models.MsrestPetAPTrue(
additional_properties={"birthdate": "2017-12-13T02:29:51Z", "complexProperty": {"color": "Red"}}, name="Buddy"
)
assert attribute_list(msrest_model) == ["additional_properties", "name"]


def test_as_attribute_dict_client_name():
model = models.ClientNameAndJsonEncodedNameModel(client_name="wall-e")
assert model.as_dict() == {"wireName": "wall-e"}
Expand Down
Loading