Skip to content

Conversation

CalvoM
Copy link
Owner

@CalvoM CalvoM commented Jul 8, 2025

Summary by Sourcery

Integrate code coverage tooling and bolster utility function robustness with logging and error handling while adding end-to-end tests and CI coverage reporting.

New Features:

  • Integrate code coverage collection and reporting using coverage and pytest-cov

Enhancements:

  • Enhance average_rating to handle negative, incorrect, or missing inputs with logging
  • Add logging and skip logic in task finalization to avoid merging invalid session data

Build:

  • Add coverage and pytest-cov to project dependencies and dev dependencies

CI:

  • Run pytest with coverage flags and upload coverage reports to Codecov in GitHub Actions

Documentation:

  • Add Codecov badge to README

Tests:

  • Add comprehensive unit tests for utility functions (group_openings_with_eco, sort_openings, average_rating, merge_game_objects)

Copy link

sourcery-ai bot commented Jul 8, 2025

Reviewer's Guide

This PR integrates coverage tooling and CI reporting, enhances utility functions with logging and robust error handling, refines task merging logic, and adds comprehensive unit tests for utility functions.

Class diagram for updated utility functions in style_predictor.utils

classDiagram
    class group_openings_with_eco {
        +list[tuple[str, str, int]] data
        +dict[str, dict[str, Any]] return
    }
    class sort_openings {
        +dict[str, dict[str, dict[str, int | list[str]]]] openings
        +list return
    }
    class average_rating {
        +dict[str, tuple[float, int]] data
        +dict[str, float] return
        +LOG: logging.Logger
    }
Loading

File-Level Changes

Change Details Files
Integrate test coverage tooling and CI reporting
  • Add coverage and pytest-cov dependencies to requirements and pyproject.toml dev extras
  • Configure coverage settings (branch tracking, missing lines) in pyproject.toml
  • Update GitHub Actions to run pytest with coverage flags and upload reports to Codecov
  • Add Codecov badge to README
requirements.txt
pyproject.toml
.github/workflows/pr.yml
README.md
Enhance utility functions with logging and robust error handling
  • Import logging and define LOG in utils.py
  • Adjust sort_openings signature to include precise type annotations
  • Expand average_rating to handle non-2-element tuples, negative values, and additional exceptions with warning logs
style_predictor/utils.py
Refine data merging logic in finalize_analysis task
  • Add continue after logging warning to skip invalid session data before merging
style_predictor/tasks.py
Add comprehensive unit tests for utility functions
  • Create test_utils.py with parameterized tests for grouping, sorting, averaging, merging, and nested merging logic
style_predictor/tests/test_utils.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

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 @CalvoM - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments

### Comment 1
<location> `style_predictor/tests/test_utils.py:43` </location>
<code_context>
+            ),
+        ],
+    )
+    def test_group_openings_with_eco(self, opening_data, expected):
+        assert group_openings_with_eco(opening_data) == expected  # nosec
+
+    @pytest.mark.parametrize(
</code_context>

<issue_to_address>
Consider adding edge case tests for empty input and duplicate ECO codes.

Adding tests for these edge cases will help verify correct handling of empty input, duplicate ECO codes for the same opening, and identical ECO codes with different opening names.

Suggested implementation:

```python
        [
            # Normal case
            (
                (
                    ("E1", "Opening_1", 1),
                    ("E2", "Opening_2", 2),
                    ("E3", "Opening_3", 3),
                    ("E1", "Opening_1", 4),
                ),
                {

```

```python
                {

```

```python
            ),
            # Edge case: empty input
            (
                (),
                {},
            ),
            # Edge case: duplicate ECO codes for the same opening
            (
                (
                    ("E1", "Opening_1", 1),
                    ("E1", "Opening_1", 2),
                    ("E1", "Opening_1", 3),
                ),
                {
                    "Opening_1": {"total": 6, "eco_codes": ["E1"]},
                },
            ),
            # Edge case: identical ECO codes with different opening names
            (
                (
                    ("E1", "Opening_1", 1),
                    ("E1", "Opening_2", 2),
                ),
                {
                    "Opening_1": {"total": 1, "eco_codes": ["E1"]},
                    "Opening_2": {"total": 2, "eco_codes": ["E1"]},
                },
            ),

```
</issue_to_address>

### Comment 2
<location> `style_predictor/tests/test_utils.py:75` </location>
<code_context>
+            ),
+        ],
+    )
+    def test_sort_openings(self, openings, expected):
+        assert sort_openings(openings) == expected  # nosec
+
+    @pytest.mark.parametrize(
</code_context>

<issue_to_address>
Add tests for edge cases in sort_openings, such as ties and fewer than five openings.

Please add tests for fewer than five openings, ties in 'total', and empty input to ensure robustness.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
    def test_sort_openings(self, openings, expected):
        assert sort_openings(openings) == expected  # nosec
=======
    def test_sort_openings(self, openings, expected):
        assert sort_openings(openings) == expected  # nosec

    @pytest.mark.parametrize(
        "openings,expected",
        [
            # Fewer than five openings
            (
                {
                    "Opening_1": {"total": 2, "eco_codes": ["E1"]},
                    "Opening_2": {"total": 1, "eco_codes": ["E2"]},
                },
                [
                    ("Opening_1", {"total": 2, "eco_codes": ["E1"]}),
                    ("Opening_2", {"total": 1, "eco_codes": ["E2"]}),
                ],
            ),
            # Ties in 'total'
            (
                {
                    "Opening_1": {"total": 3, "eco_codes": ["E1"]},
                    "Opening_2": {"total": 3, "eco_codes": ["E2"]},
                    "Opening_3": {"total": 2, "eco_codes": ["E3"]},
                },
                [
                    ("Opening_1", {"total": 3, "eco_codes": ["E1"]}),
                    ("Opening_2", {"total": 3, "eco_codes": ["E2"]}),
                    ("Opening_3", {"total": 2, "eco_codes": ["E3"]}),
                ],
            ),
            # Empty input
            (
                {},
                [],
            ),
        ],
    )
    def test_sort_openings_edge_cases(self, openings, expected):
        assert sort_openings(openings) == expected  # nosec
>>>>>>> REPLACE

</suggested_fix>

### Comment 3
<location> `style_predictor/tests/test_utils.py:91` </location>
<code_context>
+            ),
+        ],
+    )
+    def test_average_rating(self, game_rating, expected):
+        assert average_rating(game_rating) == expected  # nosec
+
+    @pytest.mark.parametrize(
</code_context>

<issue_to_address>
Add tests for missing or malformed keys in average_rating input.

Please add tests for empty input dicts, non-tuple values, tuples with incorrect lengths, and negative values to ensure robust input handling.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
            (
                {"hyper": (10, 2), "bullet": (15, 2), "classical": (20, 2)},
                {"hyper": 5.0, "bullet": 7.5, "classical": 10.0},
            ),
            (
                {"hyper": (10, 0), "bullet": (0, 0), "classical": (20, 2)},
                {"hyper": 0, "bullet": 0, "classical": 10.0},
            ),
        ],
    )
=======
            (
                {"hyper": (10, 2), "bullet": (15, 2), "classical": (20, 2)},
                {"hyper": 5.0, "bullet": 7.5, "classical": 10.0},
            ),
            (
                {"hyper": (10, 0), "bullet": (0, 0), "classical": (20, 2)},
                {"hyper": 0, "bullet": 0, "classical": 10.0},
            ),
            # Empty input dict
            (
                {},
                {},
            ),
            # Non-tuple value
            (
                {"hyper": 5},
                {"hyper": 0},
            ),
            # Tuple with incorrect length (1 element)
            (
                {"hyper": (10,)},
                {"hyper": 0},
            ),
            # Tuple with incorrect length (3 elements)
            (
                {"hyper": (10, 2, 3)},
                {"hyper": 0},
            ),
            # Negative values
            (
                {"hyper": (-10, 2)},
                {"hyper": -5.0},
            ),
            (
                {"hyper": (10, -2)},
                {"hyper": 0},
            ),
            (
                {"hyper": (-10, -2)},
                {"hyper": 0},
            ),
        ],
    )
>>>>>>> REPLACE

</suggested_fix>

### Comment 4
<location> `style_predictor/tests/test_utils.py:115` </location>
<code_context>
+            ),
+        ],
+    )
+    def test_merge_game_objects(self, game_objects, expected):
+        res = {}
+        for obj in game_objects:
+            merge_game_objects(res, obj)
+        assert res == expected  # nosec
+
+    @pytest.mark.parametrize(
</code_context>

<issue_to_address>
Consider adding tests for merge_game_objects with missing or extra keys.

Please add tests for cases with missing keys, extra keys, and an empty input list to ensure robustness against varying input shapes.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
            (
                [
                    {"count": 1, "wins": 2, "losses": 3},
                    {"count": 1, "wins": 2, "losses": 3},
                    {"count": 1, "wins": 2, "losses": 3},
                ],
                {"count": 3, "wins": 6, "losses": 9},
            ),
        ],
    )
=======
            (
                [
                    {"count": 1, "wins": 2, "losses": 3},
                    {"count": 1, "wins": 2, "losses": 3},
                    {"count": 1, "wins": 2, "losses": 3},
                ],
                {"count": 3, "wins": 6, "losses": 9},
            ),
            # Test with missing keys in some objects
            (
                [
                    {"count": 1, "wins": 2},
                    {"count": 1, "losses": 3},
                    {"wins": 2, "losses": 3},
                ],
                {"count": 2, "wins": 4, "losses": 6},
            ),
            # Test with extra keys in some objects
            (
                [
                    {"count": 1, "wins": 2, "losses": 3, "draws": 1},
                    {"count": 1, "wins": 2, "losses": 3},
                    {"count": 1, "wins": 2, "losses": 3, "draws": 2},
                ],
                {"count": 3, "wins": 6, "losses": 9, "draws": 3},
            ),
            # Test with empty input list
            (
                [],
                {},
            ),
            # Test with all objects missing all keys (empty dicts)
            (
                [{}, {}, {}],
                {},
            ),
        ],
    )
>>>>>>> REPLACE

</suggested_fix>

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.

Comment on lines +75 to +128
def test_sort_openings(self, openings, expected):
assert sort_openings(openings) == expected # nosec
Copy link

Choose a reason for hiding this comment

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

suggestion (testing): Add tests for edge cases in sort_openings, such as ties and fewer than five openings.

Please add tests for fewer than five openings, ties in 'total', and empty input to ensure robustness.

Suggested change
def test_sort_openings(self, openings, expected):
assert sort_openings(openings) == expected # nosec
def test_sort_openings(self, openings, expected):
assert sort_openings(openings) == expected # nosec
@pytest.mark.parametrize(
"openings,expected",
[
# Fewer than five openings
(
{
"Opening_1": {"total": 2, "eco_codes": ["E1"]},
"Opening_2": {"total": 1, "eco_codes": ["E2"]},
},
[
("Opening_1", {"total": 2, "eco_codes": ["E1"]}),
("Opening_2", {"total": 1, "eco_codes": ["E2"]}),
],
),
# Ties in 'total'
(
{
"Opening_1": {"total": 3, "eco_codes": ["E1"]},
"Opening_2": {"total": 3, "eco_codes": ["E2"]},
"Opening_3": {"total": 2, "eco_codes": ["E3"]},
},
[
("Opening_1", {"total": 3, "eco_codes": ["E1"]}),
("Opening_2", {"total": 3, "eco_codes": ["E2"]}),
("Opening_3", {"total": 2, "eco_codes": ["E3"]}),
],
),
# Empty input
(
{},
[],
),
],
)
def test_sort_openings_edge_cases(self, openings, expected):
assert sort_openings(openings) == expected # nosec

Comment on lines 81 to 169
(
{"hyper": (10, 2), "bullet": (15, 2), "classical": (20, 2)},
{"hyper": 5.0, "bullet": 7.5, "classical": 10.0},
),
(
{"hyper": (10, 0), "bullet": (0, 0), "classical": (20, 2)},
{"hyper": 0, "bullet": 0, "classical": 10.0},
),
],
)
Copy link

Choose a reason for hiding this comment

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

suggestion (testing): Add tests for missing or malformed keys in average_rating input.

Please add tests for empty input dicts, non-tuple values, tuples with incorrect lengths, and negative values to ensure robust input handling.

Suggested change
(
{"hyper": (10, 2), "bullet": (15, 2), "classical": (20, 2)},
{"hyper": 5.0, "bullet": 7.5, "classical": 10.0},
),
(
{"hyper": (10, 0), "bullet": (0, 0), "classical": (20, 2)},
{"hyper": 0, "bullet": 0, "classical": 10.0},
),
],
)
(
{"hyper": (10, 2), "bullet": (15, 2), "classical": (20, 2)},
{"hyper": 5.0, "bullet": 7.5, "classical": 10.0},
),
(
{"hyper": (10, 0), "bullet": (0, 0), "classical": (20, 2)},
{"hyper": 0, "bullet": 0, "classical": 10.0},
),
# Empty input dict
(
{},
{},
),
# Non-tuple value
(
{"hyper": 5},
{"hyper": 0},
),
# Tuple with incorrect length (1 element)
(
{"hyper": (10,)},
{"hyper": 0},
),
# Tuple with incorrect length (3 elements)
(
{"hyper": (10, 2, 3)},
{"hyper": 0},
),
# Negative values
(
{"hyper": (-10, 2)},
{"hyper": -5.0},
),
(
{"hyper": (10, -2)},
{"hyper": 0},
),
(
{"hyper": (-10, -2)},
{"hyper": 0},
),
],
)

Comment on lines 105 to 207
(
[
{"count": 1, "wins": 2, "losses": 3},
{"count": 1, "wins": 2, "losses": 3},
{"count": 1, "wins": 2, "losses": 3},
],
{"count": 3, "wins": 6, "losses": 9},
),
],
)
Copy link

Choose a reason for hiding this comment

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

suggestion (testing): Consider adding tests for merge_game_objects with missing or extra keys.

Please add tests for cases with missing keys, extra keys, and an empty input list to ensure robustness against varying input shapes.

Suggested change
(
[
{"count": 1, "wins": 2, "losses": 3},
{"count": 1, "wins": 2, "losses": 3},
{"count": 1, "wins": 2, "losses": 3},
],
{"count": 3, "wins": 6, "losses": 9},
),
],
)
(
[
{"count": 1, "wins": 2, "losses": 3},
{"count": 1, "wins": 2, "losses": 3},
{"count": 1, "wins": 2, "losses": 3},
],
{"count": 3, "wins": 6, "losses": 9},
),
# Test with missing keys in some objects
(
[
{"count": 1, "wins": 2},
{"count": 1, "losses": 3},
{"wins": 2, "losses": 3},
],
{"count": 2, "wins": 4, "losses": 6},
),
# Test with extra keys in some objects
(
[
{"count": 1, "wins": 2, "losses": 3, "draws": 1},
{"count": 1, "wins": 2, "losses": 3},
{"count": 1, "wins": 2, "losses": 3, "draws": 2},
],
{"count": 3, "wins": 6, "losses": 9, "draws": 3},
),
# Test with empty input list
(
[],
{},
),
# Test with all objects missing all keys (empty dicts)
(
[{}, {}, {}],
{},
),
],
)

Comment on lines 117 to 206
for obj in game_objects:
merge_game_objects(res, obj)
Copy link

Choose a reason for hiding this comment

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

issue (code-quality): Avoid loops in tests. (no-loop-in-tests)

ExplanationAvoid complex code, like loops, in test functions.

Google's software engineering guidelines says:
"Clear tests are trivially correct upon inspection"
To reach that avoid complex code in tests:

  • loops
  • conditionals

Some ways to fix this:

  • Use parametrized tests to get rid of the loop.
  • Move the complex logic into helpers.
  • Move the complex part into pytest fixtures.

Complexity is most often introduced in the form of logic. Logic is defined via the imperative parts of programming languages such as operators, loops, and conditionals. When a piece of code contains logic, you need to do a bit of mental computation to determine its result instead of just reading it off of the screen. It doesn't take much logic to make a test more difficult to reason about.

Software Engineering at Google / Don't Put Logic in Tests

Comment on lines 268 to 357
for obj in game_objects:
res = merge_game_objects(res, obj)
Copy link

Choose a reason for hiding this comment

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

issue (code-quality): Avoid loops in tests. (no-loop-in-tests)

ExplanationAvoid complex code, like loops, in test functions.

Google's software engineering guidelines says:
"Clear tests are trivially correct upon inspection"
To reach that avoid complex code in tests:

  • loops
  • conditionals

Some ways to fix this:

  • Use parametrized tests to get rid of the loop.
  • Move the complex logic into helpers.
  • Move the complex part into pytest fixtures.

Complexity is most often introduced in the form of logic. Logic is defined via the imperative parts of programming languages such as operators, loops, and conditionals. When a piece of code contains logic, you need to do a bit of mental computation to determine its result instead of just reading it off of the screen. It doesn't take much logic to make a test more difficult to reason about.

Software Engineering at Google / Don't Put Logic in Tests

@codecov-commenter
Copy link

Welcome to Codecov 🎉

Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.

Thanks for integrating Codecov - We've got you covered ☂️

@CalvoM CalvoM force-pushed the feat/docs_and_tests branch 3 times, most recently from 2bcff2f to 9935fce Compare July 8, 2025 22:46
@CalvoM
Copy link
Owner Author

CalvoM commented Jul 8, 2025

@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 @CalvoM - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments

### Comment 1
<location> `style_predictor/utils.py:28` </location>
<code_context>


-def sort_openings(openings):
+def sort_openings(openings: dict[str, dict[str, dict[str, int | list[str]]]]):
     return sorted(openings.items(), key=lambda item: item[1]["total"], reverse=True)[:5]

</code_context>

<issue_to_address>
The type annotation for 'openings' in sort_openings is overly complex and may not match actual usage.

Consider simplifying the type annotation or using a TypedDict if only specific keys or value types are needed, to improve clarity and maintainability.

Suggested implementation:

```python
from typing import TypedDict

LOG = logging.getLogger(__name__)


```

```python
class OpeningInfo(TypedDict):
    total: int
    # Add other fields as needed, e.g.:
    # moves: list[str]

def sort_openings(openings: dict[str, OpeningInfo]):
    return sorted(openings.items(), key=lambda item: item[1]["total"], reverse=True)[:5]

```
</issue_to_address>

### Comment 2
<location> `style_predictor/tests/test_utils.py:203` </location>
<code_context>
+            ),
+        ],
+    )
+    def test_merge_game_objects(self, game_objects, expected):
+        res = {}
+        for obj in game_objects:
+            merge_game_objects(res, obj)
+        assert res == expected  # nosec
+
+    @pytest.mark.parametrize(
</code_context>

<issue_to_address>
Add test for merge_game_objects with conflicting key types.

Please add a test case where the same key has different types across objects to ensure merge_game_objects handles or reports type conflicts correctly.

Suggested implementation:

```python
            # Test with all objects missing all keys (empty dicts)
            (
                [{}, {}, {}],
                {},
            ),
            # Test with conflicting key types
            (
                [{"a": 1}, {"a": [2, 3]}],
                pytest.raises(TypeError),
            ),
        ],
    )

```

```python
        res = {}
        if isinstance(expected, type) and issubclass(expected, Exception):
            with expected:
                for obj in game_objects:
                    merge_game_objects(res, obj)
        else:
            for obj in game_objects:
                merge_game_objects(res, obj)
            assert res == expected  # nosec

```
</issue_to_address>

### Comment 3
<location> `style_predictor/tests/test_utils.py:109` </location>
<code_context>
+            ),
+        ],
+    )
+    def test_sort_openings(self, openings, expected):
+        assert sort_openings(openings) == expected  # nosec
+
+    @pytest.mark.parametrize(
</code_context>

<issue_to_address>
Consider testing sort_openings with more than five openings.

Adding such a test will verify that the function correctly limits the output to five and maintains the correct sort order.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
                    ("Opening_1", {"total": 3, "eco_codes": ["E1"]}),
                    ("Opening_2", {"total": 3, "eco_codes": ["E2"]}),
                    ("Opening_3", {"total": 2, "eco_codes": ["E3"]}),
                ],
            ),
        ],
    )
=======
                    ("Opening_1", {"total": 3, "eco_codes": ["E1"]}),
                    ("Opening_2", {"total": 3, "eco_codes": ["E2"]}),
                    ("Opening_3", {"total": 2, "eco_codes": ["E3"]}),
                ],
            ),
            # New test: more than five openings, should return only top five sorted
            (
                {
                    "Opening_A": {"total": 10, "eco_codes": ["A"]},
                    "Opening_B": {"total": 8, "eco_codes": ["B"]},
                    "Opening_C": {"total": 15, "eco_codes": ["C"]},
                    "Opening_D": {"total": 5, "eco_codes": ["D"]},
                    "Opening_E": {"total": 12, "eco_codes": ["E"]},
                    "Opening_F": {"total": 7, "eco_codes": ["F"]},
                    "Opening_G": {"total": 9, "eco_codes": ["G"]},
                },
                [
                    ("Opening_C", {"total": 15, "eco_codes": ["C"]}),
                    ("Opening_E", {"total": 12, "eco_codes": ["E"]}),
                    ("Opening_A", {"total": 10, "eco_codes": ["A"]}),
                    ("Opening_G", {"total": 9, "eco_codes": ["G"]}),
                    ("Opening_B", {"total": 8, "eco_codes": ["B"]}),
                ],
            ),
        ],
    )
>>>>>>> REPLACE

</suggested_fix>

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.



def sort_openings(openings):
def sort_openings(openings: dict[str, dict[str, dict[str, int | list[str]]]]):
Copy link

Choose a reason for hiding this comment

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

suggestion: The type annotation for 'openings' in sort_openings is overly complex and may not match actual usage.

Consider simplifying the type annotation or using a TypedDict if only specific keys or value types are needed, to improve clarity and maintainability.

Suggested implementation:

from typing import TypedDict

LOG = logging.getLogger(__name__)
class OpeningInfo(TypedDict):
    total: int
    # Add other fields as needed, e.g.:
    # moves: list[str]

def sort_openings(openings: dict[str, OpeningInfo]):
    return sorted(openings.items(), key=lambda item: item[1]["total"], reverse=True)[:5]

Comment on lines 203 to 211
def test_merge_game_objects(self, game_objects, expected):
res = {}
for obj in game_objects:
merge_game_objects(res, obj)
assert res == expected # nosec
Copy link

Choose a reason for hiding this comment

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

suggestion (testing): Add test for merge_game_objects with conflicting key types.

Please add a test case where the same key has different types across objects to ensure merge_game_objects handles or reports type conflicts correctly.

Suggested implementation:

            # Test with all objects missing all keys (empty dicts)
            (
                [{}, {}, {}],
                {},
            ),
            # Test with conflicting key types
            (
                [{"a": 1}, {"a": [2, 3]}],
                pytest.raises(TypeError),
            ),
        ],
    )
        res = {}
        if isinstance(expected, type) and issubclass(expected, Exception):
            with expected:
                for obj in game_objects:
                    merge_game_objects(res, obj)
        else:
            for obj in game_objects:
                merge_game_objects(res, obj)
            assert res == expected  # nosec

Comment on lines 102 to 126
("Opening_1", {"total": 3, "eco_codes": ["E1"]}),
("Opening_2", {"total": 3, "eco_codes": ["E2"]}),
("Opening_3", {"total": 2, "eco_codes": ["E3"]}),
],
),
],
)
Copy link

Choose a reason for hiding this comment

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

suggestion (testing): Consider testing sort_openings with more than five openings.

Adding such a test will verify that the function correctly limits the output to five and maintains the correct sort order.

Suggested change
("Opening_1", {"total": 3, "eco_codes": ["E1"]}),
("Opening_2", {"total": 3, "eco_codes": ["E2"]}),
("Opening_3", {"total": 2, "eco_codes": ["E3"]}),
],
),
],
)
("Opening_1", {"total": 3, "eco_codes": ["E1"]}),
("Opening_2", {"total": 3, "eco_codes": ["E2"]}),
("Opening_3", {"total": 2, "eco_codes": ["E3"]}),
],
),
# New test: more than five openings, should return only top five sorted
(
{
"Opening_A": {"total": 10, "eco_codes": ["A"]},
"Opening_B": {"total": 8, "eco_codes": ["B"]},
"Opening_C": {"total": 15, "eco_codes": ["C"]},
"Opening_D": {"total": 5, "eco_codes": ["D"]},
"Opening_E": {"total": 12, "eco_codes": ["E"]},
"Opening_F": {"total": 7, "eco_codes": ["F"]},
"Opening_G": {"total": 9, "eco_codes": ["G"]},
},
[
("Opening_C", {"total": 15, "eco_codes": ["C"]}),
("Opening_E", {"total": 12, "eco_codes": ["E"]}),
("Opening_A", {"total": 10, "eco_codes": ["A"]}),
("Opening_G", {"total": 9, "eco_codes": ["G"]}),
("Opening_B", {"total": 8, "eco_codes": ["B"]}),
],
),
],
)

Comment on lines 205 to 206
for obj in game_objects:
merge_game_objects(res, obj)
Copy link

Choose a reason for hiding this comment

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

issue (code-quality): Avoid loops in tests. (no-loop-in-tests)

ExplanationAvoid complex code, like loops, in test functions.

Google's software engineering guidelines says:
"Clear tests are trivially correct upon inspection"
To reach that avoid complex code in tests:

  • loops
  • conditionals

Some ways to fix this:

  • Use parametrized tests to get rid of the loop.
  • Move the complex logic into helpers.
  • Move the complex part into pytest fixtures.

Complexity is most often introduced in the form of logic. Logic is defined via the imperative parts of programming languages such as operators, loops, and conditionals. When a piece of code contains logic, you need to do a bit of mental computation to determine its result instead of just reading it off of the screen. It doesn't take much logic to make a test more difficult to reason about.

Software Engineering at Google / Don't Put Logic in Tests

@CalvoM CalvoM force-pushed the feat/docs_and_tests branch from 9935fce to 6709532 Compare July 8, 2025 23:03
@CalvoM CalvoM force-pushed the feat/docs_and_tests branch from 6709532 to 9c40865 Compare July 8, 2025 23:09
@CalvoM CalvoM merged commit a65882e into main Jul 8, 2025
2 checks passed
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