diff --git a/overrides/enforce.py b/overrides/enforce.py index 824b5b6..619c2b8 100644 --- a/overrides/enforce.py +++ b/overrides/enforce.py @@ -12,32 +12,37 @@ def __new__(mcls, name, bases, namespace, **kwargs): cls = super().__new__(mcls, name, bases, namespace, **kwargs) for name, value in namespace.items(): - # Actually checking the direct parent should be enough, - # otherwise the error would have emerged during the parent class checking - if name.startswith("__"): - continue - value = mcls.handle_special_value(value) - is_override = getattr(value, "__override__", False) - for base in bases: - base_class_method = getattr(base, name, False) - if ( + mcls._check_if_overrides_final_method(name, bases) + if not name.startswith("__"): + value = mcls._handle_special_value(value) + mcls._check_if_overrides_without_overrides_decorator(name, value, bases) + return cls + + @staticmethod + def _check_if_overrides_without_overrides_decorator(name, value, bases): + is_override = getattr(value, "__override__", False) + for base in bases: + base_class_method = getattr(base, name, False) + if ( not base_class_method or not callable(base_class_method) or getattr(base_class_method, "__ignored__", False) - ): - continue - assert ( - is_override - ), "Method %s overrides but does not have @overrides decorator" % (name) - # `__finalized__` is added by `@final` decorator - assert not getattr(base_class_method, "__finalized__", False), ( - "Method %s is finalized in %s, it cannot be overridden" - % (base_class_method, base,) - ) - return cls + ): + continue + if not is_override: + raise TypeError(f"Method {name} overrides but does not have @overrides decorator") + @staticmethod + def _check_if_overrides_final_method(name, bases): + for base in bases: + base_class_method = getattr(base, name, False) + # `__finalized__` is added by `@final` decorator + if getattr(base_class_method, "__finalized__", False): + raise TypeError( + f"Method {base_class_method} is finalized in {base}, it cannot be overridden" + ) @staticmethod - def handle_special_value(value): + def _handle_special_value(value): if isinstance(value, classmethod) or isinstance(value, staticmethod): value = value.__get__(None, dict) elif isinstance(value, property): @@ -45,6 +50,7 @@ def handle_special_value(value): return value + class EnforceOverrides(metaclass=EnforceOverridesMeta): "Use this as the parent class for your custom classes" pass diff --git a/tests/test_enforce__py38.py b/tests/test_enforce__py38.py index 276d794..f15fe2b 100644 --- a/tests/test_enforce__py38.py +++ b/tests/test_enforce__py38.py @@ -12,6 +12,11 @@ class Enforcing(EnforceOverrides): def finality(self): return "final" + + @final + def __and__(self, other): + return True + def nonfinal1(self, param: int) -> str: return "super1" @@ -47,26 +52,23 @@ def nonfinal1(self, param: int) -> str: self.assertEqual(sc.classVariableIsOk, "OK!") def test_enforcing_when_finality_broken(self): - try: - + with self.assertRaises(TypeError): class BrokesFinality(Enforcing): def finality(self): return "NEVER HERE" - raise RuntimeError("Should not go here") - except AssertionError: - pass + def test_trying_to_override_final_magic_method(self): + with self.assertRaises(TypeError): + class FinalMagicOverrides(Enforcing): + def __and__(self, other): + return False def test_enforcing_when_none_explicit_override(self): - try: - + with self.assertRaises(TypeError): class Overrider(Enforcing): def nonfinal2(self): return "NEVER HERE EITHER" - raise RuntimeError("Should not go here") - except AssertionError: - pass def test_enforcing_when_property_overriden(self): class PropertyOverrider(Enforcing): @@ -116,7 +118,7 @@ class MetaClassMethodOverrider(Enforcing): def register(self): pass - with self.assertRaises(AssertionError): + with self.assertRaises(TypeError): class SubClass(MetaClassMethodOverrider): def register(self):