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 1 commit 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

Enhance the InterfaceProtocolCheckMixin to ensure that methods are correctly implemented in both parent and subclass, raising errors if methods are missing. Add comprehensive tests to validate the mixin's behavior in various scenarios.

Enhancements:

  • Enhance InterfaceProtocolCheckMixin to raise errors if methods are missing in either the parent or subclass.

Tests:

  • Add tests for InterfaceProtocolCheckMixin to verify correct implementation, missing methods, and extra methods scenarios.

Copy link
Contributor

sourcery-ai bot commented Nov 23, 2024

Reviewer's Guide by Sourcery

This PR enhances the InterfaceProtocolCheckMixin by adding stricter method validation between parent and subclasses. The implementation now checks for missing methods in both parent and subclass, and ensures proper method overriding. The changes are accompanied by comprehensive tests that verify the new validation behavior.

Updated class diagram for InterfaceProtocolCheckMixin

classDiagram
    class InterfaceProtocolCheckMixin {
        +__init_subclass__(cls, **kwargs)
    }
    class ParentInterface {
        +method_one(param: int) str
        +method_two(value: str) int
    }
    class CorrectImplementation {
        +method_one(param: int) str
        +method_two(value: str) int
    }
    class MissingMethodImplementation {
        +method_one(param: int) str
    }
    class ExtendedImplementation {
        +method_one(param: int) str
        +method_two(value: str) int
        +method_three(extra: float) float
    }
    ParentInterface <|-- CorrectImplementation
    ParentInterface <|-- MissingMethodImplementation
    ParentInterface <|-- ExtendedImplementation
    InterfaceProtocolCheckMixin <|-- CorrectImplementation
    InterfaceProtocolCheckMixin <|-- MissingMethodImplementation
    InterfaceProtocolCheckMixin <|-- ExtendedImplementation
    note for InterfaceProtocolCheckMixin "Checks for missing methods in parent and subclass"
Loading

File-Level Changes

Change Details Files
Enhanced method validation in the InterfaceProtocolCheckMixin
  • Changed method detection from inspect.ismethod to inspect.isfunction for better method discovery
  • Added validation to check if methods exist in parent class
  • Added validation to ensure subclass properly overrides parent methods
  • Improved variable naming for better readability (cls_method -> parent_cls_method)
  • Updated error messages to be more descriptive
xcov19/utils/mixins.py
Added comprehensive test suite for the InterfaceProtocolCheckMixin
  • Created test for correct implementation of interface methods
  • Added test for missing method implementation in subclass
  • Added test for extra methods in subclass not present in parent
  • Implemented test fixtures with sample parent interface and various implementation classes
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.

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