Skip to content

AWS: Fix bug on PromotionalResources model#180

Open
JAVGan wants to merge 1 commit intomainfrom
aws_models_fix
Open

AWS: Fix bug on PromotionalResources model#180
JAVGan wants to merge 1 commit intomainfrom
aws_models_fix

Conversation

@JAVGan
Copy link
Collaborator

@JAVGan JAVGan commented Feb 11, 2026

This commit fixes a bug in the PromotionalResources which did expected the attribute promotional_media to be an optional string, while it should be an optional list of resources.

It also introduces the class PromotionalMedia to map the missing resource type.

Refers to SPSTRAT-671

Summary by Sourcery

Fix the AWS PromotionalResources model to correctly represent promotional media as structured resources rather than a simple string.

New Features:

  • Introduce a PromotionalMedia model to represent individual promotional media resources within PromotionalResources.

Bug Fixes:

  • Correct the PromotionalResources.promotional_media field to be an optional list of promotional media resources instead of an optional string.

Tests:

  • Add parameterized tests covering PromotionalResources.promotional_media parsing for present, empty, null, and absent PromotionalMedia data.

@sourcery-ai
Copy link

sourcery-ai bot commented Feb 11, 2026

Reviewer's Guide

Refactors the AWS PromotionalResources model so PromotionalMedia is represented as a typed list of PromotionalMedia items instead of a single string, and adds tests to cover various payload shapes and absence cases.

Class diagram for updated AWS PromotionalResources and new PromotionalMedia model

classDiagram
    class BaseResources

    class AttrsJSONDecodeMixin

    class PromotionalMedia {
        +str title
        +Optional~str~ description
        +from_json(data)
    }

    class PromotionalResources {
        +Optional~List~promotional_media~
    }

    PromotionalMedia --|> BaseResources
    PromotionalResources ..|> AttrsJSONDecodeMixin
    PromotionalResources "0..1" --> "*" PromotionalMedia : promotional_media
Loading

File-Level Changes

Change Details Files
Model PromotionalResources.promotional_media as an optional list of typed PromotionalMedia resources instead of an optional string.
  • Introduce a PromotionalMedia model with title and optional description fields mapped from AWS JSON aliases.
  • Change PromotionalResources.promotional_media to Optional[List[PromotionalMedia]] with a converter that builds PromotionalMedia instances from JSON arrays or returns None when the value is falsy.
  • Set on_setattr=NO_OP for promotional_media and keep JSON alias/metadata consistent with existing models.
cloudpub/models/aws.py
Add tests to validate PromotionalResources.promotional_media parsing across null, empty, present, and absent scenarios.
  • Import PromotionalMedia and PromotionalResources into the AWS model test module.
  • Add a parametrized test that verifies JSON payloads with null, empty list, single-item, and multi-item PromotionalMedia arrays are parsed into the expected PromotionalResources.promotional_media values.
  • Add a test asserting that when PromotionalMedia is absent from the JSON payload, PromotionalResources.promotional_media is None.
tests/aws/test_models.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@JAVGan
Copy link
Collaborator Author

JAVGan commented Feb 11, 2026

@lslebodn @ashwgit PTAL

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 3 issues, and left some high level feedback:

  • The promotional_media converter assumes every element is raw JSON and will fail if a list of PromotionalMedia objects is passed directly; consider making the converter a small helper that is a no-op for existing PromotionalMedia instances and only calls from_json for dicts.
  • The inline lambda used as the promotional_media converter is getting a bit dense; extracting it into a named function would make the conversion logic clearer and easier to extend (e.g., to handle unexpected input types more gracefully).
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `promotional_media` converter assumes every element is raw JSON and will fail if a list of `PromotionalMedia` objects is passed directly; consider making the converter a small helper that is a no-op for existing `PromotionalMedia` instances and only calls `from_json` for dicts.
- The inline lambda used as the `promotional_media` converter is getting a bit dense; extracting it into a named function would make the conversion logic clearer and easier to extend (e.g., to handle unexpected input types more gracefully).

## Individual Comments

### Comment 1
<location> `cloudpub/models/aws.py:658-659` </location>
<code_context>

-    promotional_media: Optional[str] = field(
-        validator=optional(instance_of(str)),
+    promotional_media: Optional[List[PromotionalMedia]] = field(
+        converter=lambda x: [PromotionalMedia.from_json(a) for a in x] if x else None,
+        on_setattr=NO_OP,
         metadata={"alias": "PromotionalMedia", "hide_unset": True},
</code_context>

<issue_to_address>
**issue:** The converter collapses an empty list into None, which may conflate two distinct states.

Because the converter uses `if x else None`, both `[]` and `None` become `None`, losing the distinction between "no PromotionalMedia provided" and "provided but empty". If that distinction is needed by callers or serialization, keep empty lists, e.g.:

`converter=lambda x: [PromotionalMedia.from_json(a) for a in x] if x is not None else None`
</issue_to_address>

### Comment 2
<location> `tests/aws/test_models.py:132-141` </location>
<code_context>
+@pytest.mark.parametrize(
</code_context>

<issue_to_address>
**suggestion:** Add a test case for an empty PromotionalMedia list to cover the falsy-but-non-None edge case.

The current parametrization only covers `None`, a single-element list, and a multi-element list. Because the converter uses `if x else None`, an empty list will also become `None`, which is a distinct case worth testing. Please add a `[]` case and assert the resulting `obj.promotional_media` matches the intended behavior for an empty list. You may also need to update `if not promotional_media:` to `if promotional_media is None` so that the empty-list case behaves correctly and is accurately captured by the new test.

Suggested implementation:

```python
@pytest.mark.parametrize(
    "promotional_media",
    [
        None,
        [],
        [
            {
                'Type': 'Image',
                'Url': 'https://foo.com/bar.png',
                'Title': 'Logo1',
                'Description': None,
            }

```

To fully implement the requested behavior and assertions, you will also need to:

1. Update the production code where `promotional_media` is converted (likely in the model or response parsing logic, not shown here):
   - Change any `if not promotional_media:` guard to `if promotional_media is None:` so that an empty list is treated as an empty collection instead of `None`.
   - Ensure that when `promotional_media` is `[]`, the resulting `obj.promotional_media` is an empty list (or the equivalent empty collection type used in your model), not `None`.

2. Update the corresponding parametrized test function in `tests/aws/test_models.py` (the body is not visible in the snippet):
   - For the `promotional_media is None` case, assert that `obj.promotional_media` is `None`.
   - For the new `promotional_media == []` case, assert that `obj.promotional_media` is an empty list (or the appropriate empty collection), e.g. `assert obj.promotional_media == []`.
   - For the non-empty list case(s), keep (or add) assertions that `obj.promotional_media` contains the expected `PromotionalMedia` items with the correct fields.

Because the test function body and the converter implementation are not shown in the provided snippet, you will need to adapt these changes to the exact function names and structures in your codebase.
</issue_to_address>

### Comment 3
<location> `tests/aws/test_models.py:160-169` </location>
<code_context>
+        ],
+    ],
+)
+def test_promotional_resources_promotional_media(
+    promotional_media, promotional_resource: Dict[str, Any]
+) -> None:
+    promotional_resource["PromotionalMedia"] = promotional_media
+
+    obj = PromotionalResources.from_json(promotional_resource)
+
+    assert obj
+    if not promotional_media:
+        assert obj.promotional_media is None
+    else:
+        assert obj.promotional_media == [PromotionalMedia.from_json(x) for x in promotional_media]
</code_context>

<issue_to_address>
**suggestion (testing):** Consider adding a test where the PromotionalMedia key is completely absent from the payload.

Right now the test always includes a `PromotionalMedia` key in `promotional_resource`, even when its value is `None`. Please add coverage (or a parametrized case) where the key is omitted entirely and confirm `PromotionalResources.from_json` treats that as expected (e.g., `promotional_media is None`), since AWS can distinguish between `null` and a missing field.

Suggested implementation:

```python
_PROMOTIONAL_MEDIA_MISSING = object()


@pytest.mark.parametrize(
    "promotional_media",

```

```python
            },
        ],
        _PROMOTIONAL_MEDIA_MISSING,
    ],
)
def test_promotional_resources_promotional_media(
    promotional_media, promotional_resource: Dict[str, Any]
) -> None:
    if promotional_media is not _PROMOTIONAL_MEDIA_MISSING:
        promotional_resource["PromotionalMedia"] = promotional_media

```

```python
    obj = PromotionalResources.from_json(promotional_resource)

    assert obj
    if promotional_media is _PROMOTIONAL_MEDIA_MISSING:
        # Key is completely absent from the payload
        assert obj.promotional_media is None
    elif not promotional_media:
        # Key is present but value is falsy (e.g. None or empty list)
        assert obj.promotional_media is None
    else:
        assert obj.promotional_media == [PromotionalMedia.from_json(x) for x in promotional_media]

```

These edits assume the existing parametrization is `@pytest.mark.parametrize("promotional_media", [...])` and that the shown list is the complete list of values for `promotional_media`. 
If the decorator signature differs (e.g. multiple parameters, different name), adjust the first SEARCH/REPLACE block accordingly so that `_PROMOTIONAL_MEDIA_MISSING` is defined near the tests and added as an additional case in the appropriate parametrized argument list.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@JAVGan
Copy link
Collaborator Author

JAVGan commented Feb 11, 2026

@sourcery-ai review

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • The promotional_media field currently lacks a validator, so arbitrary types could pass through; consider adding a validator=optional(deep_iterable(member_validator=instance_of(PromotionalMedia), iterable_validator=instance_of(list))) (or similar) to keep the list contents type-safe.
  • The converter for promotional_media always calls PromotionalMedia.from_json on each element when truthy, which will fail if callers pass already-instantiated PromotionalMedia objects; consider making the converter idempotent by detecting existing instances before converting.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `promotional_media` field currently lacks a validator, so arbitrary types could pass through; consider adding a `validator=optional(deep_iterable(member_validator=instance_of(PromotionalMedia), iterable_validator=instance_of(list)))` (or similar) to keep the list contents type-safe.
- The converter for `promotional_media` always calls `PromotionalMedia.from_json` on each element when truthy, which will fail if callers pass already-instantiated `PromotionalMedia` objects; consider making the converter idempotent by detecting existing instances before converting.

## Individual Comments

### Comment 1
<location> `tests/aws/test_models.py:134-138` </location>
<code_context>
+
+@pytest.mark.parametrize(
+    "promotional_media",
+    [
+        None,
+        [],
+        [
+            {
+                'Type': 'Image',
+                'Url': 'https://foo.com/bar.png',
</code_context>

<issue_to_address>
**suggestion (testing):** Consider adding a case where `Description` is completely absent, not just `None`.

Current cases cover `Description` as `None` and as a non-empty string, but not when the key is missing. Please add a parameterized input where the JSON omits the `"Description"` field and assert that deserialization still succeeds and behaves as intended, to fully verify the field’s optionality.

Suggested implementation:

```python
@pytest.mark.parametrize(
    "promotional_media",
    [
        None,
        [],
        [
            {
                'Type': 'Image',
                'Url': 'https://foo.com/bar.png',
                'Title': 'Logo1',
                'Description': None,
            }
        ],
        [
            {
                'Type': 'Image',
                'Url': 'https://foo.com/bar.png',
                'Title': 'Logo1',
                # Description field intentionally omitted to test absence
            }
        ],
        [
            {
                'Type': 'Image',
                'Url': 'https://foo.com/bar.png',
                'Title': 'Logo1',
                'Description': 'This is the logo1',

```

This change assumes the underlying deserialization and the test assertions already treat `Description` as optional (i.e., they do not assume presence of the attribute). If any assertion in the associated test function explicitly expects a `description` attribute/key to always exist, it should be relaxed to allow for the "absent" case (for example, by using `get` or checking for `hasattr`/`in` only when appropriate).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@JAVGan
Copy link
Collaborator Author

JAVGan commented Feb 11, 2026

@sourcery-ai review

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • The promotional_media converter assumes all incoming items are raw JSON dicts; consider making it idempotent (e.g., pass through existing PromotionalMedia instances) and extracting it into a named helper for clarity and reuse.
  • In PromotionalMedia, the description field is optional but has no default; if from_json ever calls the regular initializer directly, this may raise a missing-argument error, so it could be safer to set description: Optional[str] = field(default=None, ...) to make the optionality explicit at the model level.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `promotional_media` converter assumes all incoming items are raw JSON dicts; consider making it idempotent (e.g., pass through existing `PromotionalMedia` instances) and extracting it into a named helper for clarity and reuse.
- In `PromotionalMedia`, the `description` field is optional but has no default; if `from_json` ever calls the regular initializer directly, this may raise a missing-argument error, so it could be safer to set `description: Optional[str] = field(default=None, ...)` to make the optionality explicit at the model level.

## Individual Comments

### Comment 1
<location> `tests/aws/test_models.py:172` </location>
<code_context>
+def test_promotional_resources_promotional_media(
+    promotional_media, promotional_resource: Dict[str, Any]
+) -> None:
+    promotional_resource["PromotionalMedia"] = promotional_media
+
+    obj = PromotionalResources.from_json(promotional_resource)
</code_context>

<issue_to_address>
**suggestion (testing):** Consider copying the `promotional_resource` fixture before mutating it in tests

Because this test mutates the shared fixture dict in place, it relies on the fixture remaining function-scoped and on no other test mutating it. To avoid future cross-test coupling or flaky tests if the scope changes, create and mutate a deep copy instead, e.g. `resource = copy.deepcopy(promotional_resource)`.

Suggested implementation:

```python
def test_promotional_resources_promotional_media(
    promotional_media, promotional_resource: Dict[str, Any]
) -> None:
    resource = copy.deepcopy(promotional_resource)
    resource["PromotionalMedia"] = promotional_media

    obj = PromotionalResources.from_json(resource)

    assert obj
    if not promotional_media:
        assert obj.promotional_media is None
    else:
        assert obj.promotional_media == [PromotionalMedia.from_json(x) for x in promotional_media]


def test_promotional_resources_promotional_media_absent(promotional_resource: Dict[str, Any]):
    resource = copy.deepcopy(promotional_resource)
    del resource["PromotionalMedia"]

    obj = PromotionalResources.from_json(resource)

    assert obj
    assert obj.promotional_media is None

```

1. Ensure `import copy` is present at the top of `tests/aws/test_models.py`, alongside the other imports:
   - `import copy`
2. If there are existing tests that assert behavior when `"PromotionalMedia"` is absent, confirm that the new assertion `assert obj.promotional_media is None` is consistent with the model’s expected behavior. Adjust the assertion if the domain model uses a different sentinel (e.g. empty list) to represent absence.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

This commit fixes a bug in the `PromotionalResources` which did expected
the attribute `promotional_media` to be an optional string, while it
should be an optional list of resources.

It also introduces the class `PromotionalMedia` to map the missing
resource type.

Refers to SPSTRAT-671

Signed-off-by: Jonathan Gangi <jgangi@redhat.com>
@JAVGan
Copy link
Collaborator Author

JAVGan commented Feb 11, 2026

@sourcery-ai review

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • The promotional_media converter assumes every element is JSON-like and will break if a list of PromotionalMedia instances is passed (e.g., from_json or manual construction), so consider updating the converter to handle both dicts and preconstructed PromotionalMedia objects safely.
  • Using on_setattr=NO_OP on promotional_media prevents any reassignment of this field after initialization; if the intent is only to avoid double conversion, a more targeted approach (e.g., a converter that is idempotent or checks types) would preserve mutability while avoiding reconversion.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `promotional_media` converter assumes every element is JSON-like and will break if a list of `PromotionalMedia` instances is passed (e.g., `from_json` or manual construction), so consider updating the converter to handle both dicts and preconstructed `PromotionalMedia` objects safely.
- Using `on_setattr=NO_OP` on `promotional_media` prevents any reassignment of this field after initialization; if the intent is only to avoid double conversion, a more targeted approach (e.g., a converter that is idempotent or checks types) would preserve mutability while avoiding reconversion.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

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.

1 participant