Add check that _lock is a lock in requires_lock
This commit is contained in:
parent
9be9097246
commit
cbd2f9e0e3
2 changed files with 25 additions and 1 deletions
|
|
@ -23,6 +23,16 @@ COMMIT_REGEX = re.compile(r"[0-9a-f]{40}")
|
||||||
REF_REGEX = re.compile(r"^ref:\s(.*)$")
|
REF_REGEX = re.compile(r"^ref:\s(.*)$")
|
||||||
|
|
||||||
|
|
||||||
|
def _is_lock_like(obj):
|
||||||
|
"""Check if an object is a lock.
|
||||||
|
|
||||||
|
Cannot use ``isinstance(obj, threading.RLock)``, may be possible to use
|
||||||
|
``isinstance(obj, threading._RLock)``. But this uses a private method and may
|
||||||
|
break. Instead making the check that both "acquire" and "release" exist.
|
||||||
|
"""
|
||||||
|
return hasattr(obj, "acquire") and hasattr(obj, "release")
|
||||||
|
|
||||||
|
|
||||||
def requires_lock(method: Callable[P, T]) -> Callable[P, T]:
|
def requires_lock(method: Callable[P, T]) -> Callable[P, T]:
|
||||||
"""Decorate a class method so that it requires the class lock to run.
|
"""Decorate a class method so that it requires the class lock to run.
|
||||||
|
|
||||||
|
|
@ -31,8 +41,12 @@ def requires_lock(method: Callable[P, T]) -> Callable[P, T]:
|
||||||
|
|
||||||
@wraps(method)
|
@wraps(method)
|
||||||
def wrapper(self, *args: P.args, **kwargs: P.kwargs) -> T:
|
def wrapper(self, *args: P.args, **kwargs: P.kwargs) -> T:
|
||||||
|
# Confirm the object the method is attached to has a self._lock property
|
||||||
if not hasattr(self, "_lock"):
|
if not hasattr(self, "_lock"):
|
||||||
raise AttributeError(f"{self.__class__.__name__} has no '_lock' attribute")
|
raise AttributeError(f"{self.__class__.__name__} has no '_lock' attribute")
|
||||||
|
# And that it is a reentrant lock.
|
||||||
|
if not _is_lock_like(self._lock):
|
||||||
|
raise TypeError("requires_lock requires self._lock to be a lock")
|
||||||
with self._lock:
|
with self._lock:
|
||||||
return method(self, *args, **kwargs)
|
return method(self, *args, **kwargs)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ def lockable_obj() -> LockableClass:
|
||||||
|
|
||||||
|
|
||||||
def test_error_if_no_lock(lockable_obj):
|
def test_error_if_no_lock(lockable_obj):
|
||||||
"""Test there is an Attribute error if the object has no lock attribute."""
|
"""Test there is an AttributeError if the object has no _lock attribute."""
|
||||||
# Delete the lock!
|
# Delete the lock!
|
||||||
del lockable_obj._lock
|
del lockable_obj._lock
|
||||||
with pytest.raises(AttributeError) as exec_info:
|
with pytest.raises(AttributeError) as exec_info:
|
||||||
|
|
@ -58,6 +58,16 @@ def test_error_if_no_lock(lockable_obj):
|
||||||
assert str(exec_info.value) == "LockableClass has no '_lock' attribute"
|
assert str(exec_info.value) == "LockableClass has no '_lock' attribute"
|
||||||
|
|
||||||
|
|
||||||
|
def test_error_if_lock_of_wrong_type(lockable_obj):
|
||||||
|
"""Test there is an TypeError if the object's _lock attribute isn't a lock."""
|
||||||
|
# Delete the lock!
|
||||||
|
lockable_obj._lock = "I'm a mock, not a lock!"
|
||||||
|
with pytest.raises(TypeError) as exec_info:
|
||||||
|
lockable_obj.test_property
|
||||||
|
expected_msg = "requires_lock requires self._lock to be a lock"
|
||||||
|
assert str(exec_info.value) == expected_msg
|
||||||
|
|
||||||
|
|
||||||
def test_functionality(lockable_obj):
|
def test_functionality(lockable_obj):
|
||||||
"""Check that this doesn't affect the most basic functionality of the class."""
|
"""Check that this doesn't affect the most basic functionality of the class."""
|
||||||
assert lockable_obj.test_property == 1
|
assert lockable_obj.test_property == 1
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue