Skip to content

Commit 5d395d4

Browse files
Add alias support to Property class and related tests (#619)
* Add alias support to Property class and related tests - Introduced `alias_of` parameter in Property to allow aliasing of API attributes. - Implemented `properties_with_alias` method in Base class to retrieve aliased properties. - Updated BetaProgram to include an aliased property for "class". - Added comprehensive tests for alias functionality in PropertyAliasTest. * Update linode_api4/objects/base.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Pre-compute keys * More readable condition * make format --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent e2652d8 commit 5d395d4

File tree

3 files changed

+290
-60
lines changed

3 files changed

+290
-60
lines changed

linode_api4/objects/base.py

Lines changed: 98 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import time
22
from datetime import datetime, timedelta
3+
from functools import cached_property
34
from typing import Any, Dict, Optional
45

56
from linode_api4.objects.serializable import JSONObject
@@ -35,27 +36,43 @@ def __init__(
3536
nullable=False,
3637
unordered=False,
3738
json_object=None,
39+
alias_of: Optional[str] = None,
3840
):
3941
"""
4042
A Property is an attribute returned from the API, and defines metadata
41-
about that value. These are expected to be used as the values of a
43+
about that value. These are expected to be used as the values of a
4244
class-level dict named 'properties' in subclasses of Base.
4345
44-
mutable - This Property should be sent in a call to save()
45-
identifier - This Property identifies the object in the API
46-
volatile - Re-query for this Property if the local value is older than the
47-
volatile refresh timeout
48-
relationship - The API Object this Property represents
49-
derived_class - The sub-collection type this Property represents
50-
is_datetime - True if this Property should be parsed as a datetime.datetime
51-
id_relationship - This Property should create a relationship with this key as the ID
52-
(This should be used on fields ending with '_id' only)
53-
slug_relationship - This property is a slug related for a given type.
54-
nullable - This property can be explicitly null on PUT requests.
55-
unordered - The order of this property is not significant.
56-
NOTE: This field is currently only for annotations purposes
57-
and does not influence any update or decoding/encoding logic.
58-
json_object - The JSONObject class this property should be decoded into.
46+
:param mutable: This Property should be sent in a call to save()
47+
:type mutable: bool
48+
:param identifier: This Property identifies the object in the API
49+
:type identifier: bool
50+
:param volatile: Re-query for this Property if the local value is older than the
51+
volatile refresh timeout
52+
:type volatile: bool
53+
:param relationship: The API Object this Property represents
54+
:type relationship: type or None
55+
:param derived_class: The sub-collection type this Property represents
56+
:type derived_class: type or None
57+
:param is_datetime: True if this Property should be parsed as a datetime.datetime
58+
:type is_datetime: bool
59+
:param id_relationship: This Property should create a relationship with this key as the ID
60+
(This should be used on fields ending with '_id' only)
61+
:type id_relationship: type or None
62+
:param slug_relationship: This property is a slug related for a given type
63+
:type slug_relationship: type or None
64+
:param nullable: This property can be explicitly null on PUT requests
65+
:type nullable: bool
66+
:param unordered: The order of this property is not significant.
67+
NOTE: This field is currently only for annotations purposes
68+
and does not influence any update or decoding/encoding logic.
69+
:type unordered: bool
70+
:param json_object: The JSONObject class this property should be decoded into
71+
:type json_object: type or None
72+
:param alias_of: The original API attribute name when the property key is aliased.
73+
This is useful when the API attribute name is a Python reserved word,
74+
allowing you to use a different key while preserving the original name.
75+
:type alias_of: str or None
5976
"""
6077
self.mutable = mutable
6178
self.identifier = identifier
@@ -68,6 +85,7 @@ def __init__(
6885
self.nullable = nullable
6986
self.unordered = unordered
7087
self.json_class = json_object
88+
self.alias_of = alias_of
7189

7290

7391
class MappedObject:
@@ -252,6 +270,21 @@ def __setattr__(self, name, value):
252270

253271
self._set(name, value)
254272

273+
@cached_property
274+
def properties_with_alias(self) -> dict[str, tuple[str, Property]]:
275+
"""
276+
Gets a dictionary of aliased properties for this object.
277+
278+
:returns: A dict mapping original API attribute names to their alias names and
279+
corresponding Property instances.
280+
:rtype: dict[str, tuple[str, Property]]
281+
"""
282+
return {
283+
prop.alias_of: (alias, prop)
284+
for alias, prop in type(self).properties.items()
285+
if prop.alias_of
286+
}
287+
255288
def save(self, force=True) -> bool:
256289
"""
257290
Send this object's mutable values to the server in a PUT request.
@@ -345,7 +378,8 @@ def _serialize(self, is_put: bool = False):
345378
):
346379
value = None
347380

348-
result[k] = value
381+
api_key = k if not v.alias_of else v.alias_of
382+
result[api_key] = value
349383

350384
# Resolve the underlying IDs of results
351385
for k, v in result.items():
@@ -373,55 +407,55 @@ def _populate(self, json):
373407
self._set("_raw_json", json)
374408
self._set("_updated", False)
375409

376-
for key in json:
377-
if key in (
378-
k
379-
for k in type(self).properties.keys()
380-
if not type(self).properties[k].identifier
381-
):
382-
if (
383-
type(self).properties[key].relationship
384-
and not json[key] is None
385-
):
386-
if isinstance(json[key], list):
410+
valid_keys = set(
411+
k
412+
for k, v in type(self).properties.items()
413+
if (not v.identifier) and (not v.alias_of)
414+
) | set(self.properties_with_alias.keys())
415+
416+
for api_key in json:
417+
if api_key in valid_keys:
418+
prop = type(self).properties.get(api_key)
419+
prop_key = api_key
420+
421+
if prop is None:
422+
prop_key, prop = self.properties_with_alias[api_key]
423+
424+
if prop.relationship and json[api_key] is not None:
425+
if isinstance(json[api_key], list):
387426
objs = []
388-
for d in json[key]:
427+
for d in json[api_key]:
389428
if not "id" in d:
390429
continue
391-
new_class = type(self).properties[key].relationship
430+
new_class = prop.relationship
392431
obj = new_class.make_instance(
393432
d["id"], getattr(self, "_client")
394433
)
395434
if obj:
396435
obj._populate(d)
397436
objs.append(obj)
398-
self._set(key, objs)
437+
self._set(prop_key, objs)
399438
else:
400-
if isinstance(json[key], dict):
401-
related_id = json[key]["id"]
439+
if isinstance(json[api_key], dict):
440+
related_id = json[api_key]["id"]
402441
else:
403-
related_id = json[key]
404-
new_class = type(self).properties[key].relationship
442+
related_id = json[api_key]
443+
new_class = prop.relationship
405444
obj = new_class.make_instance(
406445
related_id, getattr(self, "_client")
407446
)
408-
if obj and isinstance(json[key], dict):
409-
obj._populate(json[key])
410-
self._set(key, obj)
411-
elif (
412-
type(self).properties[key].slug_relationship
413-
and not json[key] is None
414-
):
447+
if obj and isinstance(json[api_key], dict):
448+
obj._populate(json[api_key])
449+
self._set(prop_key, obj)
450+
elif prop.slug_relationship and json[api_key] is not None:
415451
# create an object of the expected type with the given slug
416452
self._set(
417-
key,
418-
type(self)
419-
.properties[key]
420-
.slug_relationship(self._client, json[key]),
453+
prop_key,
454+
prop.slug_relationship(self._client, json[api_key]),
421455
)
422-
elif type(self).properties[key].json_class:
423-
json_class = type(self).properties[key].json_class
424-
json_value = json[key]
456+
elif prop.json_class:
457+
json_class = prop.json_class
458+
json_value = json[api_key]
425459

426460
# build JSON object
427461
if isinstance(json_value, list):
@@ -430,25 +464,29 @@ def _populate(self, json):
430464
else:
431465
value = json_class.from_json(json_value)
432466

433-
self._set(key, value)
434-
elif type(json[key]) is dict:
435-
self._set(key, MappedObject(**json[key]))
436-
elif type(json[key]) is list:
467+
self._set(prop_key, value)
468+
elif type(json[api_key]) is dict:
469+
self._set(prop_key, MappedObject(**json[api_key]))
470+
elif type(json[api_key]) is list:
437471
# we're going to use MappedObject's behavior with lists to
438472
# expand these, then grab the resulting value to set
439-
mapping = MappedObject(_list=json[key])
440-
self._set(key, mapping._list) # pylint: disable=no-member
441-
elif type(self).properties[key].is_datetime:
473+
mapping = MappedObject(_list=json[api_key])
474+
self._set(
475+
prop_key, mapping._list
476+
) # pylint: disable=no-member
477+
elif prop.is_datetime:
442478
try:
443-
t = time.strptime(json[key], DATE_FORMAT)
444-
self._set(key, datetime.fromtimestamp(time.mktime(t)))
479+
t = time.strptime(json[api_key], DATE_FORMAT)
480+
self._set(
481+
prop_key, datetime.fromtimestamp(time.mktime(t))
482+
)
445483
except:
446484
# if this came back, there's probably an issue with the
447485
# python library; a field was marked as a datetime but
448486
# wasn't in the expected format.
449-
self._set(key, json[key])
487+
self._set(prop_key, json[api_key])
450488
else:
451-
self._set(key, json[key])
489+
self._set(prop_key, json[api_key])
452490

453491
self._set("_populated", True)
454492
self._set("_last_updated", datetime.now())

linode_api4/objects/beta.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@ class BetaProgram(Base):
1919
"ended": Property(is_datetime=True),
2020
"greenlight_only": Property(),
2121
"more_info": Property(),
22+
"beta_class": Property(alias_of="class"),
2223
}

0 commit comments

Comments
 (0)