1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-01-18 03:10:58 +00:00

feat(python): introduce a deprecation helper

it's a Python class that emits a DeprecationWarning if you try to use it
for almost anything
(two exceptions that can't be overriden from the wrapper:
 * isinstance(depr, SomeOtherClass)
 * depr is None)

we will return instances of this class to indicate that a return value
of something will be going away
This commit is contained in:
matejcik 2025-01-09 15:23:33 +01:00 committed by matejcik
parent 0cae2ab2d0
commit 4610655d77
2 changed files with 127 additions and 1 deletions

View File

@ -16,8 +16,10 @@
from __future__ import annotations
import copy
import functools
import hashlib
import inspect
import re
import struct
import unicodedata
@ -46,6 +48,7 @@ if TYPE_CHECKING:
from typing_extensions import Concatenate, ParamSpec
from . import client
from .messages import Success
from .protobuf import MessageType
MT = TypeVar("MT", bound=MessageType)
@ -310,6 +313,82 @@ def expect(
return decorator
def _deprecation_retval_helper(value: Any, stacklevel: int = 0) -> Any:
stack = inspect.stack()
func_name = stack[stacklevel + 1].function
warning_text = (
f"The return value {value!r} of function {func_name}() "
"is deprecated and it will be removed in a future version."
)
# start with warnings disabled, otherwise we emit a lot of warnings while still
# constructing the deprecation warnings helper
warning_enabled = False
def deprecation_warning_wrapper(orig_value: Callable[P, R]) -> Callable[P, R]:
def emit(*args: P.args, **kwargs: P.kwargs) -> R:
nonlocal warning_enabled
if warning_enabled:
warnings.warn(warning_text, DeprecationWarning, stacklevel=2)
# only warn once per use
warning_enabled = False
return orig_value(*args, **kwargs)
return emit
# Deprecation wrapper class.
# Defined as empty at start.
class Deprecated(value.__class__):
pass
# Here we install the deprecation_warning_wrapper for all dunder methods.
# This implicitly includes __getattribute__, which causes all non-dunder attribute
# accesses to also raise the warning.
for key in dir(value.__class__):
if not key.startswith("__"):
# skip non-dunder methods
continue
if key in ("__new__", "__init__", "__class__"):
# skip some problematic items
continue
orig_value = getattr(value.__class__, key)
if not callable(orig_value):
# skip non-functions
continue
# replace the method with a wrapper that emits a warning
setattr(Deprecated, key, deprecation_warning_wrapper(orig_value))
from .protobuf import MessageType
# construct an instance:
if isinstance(value, str):
# for str, invoke the copy constructor
ret = Deprecated(value)
elif isinstance(value, MessageType):
# MessageTypes don't have a copy constructor, so
# 1. we make an explicit copy
value = copy.copy(value)
# 2. we change the class of the copy
value.__class__ = Deprecated
# note: we don't need deep copy because all accesses to inner objects already
# trigger the warning via __getattribute__
ret = value
else:
# we don't support other types currently
raise NotImplementedError
# enable warnings
warning_enabled = True
return ret
def _return_success(msg: "Success") -> str | None:
return _deprecation_retval_helper(msg.message, stacklevel=1)
def session(
f: "Callable[Concatenate[TrezorClient, P], R]",
) -> "Callable[Concatenate[TrezorClient, P], R]":

View File

@ -16,7 +16,7 @@
import pytest
from trezorlib import tools
from trezorlib import messages, tools
VECTORS = ( # descriptor, checksum
(
@ -87,3 +87,50 @@ def test_b58encode(data_hex, encoding_b58):
@pytest.mark.parametrize("data_hex,encoding_b58", BASE58_VECTORS)
def test_b58decode(data_hex, encoding_b58):
assert tools.b58decode(encoding_b58).hex() == data_hex
def test_return_success_deprecation(recwarn):
def mkfoo() -> str:
ret = tools._return_success(messages.Success(message="foo"))
assert ret is not None # too bad we can't hook "is None" check
return ret
# check that just returning success will not cause a warning
mkfoo()
assert len(recwarn) == 0
with pytest.deprecated_call():
# equality is deprecated
assert mkfoo() == "foo"
with pytest.deprecated_call():
# comparison is deprecated
assert mkfoo() < "fooa"
with pytest.deprecated_call():
# truthiness is deprecated
assert mkfoo()
with pytest.deprecated_call():
# addition is deprecated (and hopefully all other operators)
assert mkfoo() + "a" == "fooa"
with pytest.deprecated_call():
# indexing is deprecated
assert mkfoo()[0] == "f"
with pytest.deprecated_call():
# methods are deprecated
assert mkfoo().startswith("f")
def test_deprecation_helper(recwarn):
def mkfoo() -> messages.Success:
return tools._deprecation_retval_helper(messages.Success(message="foo"))
# check that just returning success will not cause a warning
mkfoo()
assert len(recwarn) == 0
with pytest.deprecated_call():
# attributes are deprecated
assert mkfoo().message == "foo"
with pytest.deprecated_call():
# equality is deprecated (along with other operators hopefully)
assert mkfoo() != "foo"