From 33aeecd95107b966d8da6361a7b1c2ca3623d70b Mon Sep 17 00:00:00 2001 From: Nathan Hess <63890205+nathan-hess@users.noreply.github.com> Date: Mon, 11 Dec 2023 21:51:50 +0000 Subject: [PATCH 1/7] Create new context manager to time code block and update API reference documentation --- docs/source/api_reference/debugging.rst | 19 +++++++ docs/source/api_reference/index.rst | 1 + docs/source/index.rst | 1 + pyxx/__init__.py | 1 + pyxx/debugging/__init__.py | 7 +++ pyxx/debugging/classes/__init__.py | 5 ++ pyxx/debugging/classes/timer.py | 76 +++++++++++++++++++++++++ 7 files changed, 110 insertions(+) create mode 100644 docs/source/api_reference/debugging.rst create mode 100644 pyxx/debugging/__init__.py create mode 100644 pyxx/debugging/classes/__init__.py create mode 100644 pyxx/debugging/classes/timer.py diff --git a/docs/source/api_reference/debugging.rst b/docs/source/api_reference/debugging.rst new file mode 100644 index 0000000..fa35373 --- /dev/null +++ b/docs/source/api_reference/debugging.rst @@ -0,0 +1,19 @@ +pyxx.debugging +============== + +.. automodule:: pyxx.debugging + +.. currentmodule:: pyxx.debugging + + +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..ce33e23 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.debugging` -- 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..8717d19 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/debugging api_reference/files api_reference/numbers api_reference/strings diff --git a/pyxx/__init__.py b/pyxx/__init__.py index c0cb562..58b58cd 100644 --- a/pyxx/__init__.py +++ b/pyxx/__init__.py @@ -6,6 +6,7 @@ # PACKAGE MODULES ------------------------------------------------------------ from . import arrays +from . import debugging from . import files from . import numbers from . import strings diff --git a/pyxx/debugging/__init__.py b/pyxx/debugging/__init__.py new file mode 100644 index 0000000..698ee36 --- /dev/null +++ b/pyxx/debugging/__init__.py @@ -0,0 +1,7 @@ +"""**Functions and utilities to assist in debugging or analyzing Python code** + +The :py:mod:`pyxx.debugging` 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/debugging/classes/__init__.py b/pyxx/debugging/classes/__init__.py new file mode 100644 index 0000000..8efcea5 --- /dev/null +++ b/pyxx/debugging/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/debugging/classes/timer.py b/pyxx/debugging/classes/timer.py new file mode 100644 index 0000000..52c0752 --- /dev/null +++ b/pyxx/debugging/classes/timer.py @@ -0,0 +1,76 @@ +"""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 + -------- + + .. code-block:: python + + >>> from pyxx.debugging import TimeIt + >>> import time + >>> with TimeIt(units='ms', message='Execution time: {time} {units}'): + ... # Code block of which to measure the duration + ... time.sleep(1) + Execution time: 1000.1010101010101 ms + """ + + def __init__(self, units: str = 's', + message: str = 'Command duration: {time} {units}') -> None: + """Creates a new context manager for measuring the duration of a code block + + Parameters + ---------- + units : str, optional + The units in which the duration will be displayed (default is ``'s'``) + message : str, optional + The message template to display the duration (default is + ``'Command duration: {time} {units}'``). The ``{time}`` and + ``{units}`` placeholders will be replaced by the duration and + units, respectively + + Raises + ------ + ValueError + If the specified units are invalid and cannot be converted from + seconds + """ + 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.__units = units + self.__message = message + + self.__t0 = 0.0 + + def __enter__(self) -> None: + self.__t0 = time.time() + + def __exit__(self, *args, **kwargs) -> None: + t1 = time.time() + duration = self.__unit_converter.convert(quantity=t1 - self.__t0, + from_unit='s', + to_unit=self.__units) + + print(self.__message.format(time=duration, units=self.__units)) From f387f4268e79b7447933074fa4ef2ec30bc6159e Mon Sep 17 00:00:00 2001 From: Nathan Hess <63890205+nathan-hess@users.noreply.github.com> Date: Tue, 12 Dec 2023 15:28:49 +0000 Subject: [PATCH 2/7] Add capability to return `pyxx.debugging.TimeIT` timer duration --- .../api_reference_class_template.rst | 1 + pyxx/debugging/classes/timer.py | 68 ++++++++++++++----- 2 files changed, 51 insertions(+), 18 deletions(-) diff --git a/docs/source/_templates/api_reference_class_template.rst b/docs/source/_templates/api_reference_class_template.rst index 91bc94d..37bef1d 100644 --- a/docs/source/_templates/api_reference_class_template.rst +++ b/docs/source/_templates/api_reference_class_template.rst @@ -8,6 +8,7 @@ np num TextFile + TimeIt TypedList TypedListWithID UnitConverter diff --git a/pyxx/debugging/classes/timer.py b/pyxx/debugging/classes/timer.py index 52c0752..8a55d48 100644 --- a/pyxx/debugging/classes/timer.py +++ b/pyxx/debugging/classes/timer.py @@ -22,35 +22,49 @@ class TimeIt: Examples -------- + Time a code block and print the duration to the terminal: + + .. code-block:: python + + >>> from pyxx.debugging 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 store the duration in a variable: + .. code-block:: python >>> from pyxx.debugging import TimeIt >>> import time - >>> with TimeIt(units='ms', message='Execution time: {time} {units}'): + >>> timer = TimeIt(print_duration=False) + >>> with timer: ... # Code block of which to measure the duration ... time.sleep(1) - Execution time: 1000.1010101010101 ms + >>> print(timer.duration('ms')) + 1000.1010101010101 """ - def __init__(self, units: str = 's', + def __init__(self, print_duration: bool = True, units: str = 's', message: str = 'Command 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 - The units in which the duration will be displayed (default is ``'s'``) + 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 - The message template to display the duration (default is + Only applicable if ``print_duration`` is ``True``. The message + template to display the duration (default is ``'Command duration: {time} {units}'``). The ``{time}`` and ``{units}`` placeholders will be replaced by the duration and units, respectively - - Raises - ------ - ValueError - If the specified units are invalid and cannot be converted from - seconds """ self.__unit_converter = UnitConverterSI() @@ -59,18 +73,36 @@ def __init__(self, units: str = 's', raise ValueError( f'Invalid units: cannot convert from seconds to "{units}"') + self.__print = bool(print_duration) self.__units = units self.__message = message - self.__t0 = 0.0 + self.__t0_s = 0.0 + self.__dt_s = 0.0 def __enter__(self) -> None: - self.__t0 = time.time() + self.__t0_s = time.time() def __exit__(self, *args, **kwargs) -> None: - t1 = time.time() - duration = self.__unit_converter.convert(quantity=t1 - self.__t0, - from_unit='s', - to_unit=self.__units) + 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)) - 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 self.__unit_converter.convert( + quantity=self.__dt_s, from_unit='s', to_unit=units + ) From 7730565e0f753b3932953b8bd733c92caf1a4b00 Mon Sep 17 00:00:00 2001 From: Nathan Hess <63890205+nathan-hess@users.noreply.github.com> Date: Tue, 12 Dec 2023 15:34:36 +0000 Subject: [PATCH 3/7] Rename module --- docs/source/_templates/api_reference_class_template.rst | 1 + docs/source/api_reference/{debugging.rst => dev.rst} | 8 ++++---- docs/source/api_reference/index.rst | 2 +- docs/source/index.rst | 2 +- pyxx/__init__.py | 2 +- pyxx/{debugging => dev}/__init__.py | 2 +- pyxx/{debugging => dev}/classes/__init__.py | 0 pyxx/{debugging => dev}/classes/timer.py | 4 ++-- 8 files changed, 11 insertions(+), 10 deletions(-) rename docs/source/api_reference/{debugging.rst => dev.rst} (71%) rename pyxx/{debugging => dev}/__init__.py (68%) rename pyxx/{debugging => dev}/classes/__init__.py (100%) rename pyxx/{debugging => dev}/classes/timer.py (97%) diff --git a/docs/source/_templates/api_reference_class_template.rst b/docs/source/_templates/api_reference_class_template.rst index 37bef1d..29e123d 100644 --- a/docs/source/_templates/api_reference_class_template.rst +++ b/docs/source/_templates/api_reference_class_template.rst @@ -3,6 +3,7 @@ args BinaryFile ConstantUnitMathConventions + dev exps ndarray np diff --git a/docs/source/api_reference/debugging.rst b/docs/source/api_reference/dev.rst similarity index 71% rename from docs/source/api_reference/debugging.rst rename to docs/source/api_reference/dev.rst index fa35373..f229cc9 100644 --- a/docs/source/api_reference/debugging.rst +++ b/docs/source/api_reference/dev.rst @@ -1,9 +1,9 @@ -pyxx.debugging -============== +pyxx.dev +======== -.. automodule:: pyxx.debugging +.. automodule:: pyxx.dev -.. currentmodule:: pyxx.debugging +.. currentmodule:: pyxx.dev Code Profiling diff --git a/docs/source/api_reference/index.rst b/docs/source/api_reference/index.rst index ce33e23..79193f9 100644 --- a/docs/source/api_reference/index.rst +++ b/docs/source/api_reference/index.rst @@ -21,7 +21,7 @@ Package Modules --------------- - :py:mod:`pyxx.arrays` -- Code for processing lists, tuples, or other array-formatted data -- :py:mod:`pyxx.debugging` -- Code for to assist in debugging and analyzing Python code +- :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 8717d19..cb6a681 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -35,7 +35,7 @@ reducing the need to duplicate code between projects. Overview api_reference/arrays - api_reference/debugging + api_reference/dev api_reference/files api_reference/numbers api_reference/strings diff --git a/pyxx/__init__.py b/pyxx/__init__.py index 58b58cd..fb89e4d 100644 --- a/pyxx/__init__.py +++ b/pyxx/__init__.py @@ -6,7 +6,7 @@ # PACKAGE MODULES ------------------------------------------------------------ from . import arrays -from . import debugging +from . import dev from . import files from . import numbers from . import strings diff --git a/pyxx/debugging/__init__.py b/pyxx/dev/__init__.py similarity index 68% rename from pyxx/debugging/__init__.py rename to pyxx/dev/__init__.py index 698ee36..f46d772 100644 --- a/pyxx/debugging/__init__.py +++ b/pyxx/dev/__init__.py @@ -1,6 +1,6 @@ """**Functions and utilities to assist in debugging or analyzing Python code** -The :py:mod:`pyxx.debugging` module is intended to provide commonly used tools +The :py:mod:`pyxx.dev` module is intended to provide commonly used tools or scripts that can be useful when debugging Python code. """ diff --git a/pyxx/debugging/classes/__init__.py b/pyxx/dev/classes/__init__.py similarity index 100% rename from pyxx/debugging/classes/__init__.py rename to pyxx/dev/classes/__init__.py diff --git a/pyxx/debugging/classes/timer.py b/pyxx/dev/classes/timer.py similarity index 97% rename from pyxx/debugging/classes/timer.py rename to pyxx/dev/classes/timer.py index 8a55d48..78871a2 100644 --- a/pyxx/debugging/classes/timer.py +++ b/pyxx/dev/classes/timer.py @@ -26,7 +26,7 @@ class TimeIt: .. code-block:: python - >>> from pyxx.debugging import TimeIt + >>> 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 @@ -37,7 +37,7 @@ class TimeIt: .. code-block:: python - >>> from pyxx.debugging import TimeIt + >>> from pyxx.dev import TimeIt >>> import time >>> timer = TimeIt(print_duration=False) >>> with timer: From a9dc750a3f433dfe3bed007ecaec2b0c8030bc27 Mon Sep 17 00:00:00 2001 From: Nathan Hess <63890205+nathan-hess@users.noreply.github.com> Date: Tue, 12 Dec 2023 15:37:45 +0000 Subject: [PATCH 4/7] Change message text --- pyxx/dev/classes/timer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyxx/dev/classes/timer.py b/pyxx/dev/classes/timer.py index 78871a2..accd59b 100644 --- a/pyxx/dev/classes/timer.py +++ b/pyxx/dev/classes/timer.py @@ -48,7 +48,7 @@ class TimeIt: """ def __init__(self, print_duration: bool = True, units: str = 's', - message: str = 'Command duration: {time} {units}') -> None: + message: str = 'Code duration: {time} {units}') -> None: """Creates a new context manager for measuring the duration of a code block Parameters @@ -62,7 +62,7 @@ def __init__(self, print_duration: bool = True, units: str = 's', message : str, optional Only applicable if ``print_duration`` is ``True``. The message template to display the duration (default is - ``'Command duration: {time} {units}'``). The ``{time}`` and + ``'Code duration: {time} {units}'``). The ``{time}`` and ``{units}`` placeholders will be replaced by the duration and units, respectively """ From 0908402eb1f958afd82c5c8983205981109d0c60 Mon Sep 17 00:00:00 2001 From: Nathan Hess <63890205+nathan-hess@users.noreply.github.com> Date: Tue, 12 Dec 2023 16:54:55 +0000 Subject: [PATCH 5/7] Add unit tests for `pyxx.dev.TimeIt` --- tests/__init__.py | 1 + tests/dev/__init__.py | 1 + tests/dev/classes/__init__.py | 1 + tests/dev/classes/test_timer.py | 56 +++++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+) create mode 100644 tests/dev/__init__.py create mode 100644 tests/dev/classes/__init__.py create mode 100644 tests/dev/classes/test_timer.py 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 From 3b1023d371187a2bde95652f33080edf0c18c4d0 Mon Sep 17 00:00:00 2001 From: Nathan Hess <63890205+nathan-hess@users.noreply.github.com> Date: Tue, 12 Dec 2023 12:02:34 -0500 Subject: [PATCH 6/7] Fix linter warning --- pyxx/dev/classes/timer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyxx/dev/classes/timer.py b/pyxx/dev/classes/timer.py index accd59b..cd489ca 100644 --- a/pyxx/dev/classes/timer.py +++ b/pyxx/dev/classes/timer.py @@ -103,6 +103,6 @@ def duration(self, units: str = 's') -> float: units : str The units in which to return the duration (default is ``'s'``) """ - return self.__unit_converter.convert( + return float(self.__unit_converter.convert( quantity=self.__dt_s, from_unit='s', to_unit=units - ) + )) From 17e505c438e1cadf8c9f245c9e699b0341f4a01e Mon Sep 17 00:00:00 2001 From: Nathan Hess <63890205+nathan-hess@users.noreply.github.com> Date: Tue, 12 Dec 2023 15:09:56 -0500 Subject: [PATCH 7/7] Revise description --- pyxx/dev/classes/timer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyxx/dev/classes/timer.py b/pyxx/dev/classes/timer.py index cd489ca..9d82420 100644 --- a/pyxx/dev/classes/timer.py +++ b/pyxx/dev/classes/timer.py @@ -33,7 +33,8 @@ class TimeIt: ... time.sleep(1) Execution time: 1000.10 ms - Time a code block and store the duration in a variable: + Time a code block and access the duration in Python code following the + code block: .. code-block:: python