-
Notifications
You must be signed in to change notification settings - Fork 12
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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") | ||
|
||
|
||
def test_correct_implementation(): | ||
# A correct subclass implementation | ||
class CorrectImplementation(ParentInterface, InterfaceProtocolCheckMixin): | ||
def method_one(self, param: int) -> str: | ||
return str(param) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -49,21 +49,36 @@ def __init_subclass__(cls, **kwargs): | |
# raise Exception(inspect.getmembers(cls, predicate=inspect.isfunction)) | ||
for defined_method in ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
|
||
method_name | ||
for method_name, _ in inspect.getmembers(cls, predicate=inspect.ismethod) | ||
for method_name, _ in inspect.getmembers(cls, predicate=inspect.isfunction) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: Instructions: |
||
): | ||
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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) |
There was a problem hiding this comment.
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.