diff --git a/sdk/core/azure-core/CHANGELOG.md b/sdk/core/azure-core/CHANGELOG.md index eaf7989b23c4..498b16648238 100644 --- a/sdk/core/azure-core/CHANGELOG.md +++ b/sdk/core/azure-core/CHANGELOG.md @@ -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 diff --git a/sdk/core/azure-core/azure/core/serialization.py b/sdk/core/azure-core/azure/core/serialization.py index 22ea11f503ea..1f4dc2dc18dc 100644 --- a/sdk/core/azure-core/azure/core/serialization.py +++ b/sdk/core/azure-core/azure/core/serialization.py @@ -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 @@ -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) @@ -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. diff --git a/sdk/core/azure-core/tests/specs_sdk/modeltypes/modeltypes/models/_patch.py b/sdk/core/azure-core/tests/specs_sdk/modeltypes/modeltypes/models/_patch.py index 4be16eb73560..20f2cffd6b76 100644 --- a/sdk/core/azure-core/tests/specs_sdk/modeltypes/modeltypes/models/_patch.py +++ b/sdk/core/azure-core/tests/specs_sdk/modeltypes/modeltypes/models/_patch.py @@ -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", @@ -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 diff --git a/sdk/core/azure-core/tests/test_serialization.py b/sdk/core/azure-core/tests/test_serialization.py index 0911e1cfe626..b1c808068523 100644 --- a/sdk/core/azure-core/tests/test_serialization.py +++ b/sdk/core/azure-core/tests/test_serialization.py @@ -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 @@ -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"}