Skip to content

Instantly share code, notes, and snippets.

@tomazursic
Last active June 3, 2022 01:32
Show Gist options
  • Save tomazursic/0247866ec7f030150c833e9a446efddf to your computer and use it in GitHub Desktop.
Save tomazursic/0247866ec7f030150c833e9a446efddf to your computer and use it in GitHub Desktop.
Python boilerplate
"""
This helper provides logging setup.
The call `logger.info("hello")` prints log messages in this format:
Usage:
from logger import setup_logger
logger = setup_logger()
logger.info("message")
In order to also log to a file, just add a `logfile` parameter:
logger = setup_logger(logfile="/tmp/test.log")
The default loglevel is `logging.DEBUG`. You can set it with the
parameter `level`.
"""
import sys
import os
import logging
import logging.handlers
DIR_PATH = os.path.dirname(os.path.realpath(__file__))
def setup_logger(name=__name__, logfile=False, level=logging.DEBUG):
"""
A utility function that you can call to easily set up logging to the
console and optionally to a file.
"""
main_logger = logging.getLogger(name)
formatter = logging.Formatter(
fmt='%(asctime)s.%(msecs)03d %(levelname)-8s %(module)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S')
handler_stream = logging.StreamHandler(sys.stdout)
handler_stream.setFormatter(formatter)
main_logger.addHandler(handler_stream)
if logfile:
handler_file = logging.handlers.RotatingFileHandler(
DIR_PATH + "/pypktfwd.log", maxBytes=2**24, backupCount=5)
handler_file.setFormatter(formatter)
main_logger.addHandler(handler_file)
main_logger.setLevel(level)
return main_logger
"""
This helper provides a versatile yet easy to use and beautiful
logging setup. You can use it to log to the console and optionally
to a logfile.
The formatter is heavily inspired by the Tornado web framework,
licensed under the Apache 2.0 license.
The call `logger.info("hello")` prints log messages in this format:
[I 170213 15:02:00 test:203] hello
Usage:
from logger_colors import setup_logger
logger = setup_logger()
logger.info("message")
In order to also log to a file, just add a `logfile` parameter:
logger = setup_logger(logfile="/tmp/test.log")
The default loglevel is `logging.DEBUG`. You can set it with the
parameter `level`.
"""
import sys
import logging
try:
import curses # type: ignore
except ImportError:
curses = None
# Python 2+3 compatibility settings for logger
bytes_type = bytes
if sys.version_info >= (3, ):
unicode_type = str
basestring_type = str
xrange = range
else:
# The names unicode and basestring don't exist in py3 so silence flake8.
unicode_type = unicode # noqa
basestring_type = basestring # noqa
def setup_logger(name=__name__, logfile=None, level=logging.DEBUG):
"""
A utility function that you can call to easily set up logging to the
console and optionally to a file. No hassles.
"""
logger = logging.getLogger(name)
logger.propagate = False
logger.setLevel(level)
# Remove old handlers to allow updating settings
for handler in list(logger.handlers):
logger.removeHandler(handler)
# create console handler
stream_handler = logging.StreamHandler()
stream_handler.setLevel(level) # propagate all messages
# add the formatter to the handler
# formatter = logging.Formatter('%(name)s - %(asctime)-15s - %(levelname)s: %(message)s');
formatter = LogFormatter()
stream_handler.setFormatter(formatter)
# setup logger and add the handlers
logger.addHandler(stream_handler)
if logfile:
filehandler = logging.FileHandler(logfile)
filehandler.setLevel(logging.NOTSET)
filehandler.setFormatter(formatter)
logger.addHandler(filehandler)
# logger.debug("logger set up. level=%d", level)
return logger
class LogFormatter(logging.Formatter):
"""
Log formatter used in Tornado. Key features of this formatter are:
* Color support when logging to a terminal that supports it.
* Timestamps on every log line.
* Robust against str/bytes encoding problems.
"""
DEFAULT_FORMAT = '%(color)s[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d]%(end_color)s %(message)s'
DEFAULT_DATE_FORMAT = '%y%m%d %H:%M:%S'
DEFAULT_COLORS = {
logging.DEBUG: 4, # Blue
logging.INFO: 2, # Green
logging.WARNING: 3, # Yellow
logging.ERROR: 1, # Red
}
def __init__(self,
color=True,
fmt=DEFAULT_FORMAT,
datefmt=DEFAULT_DATE_FORMAT,
colors=DEFAULT_COLORS):
r"""
:arg bool color: Enables color support.
:arg string fmt: Log message format.
It will be applied to the attributes dict of log records. The
text between ``%(color)s`` and ``%(end_color)s`` will be colored
depending on the level if color support is on.
:arg dict colors: color mappings from logging level to terminal color
code
:arg string datefmt: Datetime format.
Used for formatting ``(asctime)`` placeholder in ``prefix_fmt``.
.. versionchanged:: 3.2
Added ``fmt`` and ``datefmt`` arguments.
"""
logging.Formatter.__init__(self, datefmt=datefmt)
self._fmt = fmt
self._colors = {}
if color and _stderr_supports_color():
# The curses module has some str/bytes confusion in
# python3. Until version 3.2.3, most methods return
# bytes, but only accept strings. In addition, we want to
# output these strings with the logging module, which
# works with unicode strings. The explicit calls to
# unicode() below are harmless in python2 but will do the
# right conversion in python 3.
fg_color = (curses.tigetstr("setaf") or curses.tigetstr("setf") or
"")
if (3, 0) < sys.version_info < (3, 2, 3):
fg_color = unicode_type(fg_color, "ascii")
for levelno, code in colors.items():
self._colors[levelno] = unicode_type(
curses.tparm(fg_color, code), "ascii")
self._normal = unicode_type(curses.tigetstr("sgr0"), "ascii")
else:
self._normal = ''
def format(self, record):
try:
message = record.getMessage()
assert isinstance(message,
basestring_type) # guaranteed by logging
# Encoding notes: The logging module prefers to work with character
# strings, but only enforces that log messages are instances of
# basestring. In python 2, non-ascii bytestrings will make
# their way through the logging framework until they blow up with
# an unhelpful decoding error (with this formatter it happens
# when we attach the prefix, but there are other opportunities for
# exceptions further along in the framework).
#
# If a byte string makes it this far, convert it to unicode to
# ensure it will make it out to the logs. Use repr() as a fallback
# to ensure that all byte strings can be converted successfully,
# but don't do it by default so we don't add extra quotes to ascii
# bytestrings. This is a bit of a hacky place to do this, but
# it's worth it since the encoding errors that would otherwise
# result are so useless (and tornado is fond of using utf8-encoded
# byte strings whereever possible).
record.message = _safe_unicode(message)
except Exception as e:
record.message = "Bad message (%r): %r" % (e, record.__dict__)
record.asctime = self.formatTime(record, self.datefmt)
if record.levelno in self._colors:
record.color = self._colors[record.levelno]
record.end_color = self._normal
else:
record.color = record.end_color = ''
formatted = self._fmt % record.__dict__
if record.exc_info:
if not record.exc_text:
record.exc_text = self.formatException(record.exc_info)
if record.exc_text:
# exc_text contains multiple lines. We need to _safe_unicode
# each line separately so that non-utf8 bytes don't cause
# all the newlines to turn into '\n'.
lines = [formatted.rstrip()]
lines.extend(
_safe_unicode(ln) for ln in record.exc_text.split('\n'))
formatted = '\n'.join(lines)
return formatted.replace("\n", "\n ")
def _stderr_supports_color():
color = False
if curses and hasattr(sys.stderr, 'isatty') and sys.stderr.isatty():
try:
curses.setupterm()
if curses.tigetnum("colors") > 0:
color = True
except Exception:
pass
return color
_TO_UNICODE_TYPES = (unicode_type, type(None))
def to_unicode(value):
"""
Converts a string argument to a unicode string.
If the argument is already a unicode string or None, it is returned
unchanged. Otherwise it must be a byte string and is decoded as utf8.
"""
if isinstance(value, _TO_UNICODE_TYPES):
return value
if not isinstance(value, bytes):
raise TypeError(
"Expected bytes, unicode, or None; got %r" % type(value))
return value.decode("utf-8")
def _safe_unicode(s):
try:
return to_unicode(s)
except UnicodeDecodeError:
return repr(s)
if __name__ == "__main__":
logger = setup_logger()
logger.info("hello")
!/usr/bin/env python3
"""
Module Docstring
"""
__author__ = "Your Name"
__version__ = "0.1.0"
__license__ = "MIT"
import argparse
def main(args):
""" Main entry point of the app """
print("hello world")
print(args)
if __name__ == "__main__":
""" This is executed when run from the command line """
parser = argparse.ArgumentParser()
# Required positional argument
parser.add_argument("arg", help="Required positional argument")
# Optional argument flag which defaults to False
parser.add_argument("-f", "--flag", action="store_true", default=False)
# Optional argument which requires a parameter (eg. -d test)
parser.add_argument("-n", "--name", action="store", dest="name")
# Optional verbosity counter (eg. -v, -vv, -vvv, etc.)
parser.add_argument(
"-v",
"--verbose",
action="count",
default=0,
help="Verbosity (-v, -vv, etc)")
# Specify output of "--version"
parser.add_argument(
"--version",
action="version",
version="%(prog)s (version {version})".format(version=__version__))
args = parser.parse_args()
main(args)
"""
You can auto-discover and run all tests with this command:
py.test
Documentation: https://docs.pytest.org/en/latest/
requirements:
pytest>=3.0.7
"""
def inc(x):
return x + 1
def test_answer():
assert inc(3) == 4
"""
This file demonstrates common uses for the Python unittest module
https://docs.python.org/3/library/unittest.html
"""
import random
import unittest
class TestSequenceFunctions(unittest.TestCase):
""" This is one of potentially many TestCases """
def setUp(self):
self.seq = list(range(10))
def test_shuffle(self):
""" make sure the shuffled sequence does not lose any elements """
random.shuffle(self.seq)
self.seq.sort()
self.assertEqual(self.seq, list(range(10)))
# should raise an exception for an immutable sequence
self.assertRaises(TypeError, random.shuffle, (1, 2, 3))
def test_choice(self):
""" test a choice """
element = random.choice(self.seq)
self.assertTrue(element in self.seq)
def test_sample(self):
""" test that an exception is raised """
with self.assertRaises(ValueError):
random.sample(self.seq, 20)
for element in random.sample(self.seq, 5):
self.assertTrue(element in self.seq)
if __name__ == '__main__':
unittest.main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment