From ea030cd731fd66db1dc8a055b6a1445be8e1fa7a Mon Sep 17 00:00:00 2001 From: Mikko Korpela Date: Sun, 9 Oct 2022 18:21:58 +0300 Subject: [PATCH 1/3] final Ensured always, TypeError raised from metaclass --- overrides/enforce.py | 48 +++++++++++++++++++++---------------- tests/test_enforce__py38.py | 24 ++++++++++--------- 2 files changed, 40 insertions(+), 32 deletions(-) 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): From 6f6a8b389a7e1f041b45387398642180067e9a72 Mon Sep 17 00:00:00 2001 From: Mikko Korpela Date: Sun, 9 Oct 2022 18:27:54 +0300 Subject: [PATCH 2/3] black --- overrides/enforce.py | 16 +++++++++------- overrides/overrides.py | 4 +++- overrides/typing_utils.py | 8 ++++++-- setup.py | 4 +++- tests/test_enforce__py38.py | 5 +++-- 5 files changed, 24 insertions(+), 13 deletions(-) diff --git a/overrides/enforce.py b/overrides/enforce.py index 619c2b8..4db3c67 100644 --- a/overrides/enforce.py +++ b/overrides/enforce.py @@ -24,13 +24,16 @@ def _check_if_overrides_without_overrides_decorator(name, value, bases): 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) + not base_class_method + or not callable(base_class_method) + or getattr(base_class_method, "__ignored__", False) ): continue if not is_override: - raise TypeError(f"Method {name} overrides but does not have @overrides decorator") + 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: @@ -38,8 +41,8 @@ def _check_if_overrides_final_method(name, bases): # `__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" - ) + f"Method {name} is finalized in {base}, it cannot be overridden" + ) @staticmethod def _handle_special_value(value): @@ -50,7 +53,6 @@ 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/overrides/overrides.py b/overrides/overrides.py index 7cd862e..2dbd42d 100644 --- a/overrides/overrides.py +++ b/overrides/overrides.py @@ -95,7 +95,9 @@ def method(self): def _overrides( - method: _WrappedMethod, check_signature: bool, check_at_runtime: bool, + method: _WrappedMethod, + check_signature: bool, + check_at_runtime: bool, ) -> _WrappedMethod: setattr(method, "__override__", True) global_vars = getattr(method, "__globals__", None) diff --git a/overrides/typing_utils.py b/overrides/typing_utils.py index 2d9f919..9503124 100644 --- a/overrides/typing_utils.py +++ b/overrides/typing_utils.py @@ -291,7 +291,9 @@ def _is_origin_subtype(left: OriginType, right: OriginType) -> bool: NormalizedTypeArgs = typing.Union[ - typing.Tuple[typing.Any, ...], typing.FrozenSet[NormalizedType], NormalizedType, + typing.Tuple[typing.Any, ...], + typing.FrozenSet[NormalizedType], + NormalizedType, ] @@ -416,7 +418,9 @@ def _is_normal_subtype( def issubtype( - left: Type, right: Type, forward_refs: typing.Optional[dict] = None, + left: Type, + right: Type, + forward_refs: typing.Optional[dict] = None, ) -> typing.Optional[bool]: """Check that the left argument is a subtype of the right. For unions, check if the type arguments of the left is a subset of the right. diff --git a/setup.py b/setup.py index ac0e5c7..63bb5a0 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,9 @@ author_email=address, url="https://github.com/mkorpela/overrides", packages=find_packages(), - package_data={"overrides": ["*.pyi", "py.typed"],}, + package_data={ + "overrides": ["*.pyi", "py.typed"], + }, include_package_data=True, install_requires=['typing;python_version<"3.5"'], python_requires=">=3.6", diff --git a/tests/test_enforce__py38.py b/tests/test_enforce__py38.py index f15fe2b..78fb2c1 100644 --- a/tests/test_enforce__py38.py +++ b/tests/test_enforce__py38.py @@ -12,7 +12,6 @@ class Enforcing(EnforceOverrides): def finality(self): return "final" - @final def __and__(self, other): return True @@ -53,23 +52,25 @@ def nonfinal1(self, param: int) -> str: def test_enforcing_when_finality_broken(self): with self.assertRaises(TypeError): + class BrokesFinality(Enforcing): def finality(self): return "NEVER HERE" 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): with self.assertRaises(TypeError): + class Overrider(Enforcing): def nonfinal2(self): return "NEVER HERE EITHER" - def test_enforcing_when_property_overriden(self): class PropertyOverrider(Enforcing): @property From 316872f4f722e60a286ed27b71d09723e23b7ce9 Mon Sep 17 00:00:00 2001 From: Mikko Korpela Date: Sun, 9 Oct 2022 18:37:11 +0300 Subject: [PATCH 3/3] improve type error descriptions --- overrides/enforce.py | 2 +- overrides/overrides.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/overrides/enforce.py b/overrides/enforce.py index 4db3c67..56a91cd 100644 --- a/overrides/enforce.py +++ b/overrides/enforce.py @@ -31,7 +31,7 @@ def _check_if_overrides_without_overrides_decorator(name, value, bases): continue if not is_override: raise TypeError( - f"Method {name} overrides but does not have @overrides decorator" + f"Method {name} overrides method from {base} but does not have @overrides decorator" ) @staticmethod diff --git a/overrides/overrides.py b/overrides/overrides.py index 2dbd42d..49eb014 100644 --- a/overrides/overrides.py +++ b/overrides/overrides.py @@ -127,7 +127,7 @@ def _validate_method(method, super_class, check_signature): if hasattr(super_method, "__finalized__"): finalized = getattr(super_method, "__finalized__") if finalized: - raise TypeError(f"{method.__name__}: is finalized") + raise TypeError(f"{method.__name__}: is finalized in {super_class}") if not method.__doc__: method.__doc__ = super_method.__doc__ if (