diff --git a/CHANGELOG.md b/CHANGELOG.md index f3985a72782e75031b541ef14d21a24db5bf6f03..8fb8d9e96390a8f49c4116052a600ff367c8058c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ * changed the behaviour of `JSONEncoder` when it comes to serializing unknown objects * moved the exception `LockIsHeld` to it's proper place * fixed `dump_frames_on` not to log on **sys.stderr** +* added `satella.time.measure` # v2.4.16 diff --git a/docs/index.rst b/docs/index.rst index 2608553ab4f7e40311b3d5de5e918fc48be4f125..7fa8dfbe450bdb0d13a093fcee0cc3914f32125b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -23,6 +23,7 @@ Visit the project's page at GitHub_! posix import files + time Indices and tables diff --git a/docs/time.rst b/docs/time.rst new file mode 100644 index 0000000000000000000000000000000000000000..dbec771ddaf0a37e9c1249dd622a2fe5c89dd95e --- /dev/null +++ b/docs/time.rst @@ -0,0 +1,7 @@ +time +==== + +Sometimes you just need to measure how long does a routine call take. + +.. autoclass:: satella.time.measure + :members: diff --git a/satella/__init__.py b/satella/__init__.py index 2f1aa7da2daa0cdf0d078a0ee9360f4c71a78372..2e27e304b5c425a59075371eeedb58e110a613e7 100644 --- a/satella/__init__.py +++ b/satella/__init__.py @@ -1 +1 @@ -__version__ = '2.4.17a9' +__version__ = '2.4.17' diff --git a/satella/time.py b/satella/time.py new file mode 100644 index 0000000000000000000000000000000000000000..1db71846c974eee71b93cc2c687186323633ddda --- /dev/null +++ b/satella/time.py @@ -0,0 +1,67 @@ +import logging +import typing as tp +import time +import functools + +logger = logging.getLogger(__name__) + +__all__ = ['measure'] + + +class measure: + """ + A class used to measure time elapsed. Use for example like this: + + >>> with measure() as measurement: + >>> time.sleep(1) + >>> print('This has taken so far', measurement(), 'seconds') + >>> time.sleep(1) + >>> print('A total of ', measurement(), 'seconds have elapsed') + + You can also use the .start() method instead of context manager. Time measurement + will stop after exiting or calling .stop() depending on stop_on_stop flag. + + You can also decorate your functions to have them keep track time of their execution, like that: + + >>> @measure() + >>> def measuring(measurement_object: measure, *args, **kwargs): + >>> ... + + :param stop_on_stop: stop elapsing time upon calling .stop()/exiting the context manager + """ + def __init__(self, stop_on_stop: bool = True): + self.started_on = None + self.elapsed = None + self.stopped_on = None + self.stop_on_stop = stop_on_stop + + def start(self) -> None: + """Start measuring time""" + self.started_on = time.monotonic() + + def __call__(self, fun: tp.Optional[tp.Callable] = None) -> float: + if fun is None: + if self.started_on is None: + raise RuntimeError('Time measurement did not start yet, use .start()') + if self.stop_on_stop and self.elapsed is not None: + return self.elapsed + return time.monotonic() - self.started_on + else: + @functools.wraps(fun) + def inner(*args, **kwargs): + with self: + return fun(self, *args, **kwargs) + return inner + + def __enter__(self): + self.start() + return self + + def stop(self) -> None: + """Stop counting time""" + self.stopped_on = time.monotonic() + self.elapsed = self.stopped_on - self.started_on + + def __exit__(self, exc_type, exc_val, exc_tb): + self.stop() + return False diff --git a/tests/test_time.py b/tests/test_time.py new file mode 100644 index 0000000000000000000000000000000000000000..5389cc71ac9f94bc4ec4735bf11bd0c29e163a15 --- /dev/null +++ b/tests/test_time.py @@ -0,0 +1,20 @@ +import unittest +import typing as tp +import time +from satella.time import measure + + +class TestTime(unittest.TestCase): + def test_measure(self): + with measure() as measurement: + time.sleep(0.5) + + self.assertGreaterEqual(measurement(), 0.5) + + def test_measure_decorator(self): + @measure() + def measured(measurement): + time.sleep(0.5) + self.assertGreaterEqual(measurement(), 0.5) + measured() +