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
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions xcov19/tests/test_mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import pytest

from xcov19.utils.mixins import InterfaceProtocolCheckMixin


# A base interface class with defined methods
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")
Comment on lines +7 to +14
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}")



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): 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


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


def test_missing_methods():
with pytest.raises(
NotImplementedError,
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.

def method_one(self, param: int) -> str:
return str(param)


def test_extra_methods():
with pytest.raises(
NotImplementedError,
match="Method method_three must be implemented in class 'ParentInterface'",
):
# A subclass with an extra method
class ExtendedImplementation(ParentInterface, InterfaceProtocolCheckMixin):
def method_one(self, param: int) -> str:
return str(param)

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

def method_three(self, extra: float) -> float:
return extra * 2
33 changes: 24 additions & 9 deletions xcov19/utils/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

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.

if not method_name.startswith("__")
):
# TODO: Raise if either classes don't have the method declared.
cls_method = getattr(parent_class, defined_method)
# 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.

):
raise NotImplementedError(
f"Method {defined_method} must be implemented in class '{parent_class.__name__}'"
)

# No need to check if subclass method is defined since we are iterating over the subclass methods
subclass_method = getattr(cls, defined_method)
cls_method_params: dict = get_type_hints(cls_method)

# Parent class implements the method, but subclass does not
if parent_cls_method is subclass_method:
raise NotImplementedError(
f"Subclass '{cls.__name__}' must override the method '{defined_method}' from the parent class '{parent_class.__name__}'."
)

parent_cls_method_params: dict = get_type_hints(parent_cls_method)
subclass_method_params: dict = get_type_hints(subclass_method)
if len(cls_method_params) != len(subclass_method_params):
if len(parent_cls_method_params) != len(subclass_method_params):
raise NotImplementedError(f"""Method parameters mismatch:
Expected: {cls_method_params.keys()}
Expected: {parent_cls_method_params.keys()}
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.

parent_cls_method_params.items(), subclass_method_params.items()
):
match_signature(cls_signature, subclass_signature)
match_signature(parent_cls_signature, subclass_signature)
super().__init_subclass__(**kwargs)