diff --git a/docs/source/_templates/api_reference_class_template.rst b/docs/source/_templates/api_reference_class_template.rst index 91bc94d..29e123d 100644 --- a/docs/source/_templates/api_reference_class_template.rst +++ b/docs/source/_templates/api_reference_class_template.rst @@ -3,11 +3,13 @@ args BinaryFile ConstantUnitMathConventions + dev exps ndarray np num TextFile + TimeIt TypedList TypedListWithID UnitConverter diff --git a/docs/source/api_reference/dev.rst b/docs/source/api_reference/dev.rst new file mode 100644 index 0000000..f229cc9 --- /dev/null +++ b/docs/source/api_reference/dev.rst @@ -0,0 +1,19 @@ +pyxx.dev +======== + +.. automodule:: pyxx.dev + +.. currentmodule:: pyxx.dev + + +Code Profiling +-------------- + +The classes below are intended to assist in analyzing the performance of and +profiling code. + +.. autosummary:: + :toctree: ./api + :template: ../_templates/api_reference_class_template.rst + + TimeIt diff --git a/docs/source/api_reference/index.rst b/docs/source/api_reference/index.rst index 7c3f013..79193f9 100644 --- a/docs/source/api_reference/index.rst +++ b/docs/source/api_reference/index.rst @@ -21,6 +21,7 @@ Package Modules --------------- - :py:mod:`pyxx.arrays` -- Code for processing lists, tuples, or other array-formatted data +- :py:mod:`pyxx.dev` -- Code for to assist in debugging and analyzing Python code - :py:mod:`pyxx.files` -- Code for reading, writing, and processing files - :py:mod:`pyxx.numbers` -- Code for processing and managing numerical types of data - :py:mod:`pyxx.strings` -- Code for processing, parsing, and modifying strings diff --git a/docs/source/index.rst b/docs/source/index.rst index 63148bd..cb6a681 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -35,6 +35,7 @@ reducing the need to duplicate code between projects. Overview api_reference/arrays + api_reference/dev api_reference/files api_reference/numbers api_reference/strings diff --git a/pyxx/__init__.py b/pyxx/__init__.py index c0cb562..fb89e4d 100644 --- a/pyxx/__init__.py +++ b/pyxx/__init__.py @@ -6,6 +6,7 @@ # PACKAGE MODULES ------------------------------------------------------------ from . import arrays +from . import dev from . import files from . import numbers from . import strings diff --git a/pyxx/dev/__init__.py b/pyxx/dev/__init__.py new file mode 100644 index 0000000..f46d772 --- /dev/null +++ b/pyxx/dev/__init__.py @@ -0,0 +1,7 @@ +"""**Functions and utilities to assist in debugging or analyzing Python code** + +The :py:mod:`pyxx.dev` module is intended to provide commonly used tools +or scripts that can be useful when debugging Python code. +""" + +from .classes import TimeIt diff --git a/pyxx/dev/classes/__init__.py b/pyxx/dev/classes/__init__.py new file mode 100644 index 0000000..8efcea5 --- /dev/null +++ b/pyxx/dev/classes/__init__.py @@ -0,0 +1,5 @@ +"""This module contains classes that can be used to assist in debugging +Python code. +""" + +from .timer import TimeIt diff --git a/pyxx/dev/classes/timer.py b/pyxx/dev/classes/timer.py new file mode 100644 index 0000000..9d82420 --- /dev/null +++ b/pyxx/dev/classes/timer.py @@ -0,0 +1,109 @@ +"""Classes for timing code execution. +""" + +import time + +from pyxx.units.classes.unitconverter import UnitConverterSI + + +class TimeIt: + """A context manager for measuring the duration of a code block. + + This context manager can be used in a "with" statement to measure the + duration of code within the statement. The resulting duration will be + printed to the console when the "with" statement completes. + + Warnings + -------- + Using this timer will add some overhead to the code block being measured. + The measured duration outputted to the terminal will be slightly larger + than the actual execution time of the code block. + + Examples + -------- + + Time a code block and print the duration to the terminal: + + .. code-block:: python + + >>> from pyxx.dev import TimeIt + >>> import time + >>> with TimeIt(units='ms', message='Execution time: {time:.2f} {units}'): + ... # Code block of which to measure the duration + ... time.sleep(1) + Execution time: 1000.10 ms + + Time a code block and access the duration in Python code following the + code block: + + .. code-block:: python + + >>> from pyxx.dev import TimeIt + >>> import time + >>> timer = TimeIt(print_duration=False) + >>> with timer: + ... # Code block of which to measure the duration + ... time.sleep(1) + >>> print(timer.duration('ms')) + 1000.1010101010101 + """ + + def __init__(self, print_duration: bool = True, units: str = 's', + message: str = 'Code duration: {time} {units}') -> None: + """Creates a new context manager for measuring the duration of a code block + + Parameters + ---------- + print_duration : bool, optional + Whether to print to the terminal the duration of the code block + units : str, optional + Only applicable if ``print_duration`` is ``True``. Specifies the + units in which the duration will be displayed to the terminal + (default is ``'s'``) + message : str, optional + Only applicable if ``print_duration`` is ``True``. The message + template to display the duration (default is + ``'Code duration: {time} {units}'``). The ``{time}`` and + ``{units}`` placeholders will be replaced by the duration and + units, respectively + """ + self.__unit_converter = UnitConverterSI() + + if (not (self.__unit_converter.is_defined_unit(units) + and self.__unit_converter.is_convertible('s', units))): + raise ValueError( + f'Invalid units: cannot convert from seconds to "{units}"') + + self.__print = bool(print_duration) + self.__units = units + self.__message = message + + self.__t0_s = 0.0 + self.__dt_s = 0.0 + + def __enter__(self) -> None: + self.__t0_s = time.time() + + def __exit__(self, *args, **kwargs) -> None: + t1_s = time.time() + + # Calculate duration in seconds + self.__dt_s = t1_s - self.__t0_s + + if self.__print: + duration = self.__unit_converter.convert( + quantity=self.__dt_s, from_unit='s', to_unit=self.__units) + + print(self.__message.format(time=duration, units=self.__units)) + + def duration(self, units: str = 's') -> float: + """Returns the last measured duration from the context manager + + Parameters + ---------- + units : str + The units in which to return the duration (default is ``'s'``) + """ + return float(self.__unit_converter.convert( + quantity=self.__dt_s, from_unit='s', to_unit=units + )) diff --git a/tests/__init__.py b/tests/__init__.py index e50141a..d05681f 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -55,6 +55,7 @@ def max_array_diff(array1, array2): # Import and run tests from .arrays import * +from .dev import * from .files import * from .numbers import * from .strings import * diff --git a/tests/dev/__init__.py b/tests/dev/__init__.py new file mode 100644 index 0000000..c798808 --- /dev/null +++ b/tests/dev/__init__.py @@ -0,0 +1 @@ +from .classes import * diff --git a/tests/dev/classes/__init__.py b/tests/dev/classes/__init__.py new file mode 100644 index 0000000..8d1c012 --- /dev/null +++ b/tests/dev/classes/__init__.py @@ -0,0 +1 @@ +from .test_timer import * diff --git a/tests/dev/classes/test_timer.py b/tests/dev/classes/test_timer.py new file mode 100644 index 0000000..c6d2bb5 --- /dev/null +++ b/tests/dev/classes/test_timer.py @@ -0,0 +1,56 @@ +import time +import unittest + +from pyxx.dev import TimeIt +from tests import CapturePrint + + +class Test_TimeIt(unittest.TestCase): + def test_duration(self): + # Verifies that duration is measured within a reasonable range + timer = TimeIt(print_duration=False) + with timer: + time.sleep(1.5) + + self.assertAlmostEqual(timer.duration(), 1.5, delta=0.1) + + def test_default_message(self): + # Verifies that correct default message is printed after code completes + with CapturePrint() as terminal_output: + timer = TimeIt() + with timer: + time.sleep(0.1) + + self.assertEqual( + terminal_output.getvalue(), + f'Code duration: {timer.duration()} s\n', + ) + + def test_custom_message(self): + # Verifies that correct custom message is printed after code completes + with CapturePrint() as terminal_output: + timer = TimeIt(message='Execution time: {time} {units}', units='ms') + with timer: + time.sleep(0.1) + + self.assertEqual( + terminal_output.getvalue(), + f'Execution time: {timer.duration() * 1000} ms\n', + ) + + def test_convert_duration(self): + # Verifies that unit conversion of returned duration is performed correctly + timer = TimeIt(print_duration=False) + with timer: + time.sleep(0.5) + + self.assertAlmostEqual( + timer.duration(units='s') * 1000, + timer.duration(units='ms'), + ) + + def test_invalid_units(self): + # Verify that an error is raised when invalid units are specified + with self.assertRaises(ValueError): + with TimeIt(units='invalid'): + pass