Skip to content

Fix: Support iteration and indexing for ModelSimple list responses#109

Open
Xyerophyte wants to merge 3 commits into
fastly:mainfrom
Xyerophyte:copilot/vscode1761996679213
Open

Fix: Support iteration and indexing for ModelSimple list responses#109
Xyerophyte wants to merge 3 commits into
fastly:mainfrom
Xyerophyte:copilot/vscode1761996679213

Conversation

@Xyerophyte

Copy link
Copy Markdown

Problem

The DomainsResponse class (and other ModelSimple-based list response models) could not be iterated, causing ApiAttributeError when attempting common operations:

domains = domain_api.DomainApi(api_client).list_domains(id, version)
[domain for domain in domains]  # ❌ ApiAttributeError: DomainsResponse has no attribute '0'

Users had to access the internal _data_store directly, breaking the abstraction.

Root Cause

DomainsResponse inherits from ModelSimple and stores a list in its value attribute. When Python's iteration protocol calls __getitem__(0), the existing implementation only accepted string attribute names, not integer indices.

Solution

Enhanced the ModelSimple class in fastly/model_utils.py with sequence protocol support:

  1. __getitem__ - Now accepts integers and slices, delegating to the underlying value list/tuple
  2. __iter__ - Returns an iterator over the sequence in value
  3. __len__ - Returns the length of the sequence in value

The implementation intelligently handles:

  • Lists and tuples as sequences (iterable)
  • Dicts and other objects as single items (to avoid iterating dict keys)
  • Backward compatibility with existing attribute access

What Now Works

domains = domain_api.DomainApi(api_client).list_domains(id, version)

len(domains)              # ✓ Get count
domains[0]                # ✓ Index access  
domains[1:3]              # ✓ Slicing
list(domains)             # ✓ Convert to list
[d for d in domains]      # ✓ List comprehension
for domain in domains:    # ✓ Iteration
    print(domain)

Testing

  • Added comprehensive test suite in tests/test_model_simple_sequence.py
  • Tests cover iteration, indexing, slicing, len(), edge cases (empty, single item)
  • All tests pass ✓

Documentation

  • Updated CHANGELOG.md (v12.0.0 bug fixes section)
  • Fix is backward-compatible with existing code

Impact

  • Affects all ModelSimple instances that wrap sequences
  • Makes list-based response models behave intuitively
  • No breaking changes - purely additive functionality

- Add __iter__, __len__, and enhanced __getitem__ to ModelSimple
- Fixes ApiAttributeError when iterating DomainsResponse and similar models
- Users can now use for loops, list comprehensions, indexing, and len()
- Add comprehensive test coverage in test_model_simple_sequence.py
- Update CHANGELOG.md for v12.0.0

Fixes issue where DomainsResponse._data_store had to be accessed directly
to iterate over domain lists. Now DomainsResponse and other ModelSimple
list-based responses behave like native Python sequences.
Copilot AI review requested due to automatic review settings April 12, 2026 11:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the Python client’s ModelSimple base class to better support list-style response models (e.g., DomainsResponse) by implementing sequence-like behavior, and adds tests + changelog entry to validate and document the change.

Changes:

  • Extend ModelSimple with sequence protocol support (__getitem__ for int/slice, plus __iter__ and __len__) backed by the underlying value.
  • Add pytest coverage for iteration, indexing, slicing, and len() on DomainsResponse.
  • Document the behavior in the changelog.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.

File Description
fastly/model_utils.py Adds sequence semantics to ModelSimple to make list-wrapping response models iterable/indexable.
tests/test_model_simple_sequence.py Introduces tests verifying the new iteration/indexing/slicing/len behavior.
CHANGELOG.md Notes the ModelSimple sequence behavior change under bug fixes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread fastly/model_utils.py
Comment on lines +475 to +479
raise ApiAttributeError(
"{0} has no attribute '{1}'".format(
type(self).__name__, name),
[e for e in [self._path_to_item, name] if e]
)

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the new int/slice error path construction, the list comprehension uses if e, which will drop index 0 (because 0 is falsy). That can lose useful context for callers when model[0] fails. Consider filtering with is not None (or similar) so index 0 is preserved in the path.

Copilot uses AI. Check for mistakes.
Comment thread fastly/model_utils.py
Comment on lines +480 to +489
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]
)

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

__getitem__ returns v[name] for int/slice names without verifying that value is actually a list/tuple. This is inconsistent with __iter__/__len__, which treat non-(list, tuple) value as a single item; e.g., len(model)==1 but model[0] can still raise. Consider limiting numeric indexing to list/tuple values, or explicitly handling index 0/slices for the single-item case so the sequence semantics stay consistent.

Suggested change
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 isinstance(v, (list, tuple)):
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 isinstance(name, slice):
return [v][name]
if name in (0, -1):
return v
raise ApiAttributeError(
"{0} has no attribute '{1}'".format(
type(self).__name__, name),
[e for e in [self._path_to_item, name] if e]
)

Copilot uses AI. Check for mistakes.
Comment thread fastly/model_utils.py
Comment on lines +520 to +524
def __len__(self):
"""Return length of the underlying sequence if present, otherwise 0."""
try:
v = self.__dict__['_data_store'].get('value')
except Exception:

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding __len__ changes truthiness for ModelSimple instances (Python will use __len__ for bool(model) when __bool__ is not defined). For list-based responses this means empty responses become falsy, which can be a backward-incompatible behavioral change. If that’s not intended, consider defining __bool__ to preserve prior truthiness, or documenting this as a behavior change.

Copilot uses AI. Check for mistakes.
Comment thread fastly/model_utils.py
Comment on lines +528 to +532
# 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

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The __len__ docstring says “otherwise 0”, but the implementation returns 1 for non-(list, tuple) values (treating them as a single item). Update the docstring to match the actual behavior.

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,38 @@
import pytest

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pytest is imported but not used in this test file. Removing the unused import keeps the test module clean and avoids unused-import lint noise (if/when linting is enabled).

Suggested change
import pytest

Copilot uses AI. Check for mistakes.
Comment thread CHANGELOG.md
**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.

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new changelog entry is indented with an extra leading space ( - fix(model): ...), which will render as a nested list item in Markdown. Align it with the other bullets under Bug fixes: (no leading space before -).

Suggested change
- 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.
- 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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants