Skip to content

Commit

Permalink
[Merge] [#80] Create context manager for timing code blocks
Browse files Browse the repository at this point in the history
  • Loading branch information
nathan-hess authored Dec 12, 2023
2 parents 9462eda + 17e505c commit 61fa0ed
Show file tree
Hide file tree
Showing 12 changed files with 204 additions and 0 deletions.
2 changes: 2 additions & 0 deletions docs/source/_templates/api_reference_class_template.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
args
BinaryFile
ConstantUnitMathConventions
dev
exps
ndarray
np
num
TextFile
TimeIt
TypedList
TypedListWithID
UnitConverter
Expand Down
19 changes: 19 additions & 0 deletions docs/source/api_reference/dev.rst
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions docs/source/api_reference/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ reducing the need to duplicate code between projects.

Overview <api_reference/index>
api_reference/arrays
api_reference/dev
api_reference/files
api_reference/numbers
api_reference/strings
Expand Down
1 change: 1 addition & 0 deletions pyxx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

# PACKAGE MODULES ------------------------------------------------------------
from . import arrays
from . import dev
from . import files
from . import numbers
from . import strings
Expand Down
7 changes: 7 additions & 0 deletions pyxx/dev/__init__.py
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions pyxx/dev/classes/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""This module contains classes that can be used to assist in debugging
Python code.
"""

from .timer import TimeIt
109 changes: 109 additions & 0 deletions pyxx/dev/classes/timer.py
Original file line number Diff line number Diff line change
@@ -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
))
1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 *
Expand Down
1 change: 1 addition & 0 deletions tests/dev/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .classes import *
1 change: 1 addition & 0 deletions tests/dev/classes/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .test_timer import *
56 changes: 56 additions & 0 deletions tests/dev/classes/test_timer.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 61fa0ed

Please sign in to comment.