Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add default messages, stream destinations, and timer mocking to the TicToc object. #5

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
29 changes: 23 additions & 6 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ INSTALLATION
pytictoc can be installed and updated via conda or pip.

**pip** ::

pip install pytictoc
pip install pytictoc --upgrade

Expand All @@ -19,7 +19,7 @@ pytictoc can be installed and updated via conda or pip.

=============
USAGE
=============
=============

Basic usage: ::

Expand All @@ -35,6 +35,14 @@ A string passed to the toc method changes the printed message. This can be usefu
>> t.toc('Section 1 took')
Section 1 took 16.494467 seconds.

The default message can also be passed in the constructor, for easier usage with context managers: ::

>> with TicToc(default_msg='Section 1 took'):
>> ...
Section 1 took 16.494467 seconds.

The string passed to toc will override the string in the constructor if both are present.

An optional keyword argument restarts the timer (equivalent to t.tic()) after reporting the time elapsed. ::

>> t.toc(restart=True)
Expand All @@ -48,8 +56,12 @@ If you want to return the time elapsed to a variable rather than printing it, us
>>spam
20.156261717544602

You can also pass in an alternative stream to print to: ::

>> t = TicToc(stream=mystream)

The TicToc class can be used within a context manager as an alternative way to time a section of code. The time taken to run the code inside the with statement will be reported on exit. ::

>>with TicToc():
>> spam = [x+1 for x in range(10000)]
Elapsed time is 0.002343 seconds.
Expand All @@ -58,17 +70,22 @@ The TicToc class can be used within a context manager as an alternative way to t
Determining and setting the timer
------------------------------------

pytictoc uses timeit.default_timer to time code. On Python 3.3 and later, this is an alias for time.perf_counter. On earlier versions of Python it is an alias for the most precise timer for a given system.
pytictoc uses timeit.default_timer to time code. On Python 3.3 and later, this is an alias for time.perf_counter. On earlier versions of Python it is an alias for the most precise timer for a given system.

To see which function is being used: ::

>>import pytictoc
>>pytictoc.default_timer
<function time.perf_counter>

You can change the timer by simple assignment. ::
You can change the timer by simple assignment: ::

>>import time
>>pytictoc.default_timer = time.clock
>>pytictoc.default_timer
<function time.clock>

Or by passing a different timer function into an object's constructor: ::

>>import time
>>pytictoc.TicToc(timer=time.clock)
Empty file added __init__.py
Empty file.
87 changes: 57 additions & 30 deletions pytictoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,68 +10,95 @@
__version__ = '1.5.2'
__version_date__ = '23 April 2021'


import sys
from timeit import default_timer


class TicToc(object):

"""
Replicate the functionality of MATLAB's tic and toc.

#Methods
TicToc.tic() #start or re-start the timer
TicToc.toc() #print elapsed time since timer start
TicToc.tocvalue() #return floating point value of elapsed time since timer start

#Attributes
TicToc.start #Time from timeit.default_timer() when t.tic() was last called
TicToc.end #Time from timeit.default_timer() when t.toc() or t.tocvalue() was last called
TicToc.start #Time from timeit.self.timer() when t.tic() was last called
TicToc.end #Time from timeit.self.timer() when t.toc() or t.tocvalue() was last called
TicToc.elapsed #t.end - t.start; i.e., time elapsed from t.start when t.toc() or t.tocvalue() was last called
"""

def __init__(self):
"""Create instance of TicToc class."""

def __init__(self, default_msg='Elapsed time is', stream=None, timer=None):
"""
Create instance of TicToc class.

Optional arguments:
default_msg - String to replace default message of 'Elapsed time is'
stream - Stream to write the timer message to
timer - Function that returns the current time when called
"""

self.start = float('nan')
self.end = float('nan')
self.elapsed = float('nan')

self.default_msg = default_msg
self.stream = stream or sys.stdout

# Lazily accessing default timer allows the user to override it later per the docs.
self.timer = timer or (lambda: default_timer())
assert self.timer is not None
assert self.default_msg is not None
assert self.stream is not None

def tic(self):
"""Start the timer."""
self.start = default_timer()
def toc(self, msg='Elapsed time is', restart=False):
self.start = self.timer()

def toc(self, msg=None, restart=False):
"""
Report time elapsed since last call to tic().

Optional arguments:
msg - String to replace default message of 'Elapsed time is'
msg - String to replace default message initialized with the object
restart - Boolean specifying whether to restart the timer
"""
self.end = default_timer()
self.elapsed = self.end - self.start
print('%s %f seconds.' % (msg, self.elapsed))
if restart:
self.start = default_timer()
if msg is None:
msg = self.default_msg

self.tocvalue(restart=restart)
print(self._tocmsg(msg, self.elapsed), file=self.stream)

def tocvalue(self, restart=False):
"""
Return time elapsed since last call to tic().

Optional argument:
restart - Boolean specifying whether to restart the timer
"""
self.end = default_timer()
self.end = self.timer()
self.elapsed = self.end - self.start
if restart:
self.start = default_timer()
self.start = self.timer()
return self.elapsed


def _tocmsg(self, prefix_msg, elapsed):
"""
Return full message that will be output on toc().

Arguments:
prefix_msg: preamble to the timer output
elapsed: time elapsed
"""
return '%s %f seconds.' % (prefix_msg, elapsed)

def __enter__(self):
"""Start the timer when using TicToc in a context manager."""
self.start = default_timer()
self.start = self.timer()

def __exit__(self, *args):
"""On exit, pring time elapsed since entering context manager."""
self.end = default_timer()
"""On exit, print time elapsed since entering context manager."""
self.end = self.timer()
self.elapsed = self.end - self.start
print('Elapsed time is %f seconds.' % self.elapsed)
print(self._tocmsg(self.default_msg, self.elapsed), file=self.stream)
Empty file added testing/__init__.py
Empty file.
152 changes: 152 additions & 0 deletions testing/pytictoc_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import contextlib
import re
import time
import unittest
from abc import ABCMeta
from io import StringIO

from .. import pytictoc as ptt


class BaseTicTocTest(unittest.TestCase, metaclass=ABCMeta):

def setUp(self):
self.mock_time = 0
self.stream = StringIO()
self.mock_timer = lambda: self.mock_time
self.tt = ptt.TicToc(default_msg="Time:", stream=self.stream, timer=self.mock_timer)

def output(self):
return self.stream.getvalue()

def times_passed(self):
return re.findall(r'([-\d\.,]+) seconds.$', self.output(), re.MULTILINE)

def last_time_passed(self):
times_passed = self.times_passed()
self.assertGreater(len(times_passed), 0, self.err_msg())
return float(times_passed[-1])

def err_msg(self):
raw_output = self.output().splitlines() or ["<null>"]
return "Last output: %s" % raw_output[-1]

def _run_tictoc(self, start_time, end_time, work_function=None, **toc_args):
self.mock_time = start_time
self.tt.tic()
if work_function:
work_function()
self.mock_time = end_time
self.tt.toc(**toc_args)

def tictoc(self, start_time=0, end_time=0, **toc_args):
self._run_tictoc(start_time, end_time, **toc_args)
self.assertTime(start_time, self.tt.start)
self.assertTime(end_time, self.tt.end)
self.assertTime(end_time - start_time, self.last_time_passed())
self.assertTime(end_time - start_time, self.tt.tocvalue())

def assertTime(self, expected, actual, *args, **kwargs):
# Measure equality to 6 decimal places to avoid float precision errors
self.assertAlmostEqual(expected, actual, 6, self.err_msg(), *args, **kwargs)

def test_increment(self):
self.tictoc(0, 1)
self.tictoc(0, 2)
self.tictoc(5, 9)
self.tictoc(-9, 20)

def test_no_time_passed(self):
self.tictoc(0, 0)
self.tictoc(1, 1)
self.tictoc(-5, -5)

def test_decrement(self):
self.tictoc(1, 0)
self.tictoc(5, -5)
self.tictoc(-100, -300)

def test_decimal(self):
self.tictoc(0.3, 3.6)
self.tictoc(0.3, -3.6)
self.tictoc(0.34, 0)
self.tictoc(0, -0.25)
self.tictoc(-7.9, -20)
self.tictoc(4, 25.89)

def test_default_msg(self):
self.tt = ptt.TicToc(stream=self.stream, timer=self.mock_timer)
self.tictoc(4, 6)
self.assertIn("Elapsed time is 2.0", self.output())

def test_default_stream_is_stdout(self):
with contextlib.redirect_stdout(self.stream):
self.tt = ptt.TicToc(default_msg="Time:", timer=self.mock_timer)
self.tictoc(-4, 8.2)
self.tictoc(8, 16)

def test_default_invocation_works(self):
with contextlib.redirect_stdout(self.stream):
self.tt = ptt.TicToc()

# I suggest keeping a negative interval to confirm the actual time is used
self._run_tictoc(0, -1, lambda: time.sleep(0.01))

# NB: Prior to Python 3.5 the sleep could end early. Will still work with zero.
self.assertGreaterEqual(self.last_time_passed(), 0)

def test_setting_default_timer(self):
self.tt = ptt.TicToc(stream=self.stream)
old_timer = ptt.default_timer
try:
ptt.default_timer = self.mock_timer
self.tictoc(4, 60)
self.tictoc(5, -2)
finally:
ptt.default_timer = old_timer

def test_not_setting_default_timer(self):
self.tt = ptt.TicToc(stream=self.stream)
self._run_tictoc(5, -2)
self.assertGreaterEqual(self.last_time_passed(), 0)


class TicTocTest(BaseTicTocTest):
def test_msg(self):
self.tictoc(1, 6, msg="Tic Toc")
self.assertIn("Tic Toc 5.0", self.output())

def test_msg_in_stdout(self):
with contextlib.redirect_stdout(self.stream):
self.tt = ptt.TicToc(default_msg="Time:", timer=self.mock_timer)
self.tictoc(1, -7, msg="Tic Toc")
self.assertIn("Tic Toc -8.0", self.output())
self.assertNotIn("Time:", self.output())

def test_restart(self):
self._run_tictoc(4, 6, restart=True)
self.assertTime(6, self.tt.start)


class TicTocTestWithContextManager(BaseTicTocTest):
def _run_tictoc(self, start_time, end_time, **toc_args):
if toc_args:
raise unittest.SkipTest("Toc arguments aren't supported with context managers")

self.mock_time = start_time
with self.tt:
self.mock_time = end_time

def test_default_invocation_works(self):
with contextlib.redirect_stdout(self.stream):
self.tt = ptt.TicToc()
with self.tt:
time.sleep(0.01)
# NB: Prior to Python 3.5 the sleep could end early. Will still work with zero.
self.assertGreaterEqual(self.last_time_passed(), 0)


del BaseTicTocTest # Don't run the tests in the abstract class.

if __name__ == '__main__':
unittest.main()
Loading