Source code for opinf.utils._timer
# utils/_timer.py
"""Context manager for timing blocks of code."""
__all__ = [
"TimedBlock",
]
import io
import os
import sys
import time
import signal
import logging
[docs]
class TimedBlock:
r"""Context manager for timing a block of code and reporting the timing.
Parameters
----------
message : str
Message to log / print.
timelimit : int
Number of seconds to wait before raising an error.
Floats are rounded down to an integer.
Warnings
--------
This context manager may only function on Linux/Unix machines
(Windows is not currently supported).
Examples
--------
>>> import time
>>> import opinf
Without a time limit.
>>> with opinf.utils.TimedBlock():
... # Code to be timed
... time.sleep(2)
Running code block...done in 2.00 s.
With a custom message.
>>> with opinf.utils.TimedBlock("This is a test"):
... time.sleep(3)
This is a test...done in 3.00 s.
With a time limit.
>>> with opinf.utils.TimedBlock("Another test", timelimit=3):
... # Code to be timed and halted within the specified time limit.
... i = 0
... while True:
... i += 1
Another test...
TimeoutError: TIMED OUT after 3.00s.
Set up a logfile to record messages to.
>>> opinf.utils.TimedBlock.setup_logfile("log.log")
Logging to '/path/to/current/folder/log.log'
``TimedBlock()`` will now write to the log file as well as print to screen.
>>> with opinf.utils.TimedBlock("logfile test"):
... time.sleep(1)
logfile test...done in 1.00 s.
>>> with open("log.log", "r") as infile:
... print(infile.read().strip())
INFO: logfile test...done in 1.001150 s.
Turn off print statements (but keep logging).
>>> opinf.utils.TimedBlock.verbose = False
>>> with opinf.utils.TimedBlock("not printed to the screen"):
... time.sleep(1)
>>> with open("log.log", "r") as infile:
... print(infile.read().strip())
INFO: logfile test...done in 1.001150 s.
INFO: not printed to the screen...done in 1.002232 s.
Capture the time elapsed for later use.
>>> with opinf.utils.TimedBlock("how long?") as timer:
... time.sleep(2)
>>> timer.elapsed
2.002866268157959
"""
verbose = True
rebuffer = False
formatter = logging.Formatter(
fmt="%(asctime)s %(levelname)s:\t%(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
def __init__(
self,
message: str = "Running code block",
timelimit: int = None,
):
"""Store print/log message."""
self.__original_stdout = sys.stdout
self.__new_buffer = None
self.__front = "\n" if message.endswith("\n") else ""
self.message = message.rstrip()
self.__back = "\n" if "\r" not in message else ""
if timelimit is not None:
timelimit = max(int(timelimit), 1)
self.__timelimit = timelimit
self.__elapsed = None
@property
def timelimit(self):
"""Time limit (in seconds) for the block to complete."""
return self.__timelimit
@property
def elapsed(self):
"""Actual time (in seconds) the block took to complete."""
return self.__elapsed
@staticmethod
def _signal_handler(signum, frame):
raise TimeoutError("timed out!")
def _reset_stdout(self):
text = self.__new_buffer.getvalue()
sys.stdout = self.__original_stdout
print(text, end="", flush=True)
def __enter__(self):
"""Print the message and record the current time."""
if self.rebuffer:
sys.stdout = self.__new_buffer = io.StringIO()
if self.verbose:
print(f"{self.message}...", end=self.__front, flush=True)
self._tic = time.time()
if self.timelimit is not None:
signal.signal(signal.SIGALRM, self._signal_handler)
signal.alarm(self.timelimit)
return self
def __exit__(self, exc_type, exc_value, exc_traceback):
"""Calculate and report the elapsed time."""
self._toc = time.time()
if self.timelimit is not None:
signal.alarm(0)
elapsed = self._toc - self._tic
if exc_type: # Report an exception if present.
if self.timelimit is not None and exc_type is TimeoutError:
print(flush=True)
report = f"TIMED OUT after {elapsed:.2f} s."
logging.info(f"{self.message}...{report}")
if self.rebuffer:
self._reset_stdout()
raise TimeoutError(report)
print(f"{exc_type.__name__}: {exc_value}")
logging.info(self.message)
logging.error(
f"({exc_type.__name__}) {exc_value} "
f"(raised after {elapsed:.6f} s)"
)
if self.rebuffer:
self._reset_stdout()
raise
else: # If no exception, report execution time.
if self.verbose:
print(f"done in {elapsed:.2f} s.", flush=True, end=self.__back)
logging.info(f"{self.message}...done in {elapsed:.6f} s.")
self.__elapsed = elapsed
if self.rebuffer:
self._reset_stdout()
return
[docs]
@classmethod
def add_logfile(cls, logfile: str = "log.log") -> None:
"""Instruct :class:`TimedBlock` to log messages to the ``logfile``.
Parameters
----------
logfile : str
File to log to.
"""
logger = logging.getLogger()
logpath = os.path.abspath(logfile)
# Check that we aren't already logging to this file.
for handler in logger.handlers:
if (
isinstance(handler, logging.FileHandler)
and os.path.abspath(handler.baseFilename) == logpath
):
if cls.verbose:
print(f"Already logging to {logpath}")
return
# Add a new handler for this file.
newhandler = logging.FileHandler(logpath, "a")
newhandler.setFormatter(cls.formatter)
newhandler.setLevel(logging.INFO)
logger.setLevel(logging.INFO)
logger.addHandler(newhandler)
if cls.verbose:
print(f"Logging to '{os.path.abspath(logfile)}'")