diff --git a/README.rst b/README.rst index 26e7919..277fa7e 100644 --- a/README.rst +++ b/README.rst @@ -7,7 +7,7 @@ INSTALLATION pytictoc can be installed and updated via conda or pip. **pip** :: - + pip install pytictoc pip install pytictoc --upgrade @@ -19,7 +19,7 @@ pytictoc can be installed and updated via conda or pip. ============= USAGE -============= +============= Basic usage: :: @@ -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) @@ -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. @@ -58,7 +70,7 @@ 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: :: @@ -66,9 +78,14 @@ To see which function is being used: :: >>pytictoc.default_timer -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 + +Or by passing a different timer function into an object's constructor: :: + + >>import time + >>pytictoc.TicToc(timer=time.clock) diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pytictoc.py b/pytictoc.py index 031ed8d..c80e0ab 100644 --- a/pytictoc.py +++ b/pytictoc.py @@ -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) diff --git a/testing/__init__.py b/testing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/testing/pytictoc_test.py b/testing/pytictoc_test.py new file mode 100644 index 0000000..9d5f94b --- /dev/null +++ b/testing/pytictoc_test.py @@ -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 [""] + 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() diff --git a/testing/test_pytictoc.py b/testing/test_pytictoc.py index 0e0798b..36b1049 100644 --- a/testing/test_pytictoc.py +++ b/testing/test_pytictoc.py @@ -8,45 +8,48 @@ if sys.version_info.major == 3: from importlib import reload - -os.chdir(r'C:\Users\ecfne\Documents\Eric\Coding\Python\pytictoc') + +os.chdir(os.path.dirname(os.getcwd())) try: reload(pytictoc) except (NameError, ImportError): import pytictoc - + + def waste_time(num_reps=1000000): for i in range(num_reps): spam = i+1 eggs = spam**2 pancakes = eggs % 3 - + + def main(): print('\npytictoc file:') print(pytictoc.__file__ + '\n') - + t = pytictoc.TicToc() - + t.tic() waste_time() t.toc() - + with pytictoc.TicToc(): waste_time() - + t.toc('It has been', restart=True) t.toc() - + spam = t.tocvalue() print(spam) - + waste_time() - + spam = t.tocvalue(restart=True) print(spam) t.toc() - + + if __name__ == '__main__': main()