Skip to content
Open
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
**Bug fixes:**

- fix(dictionary): Correct example dictionary name to use valid characters
- fix(model): Make `ModelSimple` behave like a sequence when `value` is a list/tuple so generated response models (e.g. `DomainsResponse`) can be iterated, indexed and sized.

**Enhancements:**

Expand Down
60 changes: 60 additions & 0 deletions fastly/model_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,31 @@ def get(self, name, default=None):

def __getitem__(self, name):
"""get the value of an attribute using square-bracket notation: `instance[attr]`"""
# Support numeric indexing for simple models that wrap a list-like
# value (for example response models that have `value: [Item]`).
# Iteration (for x in model) falls back to __getitem__ with integer
# indices if __iter__ is not present, so accept ints and slices here.
if isinstance(name, (int, slice)):
try:
v = self.__dict__['_data_store'].get('value')
except Exception:
v = None
if v is None:
raise ApiAttributeError(
"{0} has no attribute '{1}'".format(
type(self).__name__, name),
[e for e in [self._path_to_item, name] if e]
)
try:
return v[name]
except Exception:
# normalize to attribute error expected by callers
raise ApiAttributeError(
"{0} has no attribute '{1}'".format(
type(self).__name__, name),
[e for e in [self._path_to_item, name] if e]
)

if name in self:
return self.get(name)

Expand All @@ -471,6 +496,41 @@ def __getitem__(self, name):
[e for e in [self._path_to_item, name] if e]
)

def __iter__(self):
"""Allow iteration over simple models that wrap a sequence in `value`.

Example: for item in DomainsResponse(...): iterates over the underlying list.
"""
try:
v = self.__dict__['_data_store'].get('value')
except Exception:
v = None
if v is None:
# behave like an empty iterator
return iter(())
# Only treat list/tuple as a true sequence to iterate over. Dicts are
# technically iterable (yield keys) but in API models the `value`
# field may contain a dict representing a single item. Treat non
# (list, tuple) values as single items.
if isinstance(v, (list, tuple)):
return iter(v)
# Otherwise wrap single value in an iterator
return iter((v,))

def __len__(self):
"""Return length of the underlying sequence if present, otherwise 0."""
try:
v = self.__dict__['_data_store'].get('value')
except Exception:
return 0
if v is None:
return 0
# If underlying value is a sequence (list/tuple), return its length.
if isinstance(v, (list, tuple)):
return len(v)
# For dict or other single objects, treat as single item.
return 1

def __contains__(self, name):
"""used by `in` operator to check if an attribute value was set in an instance: `'attr' in instance`"""
if name in self.required_properties:
Expand Down
38 changes: 38 additions & 0 deletions tests/test_model_simple_sequence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import pytest
from fastly.model.domains_response import DomainsResponse


def test_domains_response_iteration_and_indexing():
data = [{'name': 'a'}, {'name': 'b'}, {'name': 'c'}]
d = DomainsResponse(data, _check_type=False)

# len
assert len(d) == 3

# list conversion
assert list(d) == data

# indexing
assert d[0] == data[0]
assert d[1] == data[1]

# slicing
assert d[1:3] == data[1:3]

# iteration via for
out = []
for it in d:
out.append(it)
assert out == data


def test_domains_response_empty_and_single():
# empty
d_empty = DomainsResponse([], _check_type=False)
assert len(d_empty) == 0
assert list(d_empty) == []

# single non-sequence value should be iterable as single item
d_single = DomainsResponse({'k': 'v'}, _check_type=False)
assert len(d_single) == 1
assert list(d_single) == [{'k': 'v'}]