Skip to content

Commit

Permalink
[#42] Added type annotation to all public methods (#45)
Browse files Browse the repository at this point in the history
* Added type annotation to all public methods
* Added 3,5 & 3.10 compatibility
  • Loading branch information
rockem authored Oct 17, 2022
1 parent 278d315 commit 9e2476b
Show file tree
Hide file tree
Showing 16 changed files with 96 additions and 74 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
strategy:
max-parallel: 3
matrix:
python-version: [3.6, 3.7, 3.8, 3.9]
python-version: ['3.6', '3.7', '3.8', '3.9', '3.10']

steps:
- uses: actions/checkout@v1
Expand Down
2 changes: 1 addition & 1 deletion backports/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# A Python "namespace package" http://www.python.org/dev/peps/pep-0382/
# This always goes inside of a namespace package's __init__.py
# This always goes inside a namespace package's __init__.py

from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)
2 changes: 0 additions & 2 deletions backports/asyncio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

# flake8: noqa

import sys

from backports.asyncio import runners
from backports.asyncio.runners import *

Expand Down
27 changes: 15 additions & 12 deletions busypie/awaiter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,54 @@

import busypie
import time

from busypie.condition import Condition
from busypie.func import describe
from busypie.types import Checker, ConditionEvaluator


class AsyncConditionAwaiter:
def __init__(self, condition, func_checker):
self._condition = condition
self._func_check = func_checker
def __init__(self, condition_evaluator: 'Condition', evaluator_checker: Checker):
self._condition = condition_evaluator
self._evaluator_check = evaluator_checker
self._validate_condition()
self._last_error = None

def _validate_condition(self):
if self._condition.poll_delay > self._condition.wait_time_in_secs:
raise ValueError('Poll delay should be shorter than maximum wait constraint')

async def wait_for(self, func):
async def wait_for(self, evaluator: ConditionEvaluator) -> any:
start_time = time.time()
await asyncio.sleep(self._condition.poll_delay)
while True:
try:
result = await self._func_check(func)
result = await self._evaluator_check(evaluator)
if result:
return result
except Exception as e:
self._raise_exception_if_not_ignored(e)
self._last_error = e
self._validate_wait_constraint(func, start_time)
self._validate_wait_constraint(evaluator, start_time)
await asyncio.sleep(self._condition.poll_interval)

def _raise_exception_if_not_ignored(self, e):
def _raise_exception_if_not_ignored(self, e: Exception):
ignored_exceptions = self._condition.ignored_exceptions
if ignored_exceptions is None or \
(ignored_exceptions and e.__class__ not in ignored_exceptions):
raise e

def _validate_wait_constraint(self, condition_func, start_time):
def _validate_wait_constraint(self, condition_evaluator: ConditionEvaluator, start_time: float):
if (time.time() - start_time) > self._condition.wait_time_in_secs:
raise busypie.ConditionTimeoutError(
self._describe(condition_func), self._condition.wait_time_in_secs) from self._last_error
self._describe(condition_evaluator), self._condition.wait_time_in_secs) from self._last_error

def _describe(self, condition_func):
return self._condition.description or describe(condition_func)
def _describe(self, condition_evaluator: ConditionEvaluator) -> str:
return self._condition.description or describe(condition_evaluator)


class ConditionTimeoutError(Exception):
def __init__(self, description, wait_time_in_secs):
def __init__(self, description: str, wait_time_in_secs: float):
super(ConditionTimeoutError, self).__init__("Failed to meet condition of [{}] within {} seconds"
.format(description, wait_time_in_secs))
self.description = description
25 changes: 13 additions & 12 deletions busypie/checker.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
from busypie.func import is_async
from busypie.types import ConditionEvaluator


async def check(f):
if is_async(f):
return await f()
return f()
async def check(condition_evaluator: ConditionEvaluator) -> any:
if is_async(condition_evaluator):
return await condition_evaluator()
return condition_evaluator()


async def negative_check(f):
if is_async(f):
result = await f()
async def negative_check(condition_evaluator: ConditionEvaluator) -> bool:
if is_async(condition_evaluator):
result = await condition_evaluator()
return not result
return not f()
return not condition_evaluator()


async def assert_check(f):
if is_async(f):
await f()
async def assert_check(condition_evaluator: ConditionEvaluator) -> bool:
if is_async(condition_evaluator):
await condition_evaluator()
else:
f()
condition_evaluator()
return True
64 changes: 34 additions & 30 deletions busypie/condition.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from builtins import Exception
from copy import deepcopy
from functools import partial
from typing import Type

from busypie import runner
from busypie.awaiter import AsyncConditionAwaiter
from busypie.checker import check, negative_check, assert_check
from busypie.durations import ONE_HUNDRED_MILLISECONDS, SECOND
from busypie.time import time_value_operator
from busypie.types import ConditionEvaluator, Checker

DEFAULT_MAX_WAIT_TIME = 10 * SECOND
DEFAULT_POLL_INTERVAL = ONE_HUNDRED_MILLISECONDS
Expand All @@ -14,61 +16,63 @@

class ConditionBuilder:

def __init__(self, condition=None):
def __init__(self, condition: 'Condition' = None):
self._condition = Condition() if condition is None else condition
self._create_time_based_functions()
self._create_time_based_evaluatortions()

def _create_time_based_functions(self):
self.at_most = self._time_property_func_for('wait_time_in_secs')
def _create_time_based_evaluatortions(self):
self.at_most = self._time_property_evaluator_for('wait_time_in_secs')
self.wait_at_most = self.at_most
self.poll_delay = self._time_property_func_for('poll_delay')
self.poll_interval = self._time_property_func_for('poll_interval')
self.poll_delay = self._time_property_evaluator_for('poll_delay')
self.poll_interval = self._time_property_evaluator_for('poll_interval')

def _time_property_func_for(self, name):
def _time_property_evaluator_for(self, name: str):
return partial(time_value_operator, visitor=partial(self._time_property, name=name))

def _time_property(self, value, name):
def _time_property(self, value: any, name: str) -> 'ConditionBuilder':
setattr(self._condition, name, value)
return self._new_builder_with_cloned_condition()

def _new_builder_with_cloned_condition(self):
def _new_builder_with_cloned_condition(self) -> 'ConditionBuilder':
return ConditionBuilder(deepcopy(self._condition))

def ignore_exceptions(self, *excludes):
def ignore_exceptions(self, *excludes: Type[Exception]) -> 'ConditionBuilder':
self._condition.ignored_exceptions = excludes
return self._new_builder_with_cloned_condition()

def wait(self):
def wait(self) -> 'ConditionBuilder':
return self._new_builder_with_cloned_condition()

def with_description(self, description):
def with_description(self, description: str) -> 'ConditionBuilder':
self._condition.description = description
return self._new_builder_with_cloned_condition()

def until(self, func):
return runner.run(self._wait_for(func, check))
def until(self, evaluator: ConditionEvaluator) -> any:
return runner.run(self._wait_for(evaluator, check))

async def _wait_for(self, evaluator: ConditionEvaluator, checker: Checker):
from busypie.awaiter import AsyncConditionAwaiter

async def _wait_for(self, func, checker):
return await AsyncConditionAwaiter(
condition=self._condition,
func_checker=checker).wait_for(func)
condition_evaluator=self._condition,
evaluator_checker=checker).wait_for(evaluator)

def during(self, func):
runner.run(self._wait_for(func, negative_check))
def during(self, evaluator: ConditionEvaluator) -> None:
runner.run(self._wait_for(evaluator, negative_check))

async def until_async(self, func):
return await self._wait_for(func, check)
async def until_async(self, evaluator: ConditionEvaluator) -> any:
return await self._wait_for(evaluator, check)

async def during_async(self, func):
await self._wait_for(func, negative_check)
async def during_async(self, evaluator: ConditionEvaluator) -> None:
await self._wait_for(evaluator, negative_check)

def until_asserted(self, func):
def until_asserted(self, evaluator: ConditionEvaluator) -> any:
self._condition.append_exception(AssertionError)
return runner.run(self._wait_for(func, assert_check))
return runner.run(self._wait_for(evaluator, assert_check))

async def until_asserted_async(self, func):
async def until_asserted_async(self, evaluator: ConditionEvaluator) -> any:
self._condition.append_exception(AssertionError)
await self._wait_for(func, assert_check)
return await self._wait_for(evaluator, assert_check)

def __eq__(self, other):
if not isinstance(other, ConditionBuilder):
Expand All @@ -83,7 +87,7 @@ class Condition:
poll_delay = DEFAULT_POLL_DELAY
description = None

def append_exception(self, exception):
def append_exception(self, exception: Type[Exception]):
if self.ignored_exceptions is None:
self.ignored_exceptions = []
self.ignored_exceptions.append(exception)
Expand All @@ -103,5 +107,5 @@ def __eq__(self, other):
visitor=partial(setattr, Condition, 'wait_time_in_secs'))


def reset_defaults():
def reset_defaults() -> None:
Condition.wait_time_in_secs = DEFAULT_MAX_WAIT_TIME
4 changes: 2 additions & 2 deletions busypie/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
from busypie.durations import SECOND


def wait():
def wait() -> 'ConditionBuilder':
return ConditionBuilder()


given = wait


def wait_at_most(value, unit=SECOND):
def wait_at_most(value: float, unit: float = SECOND) -> 'ConditionBuilder':
return wait().at_most(value, unit)


Expand Down
11 changes: 6 additions & 5 deletions busypie/func.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,32 @@
import inspect
import re
from functools import partial
from typing import Callable


def is_async(func):
def is_async(func) -> bool:
return inspect.iscoroutinefunction(_unpartial(func))


def _unpartial(func):
def _unpartial(func: Callable) -> Callable:
while isinstance(func, partial):
func = func.func
return func


def describe(func):
def describe(func: Callable) -> str:
if _is_a_lambda(func):
return _content_of(func)
return _unpartial(func).__name__


def _is_a_lambda(func):
def _is_a_lambda(func: Callable) -> bool:
lambda_template = lambda: 0 # noqa: E731
return isinstance(func, type(lambda_template)) and \
func.__name__ == lambda_template.__name__


def _content_of(lambda_func):
def _content_of(lambda_func: Callable) -> str:
source_line = inspect.getsource(lambda_func)
r = re.search(r'lambda[^:]*:\s*(.+)\s*\)', source_line)
return r.group(1)
7 changes: 4 additions & 3 deletions busypie/time.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
from numbers import Number
from typing import Callable

from busypie.durations import SECOND


def time_value_operator(value, unit=SECOND, visitor=None):
def time_value_operator(value: float, unit: float = SECOND, visitor: Callable[[float], any] = None) -> any:
_validate_time_and_unit(value, unit)
return visitor(value * unit)


def _validate_time_and_unit(value, unit):
def _validate_time_and_unit(value: float, unit: float) -> None:
_validate_positive_number(value, 'Time value of {} is not allowed')
_validate_positive_number(unit, 'Unit value of {} is not allowed')


def _validate_positive_number(value, message):
def _validate_positive_number(value: float, message: str) -> None:
if value is None or not isinstance(value, Number) or value < 0:
raise ValueError(message.format(value))
5 changes: 5 additions & 0 deletions busypie/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from typing import Callable

ConditionEvaluator = Callable[[], any]

Checker = Callable[[ConditionEvaluator], any]
1 change: 1 addition & 0 deletions docs/source/assert.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ of checking a boolean expression. It's possible to achieve this with the
async def test_event_should_be_dispatched():
dispatcher.dispatch(event)
await wait().until_asserted_async(validate_dispatched_event)

1 change: 1 addition & 0 deletions docs/source/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ As with ``until`` async support is available for it as well::
async def create_user():
dispatch_user_create_command()
await wait().during_async(lambda: !app.has_user(user))

11 changes: 9 additions & 2 deletions docs/source/timeout.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ It's possible to specify the timeout either by wait or wait_at_most::
wait().at_most(5 * SECOND).until(condition_function)
wait_at_most(5 * SECOND).until(condition_function)

Condition description
-------------------
Condition timeout description
-----------------------------
Upon a timeout :pypi:`busypie` will raise a 'ConditionTimeoutError' exception, with the following message::

Failed to meet condition of <description> within X seconds
Expand All @@ -22,10 +22,17 @@ For description there are 3 options:

wait().with_description('check app is running').until(lambda: app_state() == 'UP')


Condition timeout cause
-----------------------
If there was an ignored exception that was thrown prior to the the timeout, the
'ConditionTimeoutError' error, will contain that last exception as the cause for it.

Default timeout
---------------
The default timeout in :pypi:`busypie` is set to 10 seconds, you can change that by using::

from busypie import set_default_timeout

set_default_timeout(1 * MINUTE)

2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ test=pytest
testpaths = tests

[flake8]
exclude = venv,.egg
exclude = venv,.egg,.eggs
max-line-length = 120
count = True
max-complexity = 6
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
],
python_requires='>=3.5',
python_requires='>=3.6',
)
4 changes: 2 additions & 2 deletions tests/test_description.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,5 @@ def test_lambda_content_with_captures():
assert 'x == y' == e.value.description


def _always_fail_check(x=None):
return False
def _always_fail_check(x=10):
return x == 0

0 comments on commit 9e2476b

Please sign in to comment.