From 5995eedf2b09875112897f070a7298f2cee4a626 Mon Sep 17 00:00:00 2001 From: Matthew Wardrop Date: Tue, 16 Jan 2024 16:45:57 -0800 Subject: [PATCH] Align warning frame with user input. --- docsite/docs/usage/special_types.md | 19 +++++++++++++++---- spec_classes/types/spec_property.py | 5 +++-- spec_classes/utils/stackdepth.py | 20 ++++++++++++++++++++ 3 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 spec_classes/utils/stackdepth.py diff --git a/docsite/docs/usage/special_types.md b/docsite/docs/usage/special_types.md index 700ebb1..8cd2a37 100644 --- a/docsite/docs/usage/special_types.md +++ b/docsite/docs/usage/special_types.md @@ -302,9 +302,15 @@ as shown): class MyClass: @spec_property( overridable=True, # Whether to allow overriding by default. + warn_on_override=False, # Whether to warn the user when the property getter + # is overridden. Can be a boolean, string, or `Warning` instance. + # If non-boolean, then it is treated as the message to present to + # the user using `warnings.warn`. cache=False, # Whether to cache the result after first evaluation. - invalidated_by=None, # An iterable of attributes which when mutated invalidate the cache (only supported when used with spec-classes) - allow_attribute_error=True, # Whether to allow properties to raise `AttributeErrors` which are often masked during attribute lookup. + invalidated_by=None, # An iterable of attributes which when mutated + # invalidate the cache (only supported when used with spec-classes) + allow_attribute_error=True, # Whether to allow properties to raise + # `AttributeErrors` which are often masked during attribute lookup. ) def method(self): ... @@ -328,11 +334,16 @@ as follows: ```python class MyClass: @classproperty( - overridable=True, # Whether to allow overriding by default. + overridable=False, # Whether to allow overriding by default. + warn_on_override=False, # Whether to warn the user when the property getter + # is overridden. Can be a boolean, string, or `Warning` instance. + # If non-boolean, then it is treated as the message to present to + # the user using `warnings.warn`. cache=False, # Whether to cache the result after first evaluation. cache_per_subclass=False, # Whether cache should be stored per subclass # rather than once for all classes. - allow_attribute_error=True, # Whether to allow properties to raise `AttributeErrors` which are often masked during attribute lookup. + allow_attribute_error=True, # Whether to allow properties to raise + # `AttributeErrors` which are often masked during attribute lookup. ) def method(cls): ... diff --git a/spec_classes/types/spec_property.py b/spec_classes/types/spec_property.py index 30b9c1d..b2d7a6f 100644 --- a/spec_classes/types/spec_property.py +++ b/spec_classes/types/spec_property.py @@ -7,6 +7,7 @@ from spec_classes.errors import NestedAttributeError from spec_classes.utils.mutation import prepare_attr_value +from spec_classes.utils.stackdepth import get_spec_classes_depth from spec_classes.utils.type_checking import check_type, type_label @@ -292,7 +293,7 @@ def __set__(self, instance, value): f"Property `{self._qualified_name}` is now overridden and will not update based on instance state." if isinstance(self.warn_on_override, bool) else self.warn_on_override, - stacklevel=2, + stacklevel=get_spec_classes_depth(), ) return raise AttributeError( @@ -470,7 +471,7 @@ def __set__(self, obj, value): f"Class property `{self._qualified_name}` is now overridden and will not update based on class state." if isinstance(self.warn_on_override, bool) else self.warn_on_override, - stacklevel=2, + stacklevel=get_spec_classes_depth(), ) return raise AttributeError( diff --git a/spec_classes/utils/stackdepth.py b/spec_classes/utils/stackdepth.py new file mode 100644 index 0000000..656abcf --- /dev/null +++ b/spec_classes/utils/stackdepth.py @@ -0,0 +1,20 @@ +import sys + + +def get_spec_classes_depth(): + """ + The recursion depth of the caller into spec-classes from first + non-spec-classes frame. + + Args: + offset: An offset to apply the calculated depth. An `offset` of 1 aligns + the depth with that of the caller. + """ + stacklevel = 0 + f = sys._getframe() + while f.f_back and ( + "spec_classes" in f.f_code.co_filename or f.f_code.co_filename == "" + ): + f = f.f_back + stacklevel += 1 + return stacklevel