Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Raise error if method is missing in either parent or subclasse; add tests for mixin #95

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

ugly-custard
Copy link

@ugly-custard ugly-custard commented Nov 23, 2024

Related Issue

#33

Changes Done

  • Updated the InterfaceProtocolCheckMixin to check for missing methods

  • Wrote tests for it in tests/test_mixins.py

Summary by Sourcery

Tests:

  • Added tests for the InterfaceProtocolCheckMixin.

Copy link
Contributor

sourcery-ai bot commented Nov 23, 2024

Reviewer's Guide by Sourcery

The pull request enhances the InterfaceProtocolCheckMixin to ensure that methods are correctly implemented in both parent and subclass, raising errors if methods are missing. Additionally, it adds comprehensive tests to validate the mixin's behavior in various scenarios.

Sequence diagram for InterfaceProtocolCheckMixin method validation

sequenceDiagram
    participant S as Subclass
    participant M as InterfaceProtocolCheckMixin
    participant P as ParentClass

    S->>M: __init_subclass__()
    activate M
    M->>M: Get defined methods
    M->>P: Check if method exists
    alt Method missing in parent
        M-->>S: Raise NotImplementedError
    else Method exists
        M->>M: Compare method signatures
        alt Signatures don't match
            M-->>S: Raise NotImplementedError
        else Signatures match
            M-->>S: Continue initialization
        end
    end
    deactivate M
Loading

Class diagram for InterfaceProtocolCheckMixin implementation

classDiagram
    class InterfaceProtocolCheckMixin {
        +__init_subclass__()
    }

    class ParentClass {
        +method()
    }

    class SubClass {
        +method()
    }

    ParentClass --|> InterfaceProtocolCheckMixin
    SubClass --|> ParentClass

    note for InterfaceProtocolCheckMixin "Validates method implementations
and signatures between
parent and subclass"
Loading

Flow diagram for method validation process

graph TD
    A[Start Validation] --> B{Method in Parent?}
    B -->|No| C[Raise Error:
Missing in Parent]
    B -->|Yes| D{Method in Subclass?}
    D -->|No| E[Raise Error:
Missing Override]
    D -->|Yes| F{Check Signatures}
    F -->|Match| G[Continue]
    F -->|Mismatch| H[Raise Error:
Signature Mismatch]
Loading

File-Level Changes

Change Details Files
The InterfaceProtocolCheckMixin was updated to check for missing methods in both the parent and subclass.
  • The mixin now verifies if a method defined in the subclass is also present in the parent class.
  • It raises a NotImplementedError if a method is missing in the parent class.
  • It raises a NotImplementedError if a subclass does not override a method from the parent class.
  • The mixin now checks for parameter mismatches between parent and subclass methods.
xcov19/utils/mixins.py
Added tests for the InterfaceProtocolCheckMixin.
  • Added a test case for a correct subclass implementation.
  • Added a test case for a subclass missing a required method.
  • Added a test case for a subclass with an extra method.
xcov19/tests/test_mixins.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.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a 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. You can also use
    this command to specify where the summary should be inserted.

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
Contributor

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

Here's what I looked at during the review
  • 🟡 General issues: 2 issues found
  • 🟢 Security: all looks good
  • 🟡 Review instructions: 1 issue found
  • 🟡 Testing: 2 issues found
  • 🟡 Complexity: 1 issue found
  • 🟢 Documentation: all looks good

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.

Got: {subclass_method_params.keys()}
""")
for cls_signature, subclass_signature in zip(
cls_method_params.items(), subclass_method_params.items()
for parent_cls_signature, subclass_signature in zip(
Copy link
Contributor

Choose a reason for hiding this comment

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

issue: Parameter comparison using zip() assumes parameters are in the same order, which might not catch valid implementations with reordered parameters

Consider using a more flexible comparison that matches parameters by name rather than position to allow valid parameter reordering in subclass implementations.

@@ -49,21 +49,36 @@ def __init_subclass__(cls, **kwargs):
# raise Exception(inspect.getmembers(cls, predicate=inspect.isfunction))
for defined_method in (
method_name
for method_name, _ in inspect.getmembers(cls, predicate=inspect.ismethod)
for method_name, _ in inspect.getmembers(cls, predicate=inspect.isfunction)
Copy link
Contributor

Choose a reason for hiding this comment

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

issue: Using isfunction might not properly handle all method types like static and class methods

Consider adding specific handling for @staticmethod and @classmethod decorators to ensure proper method detection and comparison.

# A correct subclass implementation
class CorrectImplementation(ParentInterface, InterfaceProtocolCheckMixin):
def method_one(self, param: int) -> str:
return str(param)
Copy link
Contributor

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 missing first method

The test only checks for missing second method. Consider adding another test case where method_one is missing to ensure both scenarios are covered.

def test_missing_methods():
    with pytest.raises(
        NotImplementedError,
        match="Subclass 'MissingMethodImplementation' must override the method 'method_two' from the parent class 'ParentInterface'.",
    ):
        class MissingMethodImplementation(ParentInterface, InterfaceProtocolCheckMixin):
            def method_one(self, param: int) -> str:
                return str(param)

    with pytest.raises(
        NotImplementedError, 
        match="Subclass 'MissingFirstMethodImplementation' must override the method 'method_one' from the parent class 'ParentInterface'.",
    ):
        class MissingFirstMethodImplementation(ParentInterface, InterfaceProtocolCheckMixin):
            def method_two(self) -> int:
                return 42

Comment on lines +7 to +14
class ParentInterface:
def method_one(self, param: int) -> str:
"""Method one of Base Parent Interface Class"""
raise NotImplementedError("Method one not implemented")

def method_two(self, value: str) -> int:
"""Method two Base Parent Interface Class"""
raise NotImplementedError("Method two not implemented")
Copy link
Contributor

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 method signature mismatch

The changes in mixins.py include parameter type checking, but there are no tests for mismatched parameter types or return types. Consider adding tests where the subclass implements methods with incorrect signatures.

class ParentInterface:
    def method_one(self, param: int) -> str:
        raise NotImplementedError("Method one not implemented")

    def method_two(self, value: str) -> int:
        raise NotImplementedError("Method two not implemented")

    def validate_signature(self, method_name: str, param_type: type, return_type: type) -> None:
        method = getattr(self, method_name)
        if method.__annotations__ != {param_type.__name__: param_type, 'return': return_type}:
            raise TypeError(f"Invalid signature for {method_name}")

@@ -49,21 +49,36 @@ def __init_subclass__(cls, **kwargs):
# raise Exception(inspect.getmembers(cls, predicate=inspect.isfunction))
for defined_method in (
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (complexity): Consider extracting validation logic into separate methods to improve code organization

The validation logic can be simplified by extracting the checks into separate methods while maintaining all functionality. Here's a suggested refactor:

def __init_subclass__(cls, **kwargs):
    parent_class = inspect.getmro(cls)[1]

    for method_name, _ in inspect.getmembers(cls, predicate=inspect.isfunction):
        if method_name.startswith("__"):
            continue

        cls._validate_method_exists(method_name, parent_class)
        cls._validate_method_override(method_name, parent_class)
        cls._validate_signatures(method_name, parent_class)

    super().__init_subclass__(**kwargs)

@classmethod
def _validate_method_exists(cls, method_name: str, parent_class: type) -> None:
    if not hasattr(parent_class, method_name):
        raise NotImplementedError(
            f"Method {method_name} must be implemented in class '{parent_class.__name__}'"
        )

@classmethod
def _validate_method_override(cls, method_name: str, parent_class: type) -> None:
    if getattr(parent_class, method_name) is getattr(cls, method_name):
        raise NotImplementedError(
            f"Subclass '{cls.__name__}' must override the method '{method_name}' from the parent class '{parent_class.__name__}'."
        )

@classmethod
def _validate_signatures(cls, method_name: str, parent_class: type) -> None:
    parent_params = get_type_hints(getattr(parent_class, method_name))
    subclass_params = get_type_hints(getattr(cls, method_name))

    if len(parent_params) != len(subclass_params):
        raise NotImplementedError(f"Method parameters mismatch:\nExpected: {parent_params.keys()}\nGot: {subclass_params.keys()}")

    for parent_sig, subclass_sig in zip(parent_params.items(), subclass_params.items()):
        match_signature(parent_sig, subclass_sig)

This refactoring:

  1. Separates validation into clear, single-purpose methods
  2. Removes nested conditions and walrus operators
  3. Makes the validation flow linear and easy to follow
  4. Maintains all validation checks and error messages

# Check if method is defined in parent class
if not (
hasattr(parent_class, defined_method)
and (parent_cls_method := getattr(parent_class, defined_method))
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (review_instructions): Assignment expressions (:=) should be used sparingly according to Google style guide

While the walrus operator is allowed, Google's style guide recommends using it sparingly and only when it makes the code significantly more readable. Consider splitting this into two separate lines for better clarity.

Review instructions:

Path patterns: **/*.py, **/*.js, **/*.ex

Instructions:
Check code for Google style guide for python code.

Copy link
Contributor

@codecakes codecakes left a comment

Choose a reason for hiding this comment

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

there are a few review comments already. I'd suggest those are looked at first.

match="Subclass 'MissingMethodImplementation' must override the method 'method_two' from the parent class 'ParentInterface'.",
):
# A subclass missing a required method
class MissingMethodImplementation(ParentInterface, InterfaceProtocolCheckMixin):
Copy link
Contributor

Choose a reason for hiding this comment

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

you can move all components needed before test run before the actual tests. this will be your setup and help in the future for easier reading.

@codecakes
Copy link
Contributor

@sourcery-ai review

Copy link
Contributor

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

Here's what I looked at during the review
  • 🟢 General issues: all looks good
  • 🟢 Security: all looks good
  • 🟢 Review instructions: all looks good
  • 🟡 Testing: 1 issue found
  • 🟢 Complexity: all looks good
  • 🟢 Documentation: all looks good

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 +12 to +21
def method_two(self, value: str) -> int:
"""Method two Base Parent Interface Class"""
raise NotImplementedError("Method two not implemented")


def test_correct_implementation():
# A correct subclass implementation
class CorrectImplementation(ParentInterface, InterfaceProtocolCheckMixin):
def method_one(self, param: int) -> str:
return str(param)
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Suggest adding a test case for incorrect parameter types.

It's important to verify that the mixin correctly raises an error when the subclass's method signature doesn't match the parent's, including cases with incorrect parameter types or return types.

Suggested implementation:

def test_correct_implementation():
    # A correct subclass implementation
    class CorrectImplementation(ParentInterface, InterfaceProtocolCheckMixin):
        def method_one(self, param: int) -> str:
            return str(param)

        def method_two(self, value: str) -> int:
            return len(value)


def test_incorrect_parameter_types():
    # A subclass with incorrect parameter types
    class IncorrectParameterTypes(ParentInterface, InterfaceProtocolCheckMixin):
        def method_one(self, param: str) -> str:  # param should be int
            return param

        def method_two(self, value: int) -> int:  # value should be str
            return value

    # Should raise TypeError due to incompatible method signatures
    with pytest.raises(TypeError) as exc_info:
        IncorrectParameterTypes()

    assert "method_one" in str(exc_info.value)
    assert "incompatible type" in str(exc_info.value).lower()

You'll need to add the following import at the top of the file if not already present:

import pytest

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