summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Stender <debian@danielstender.com>2015-12-30 19:25:18 +0100
committerDaniel Stender <debian@danielstender.com>2015-12-30 19:25:18 +0100
commit63226db7ed2b73d492b4e8f61037d221a293a919 (patch)
treecaba34bec3fcf50317ffe0d334eec058fa71f512
parent8de21ef314771149763811284eaa62fca7f763d2 (diff)
parent404dfc09250a5bb3e820347ff1ecdfb9e9f7fb85 (diff)
record new upstream branch created by importing pytest-catchlog_1.2.1.orig.tar.xz and merge it
-rw-r--r--.travis.yml20
-rw-r--r--CHANGES.rst48
-rw-r--r--MANIFEST.in1
-rw-r--r--README.rst42
-rw-r--r--debian/.git-dpm14
-rw-r--r--pytest_catchlog.py287
-rwxr-xr-xsetup.py9
-rw-r--r--tasks.py188
-rw-r--r--test_pytest_catchlog.py156
-rw-r--r--tox.ini2
10 files changed, 622 insertions, 145 deletions
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..db995e2
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,20 @@
+sudo: false
+language: python
+python:
+ - "2.6"
+ - "2.7"
+ - "3.2"
+ - "3.3"
+ - "3.4"
+ - "3.5"
+ - "pypy"
+ - "pypy3"
+
+install:
+ - pip install -e .
+script:
+ - py.test
+
+cache:
+ directories:
+ - $HOME/.cache/pip/http
diff --git a/CHANGES.rst b/CHANGES.rst
index b7d938e..ed164ab 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -3,6 +3,53 @@ Changelog
List of notable changes between pytest-catchlog releases.
+.. %UNRELEASED_SECTION%
+
+`Unreleased`_
+-------------
+
+Yet to be released.
+
+
+`1.2.1`_
+-------------
+
+Released on 2015-12-07.
+
+- [Bugfix] #18 - Allow ``caplog.records()`` to be modified. Thanks to Eldar Abusalimov for the PR and Marco Nenciarini for reporting the issue.
+- [Bugfix] #15 #17 - Restore Python 2.6 compatibility. (Thanks to Marco Nenciarini!)
+
+.. attention::
+ Deprecation warning: the following objects (i.e. functions, properties)
+ are slated for removal in the next major release.
+
+ - ``caplog.at_level`` and ``caplog.set_level`` should be used instead of
+ ``caplog.atLevel`` and ``caplog.setLevel``.
+
+ The methods ``caplog.atLevel`` and ``caplog.setLevel`` are still
+ available but deprecated and not supported since they don't follow
+ the PEP8 convention for method names.
+
+ - ``caplog.text``, ``caplog.records`` and
+ ``caplog.record_tuples`` were turned into properties.
+ They still can be used as regular methods for backward compatibility,
+ but that syntax is considered deprecated and scheduled for removal in
+ the next major release.
+
+
+Version 1.2
+-----------
+
+Released on 2015-11-08.
+
+- [Feature] #6 - Configure logging message and date format through ini file.
+- [Feature] #7 - Also catch logs from setup and teardown stages.
+- [Feature] #7 - Replace deprecated ``__multicall__`` use to support future Py.test releases.
+- [Feature] #11 - reintroduce ``setLevel`` and ``atLevel`` to retain backward compatibility with pytest-capturelog. Also the members ``text``, ``records`` and ``record_tuples`` of the ``caplog`` fixture can be used as properties now.
+
+Special thanks for this release goes to Eldar Abusalimov. He provided all of the changed features.
+
+
Version 1.1
-----------
@@ -20,4 +67,3 @@ Released on 2014-12-08.
- Add ``record_tuples`` for comparing recorded log entries against expected
log entries with their logger name, severity and formatted message.
-
diff --git a/MANIFEST.in b/MANIFEST.in
index 8ad1455..bc85cc3 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,4 +1,5 @@
include MANIFEST.in Makefile LICENSE.txt README.rst CHANGES.rst setup.cfg
+include test_pytest_catchlog.py
global-exclude *pyc
prune __pycache__
diff --git a/README.rst b/README.rst
index 2287412..bc7fcba 100644
--- a/README.rst
+++ b/README.rst
@@ -30,11 +30,11 @@ Running without options::
Shows failed tests like so::
- -------------------------- Captured log ---------------------------
+ ----------------------- Captured stdlog call ----------------------
test_pytest_catchlog.py 26 INFO text going to logger
- ------------------------- Captured stdout -------------------------
+ ----------------------- Captured stdout call ----------------------
text going to stdout
- ------------------------- Captured stderr -------------------------
+ ----------------------- Captured stderr call ----------------------
text going to stderr
==================== 2 failed in 0.02 seconds =====================
@@ -50,14 +50,26 @@ Running pytest specifying formatting options::
Shows failed tests like so::
- -------------------------- Captured log ---------------------------
+ ----------------------- Captured stdlog call ----------------------
2010-04-10 14:48:44 INFO text going to logger
- ------------------------- Captured stdout -------------------------
+ ----------------------- Captured stdout call ----------------------
text going to stdout
- ------------------------- Captured stderr -------------------------
+ ----------------------- Captured stderr call ----------------------
text going to stderr
==================== 2 failed in 0.02 seconds =====================
+These options can also be customized through a configuration file::
+
+ [pytest]
+ log_format = %(asctime)s %(levelname)s %(message)s
+ log_date_format = %Y-%m-%d %H:%M:%S
+
+Although the same effect could be achieved through the ``addopts`` setting,
+using dedicated options should be preferred since the latter doesn't
+force other developers to have ``pytest-catchlog`` installed (while at
+the same time, ``addopts`` approach would fail with 'unrecognized arguments'
+error). Command line arguments take precedence.
+
Further it is possible to disable reporting logs on failed tests
completely with::
@@ -65,9 +77,9 @@ completely with::
Shows failed tests in the normal manner as no logs were captured::
- ------------------------- Captured stdout -------------------------
+ ----------------------- Captured stdout call ----------------------
text going to stdout
- ------------------------- Captured stderr -------------------------
+ ----------------------- Captured stderr call ----------------------
text going to stderr
==================== 2 failed in 0.02 seconds =====================
@@ -75,7 +87,7 @@ Inside tests it is possible to change the log level for the captured
log messages. This is supported by the ``caplog`` funcarg::
def test_foo(caplog):
- caplog.setLevel(logging.INFO)
+ caplog.set_level(logging.INFO)
pass
By default the level is set on the handler used to catch the log
@@ -83,21 +95,21 @@ messages, however as a convenience it is also possible to set the log
level of any logger::
def test_foo(caplog):
- caplog.setLevel(logging.CRITICAL, logger='root.baz')
+ caplog.set_level(logging.CRITICAL, logger='root.baz')
pass
It is also possible to use a context manager to temporarily change the
log level::
def test_bar(caplog):
- with caplog.atLevel(logging.INFO):
+ with caplog.at_level(logging.INFO):
pass
Again, by default the level of the handler is affected but the level
of any logger can be changed instead with::
def test_bar(caplog):
- with caplog.atLevel(logging.CRITICAL, logger='root.baz'):
+ with caplog.at_level(logging.CRITICAL, logger='root.baz'):
pass
Lastly all the logs sent to the logger during the test run are made
@@ -107,9 +119,9 @@ the contents of a message::
def test_baz(caplog):
func_under_test()
- for record in caplog.records():
+ for record in caplog.records:
assert record.levelname != 'CRITICAL'
- assert 'wally' not in caplog.text()
+ assert 'wally' not in caplog.text
For all the available attributes of the log records see the
``logging.LogRecord`` class.
@@ -121,7 +133,7 @@ given severity and message::
def test_foo(caplog):
logging.getLogger().info('boo %s', 'arg')
- assert caplog.record_tuples() == [
+ assert caplog.record_tuples == [
('root', logging.INFO, 'boo arg'),
]
diff --git a/debian/.git-dpm b/debian/.git-dpm
index 9832ed6..28b2c37 100644
--- a/debian/.git-dpm
+++ b/debian/.git-dpm
@@ -1,11 +1,11 @@
# see git-dpm(1) from git-dpm package
-9f7b3008a9ac46b7780bbc50629e207b4ca365e9
-9f7b3008a9ac46b7780bbc50629e207b4ca365e9
-9f7b3008a9ac46b7780bbc50629e207b4ca365e9
-9f7b3008a9ac46b7780bbc50629e207b4ca365e9
-pytest-catchlog_1.1.orig.tar.gz
-85268b5141fae91f38009ac4863ef80d863d59e2
-6540
+404dfc09250a5bb3e820347ff1ecdfb9e9f7fb85
+404dfc09250a5bb3e820347ff1ecdfb9e9f7fb85
+404dfc09250a5bb3e820347ff1ecdfb9e9f7fb85
+404dfc09250a5bb3e820347ff1ecdfb9e9f7fb85
+pytest-catchlog_1.2.1.orig.tar.xz
+bb8ad2d5d84f9559c06c88016056cdf557417c3a
+9860
debianTag="debian/%e%v"
patchedTag="patched/%e%v"
upstreamTag="upstream/%e%u"
diff --git a/pytest_catchlog.py b/pytest_catchlog.py
index 61cdb94..892fd68 100644
--- a/pytest_catchlog.py
+++ b/pytest_catchlog.py
@@ -1,30 +1,104 @@
# -*- coding: utf-8 -*-
-from __future__ import (absolute_import, print_function,
- unicode_literals, division)
+from __future__ import absolute_import, division, print_function
+import functools
import logging
+from contextlib import closing, contextmanager
+import pytest
import py
-__version__ = '1.1'
+__version__ = '1.2.1'
+
+
+def get_logger_obj(logger=None):
+ """Get a logger object that can be specified by its name, or passed as is.
+
+ Defaults to the root logger.
+ """
+ if logger is None or isinstance(logger, py.builtin._basestring):
+ logger = logging.getLogger(logger)
+ return logger
+
+
+@contextmanager
+def logging_at_level(level, logger=None):
+ """Context manager that sets the level for capturing of logs."""
+ logger = get_logger_obj(logger)
+
+ orig_level = logger.level
+ logger.setLevel(level)
+ try:
+ yield
+ finally:
+ logger.setLevel(orig_level)
+
+
+@contextmanager
+def logging_using_handler(handler, logger=None):
+ """Context manager that safely register a given handler."""
+ logger = get_logger_obj(logger)
+
+ if handler in logger.handlers: # reentrancy
+ # Adding the same handler twice would confuse logging system.
+ # Just don't do that.
+ yield
+ else:
+ logger.addHandler(handler)
+ try:
+ yield
+ finally:
+ logger.removeHandler(handler)
+
+
+@contextmanager
+def catching_logs(handler, filter=None, formatter=None,
+ level=logging.NOTSET, logger=None):
+ """Context manager that prepares the whole logging machinery properly."""
+ logger = get_logger_obj(logger)
+
+ if filter is not None:
+ handler.addFilter(filter)
+ if formatter is not None:
+ handler.setFormatter(formatter)
+ handler.setLevel(level)
+
+ with closing(handler):
+ with logging_using_handler(handler, logger):
+ with logging_at_level(min(handler.level, logger.level), logger):
+
+ yield handler
+
+
+def add_option_ini(parser, option, dest, default=None, help=None):
+ parser.addini(dest, default=default,
+ help='default value for ' + option)
+ parser.getgroup('catchlog').addoption(option, dest=dest, help=help)
+
+def get_option_ini(config, name):
+ ret = config.getoption(name) # 'default' arg won't work as expected
+ if ret is None:
+ ret = config.getini(name)
+ return ret
def pytest_addoption(parser):
"""Add options to control log capturing."""
- group = parser.getgroup('catchlog', 'Log catching.')
+ group = parser.getgroup('catchlog', 'Log catching')
group.addoption('--no-print-logs',
dest='log_print', action='store_false', default=True,
help='disable printing caught logs on failed tests.')
- group.addoption('--log-format',
- dest='log_format',
- default=('%(filename)-25s %(lineno)4d'
- ' %(levelname)-8s %(message)s'),
- help='log format as used by the logging module.')
- group.addoption('--log-date-format',
- dest='log_date_format', default=None,
- help='log date format as used by the logging module.')
+ add_option_ini(parser,
+ '--log-format',
+ dest='log_format', default=('%(filename)-25s %(lineno)4d'
+ ' %(levelname)-8s %(message)s'),
+ help='log format as used by the logging module.')
+ add_option_ini(parser,
+ '--log-date-format',
+ dest='log_date_format', default=None,
+ help='log date format as used by the logging module.')
def pytest_configure(config):
@@ -44,62 +118,44 @@ class CatchLogPlugin(object):
The formatter can be safely shared across all handlers so
create a single one for the entire test session here.
"""
- self.print_logs = config.getvalue('log_print')
- self.formatter = logging.Formatter(config.getvalue('log_format'),
- config.getvalue('log_date_format'))
-
+ self.print_logs = config.getoption('log_print')
+ self.formatter = logging.Formatter(
+ get_option_ini(config, 'log_format'),
+ get_option_ini(config, 'log_date_format'))
+
+ @contextmanager
+ def _runtest_for(self, item, when):
+ """Implements the internals of pytest_runtest_xxx() hook."""
+ with catching_logs(LogCaptureHandler(),
+ formatter=self.formatter) as log_handler:
+ item.catch_log_handler = log_handler
+ try:
+ yield # run test
+ finally:
+ del item.catch_log_handler
+
+ if self.print_logs:
+ # Add a captured log section to the report.
+ log = log_handler.stream.getvalue().strip()
+ item.add_report_section(when, 'log', log)
+
+ @pytest.mark.hookwrapper
def pytest_runtest_setup(self, item):
- """Start capturing log messages for this test.
-
- Creating a specific handler for each test ensures that we
- avoid multi threading issues.
-
- Attaching the handler and setting the level at the beginning
- of each test ensures that we are setup to capture log
- messages.
- """
+ with self._runtest_for(item, 'setup'):
+ yield
- # Create a handler for this test.
- item.catch_log_handler = CatchLogHandler()
- item.catch_log_handler.setFormatter(self.formatter)
+ @pytest.mark.hookwrapper
+ def pytest_runtest_call(self, item):
+ with self._runtest_for(item, 'call'):
+ yield
- # Attach the handler to the root logger and ensure that the
- # root logger is set to log all levels.
- root_logger = logging.getLogger()
- root_logger.addHandler(item.catch_log_handler)
- root_logger.setLevel(logging.NOTSET)
+ @pytest.mark.hookwrapper
+ def pytest_runtest_teardown(self, item):
+ with self._runtest_for(item, 'teardown'):
+ yield
- def pytest_runtest_makereport(self, __multicall__, item, call):
- """Add captured log messages for this report."""
- report = __multicall__.execute()
-
- # This fn called after setup, call and teardown. Only
- # interested in just after test call has finished.
- if call.when == 'call':
-
- # Detach the handler from the root logger to ensure no
- # further access to the handler.
- root_logger = logging.getLogger()
- root_logger.removeHandler(item.catch_log_handler)
-
- # For failed tests that have captured log messages add a
- # captured log section to the report if desired.
- if not report.passed and self.print_logs:
- long_repr = getattr(report, 'longrepr', None)
- if hasattr(long_repr, 'addsection'):
- log = item.catch_log_handler.stream.getvalue().strip()
- if log:
- long_repr.addsection('Captured log', log)
-
- # Release the handler resources.
- item.catch_log_handler.close()
- del item.catch_log_handler
-
- return report
-
-
-class CatchLogHandler(logging.StreamHandler):
+class LogCaptureHandler(logging.StreamHandler):
"""A logging handler that stores log records and the log text."""
def __init__(self):
@@ -122,24 +178,28 @@ class CatchLogHandler(logging.StreamHandler):
logging.StreamHandler.emit(self, record)
-class CatchLogFuncArg(object):
+class LogCaptureFixture(object):
"""Provides access and control of log capturing."""
- def __init__(self, handler):
- """Creates a new funcarg."""
+ @property
+ def handler(self):
+ return self._item.catch_log_handler
- self.handler = handler
+ def __init__(self, item):
+ """Creates a new funcarg."""
+ self._item = item
+ @property
def text(self):
"""Returns the log text."""
-
return self.handler.stream.getvalue()
+ @property
def records(self):
"""Returns the list of log records."""
-
return self.handler.records
+ @property
def record_tuples(self):
"""Returns a list of a striped down version of log records intended
for use in assertion comparison.
@@ -148,7 +208,7 @@ class CatchLogFuncArg(object):
(logger_name, log_level, message)
"""
- return [(r.name, r.levelno, r.getMessage()) for r in self.records()]
+ return [(r.name, r.levelno, r.getMessage()) for r in self.records]
def set_level(self, level, logger=None):
"""Sets the level for capturing of logs.
@@ -170,37 +230,84 @@ class CatchLogFuncArg(object):
"""
obj = logger and logging.getLogger(logger) or self.handler
- return CatchLogLevel(obj, level)
+ return logging_at_level(level, obj)
-class CatchLogLevel(object):
- """Context manager that sets the logging level of a handler or logger."""
+class CallablePropertyMixin(object):
+ """Backward compatibility for functions that became properties."""
- def __init__(self, obj, level):
- """Creates a new log level context manager."""
+ @classmethod
+ def compat_property(cls, func):
+ if isinstance(func, property):
+ make_property = func.getter
+ func = func.fget
+ else:
+ make_property = property
- self.obj = obj
- self.level = level
+ @functools.wraps(func)
+ def getter(self):
+ naked_value = func(self)
+ ret = cls(naked_value)
+ ret._naked_value = naked_value
+ ret._warn_compat = self._warn_compat
+ ret._prop_name = func.__name__
+ return ret
- def __enter__(self):
- """Adjust the log level."""
+ return make_property(getter)
- self.orig_level = self.obj.level
- self.obj.setLevel(self.level)
+ def __call__(self):
+ self._warn_compat(old="'caplog.{0}()' syntax".format(self._prop_name),
+ new="'caplog.{0}' property".format(self._prop_name))
+ return self._naked_value # to let legacy clients modify the object
- def __exit__(self, exc_type, exc_value, traceback):
- """Restore the log level."""
+class CallableList(CallablePropertyMixin, list):
+ pass
- self.obj.setLevel(self.orig_level)
+class CallableStr(CallablePropertyMixin, str):
+ pass
-def pytest_funcarg__caplog(request):
- """Returns a funcarg to access and control log capturing."""
+class CompatLogCaptureFixture(LogCaptureFixture):
+ """Backward compatibility with pytest-capturelog."""
- return CatchLogFuncArg(request._pyfuncitem.catch_log_handler)
+ def _warn_compat(self, old, new):
+ self._item.warn(code='L1',
+ message=("{0} is deprecated, use {1} instead"
+ .format(old, new)))
+ @CallableStr.compat_property
+ def text(self):
+ return super(CompatLogCaptureFixture, self).text
+
+ @CallableList.compat_property
+ def records(self):
+ return super(CompatLogCaptureFixture, self).records
+
+ @CallableList.compat_property
+ def record_tuples(self):
+ return super(CompatLogCaptureFixture, self).record_tuples
+
+ def setLevel(self, level, logger=None):
+ self._warn_compat(old="'caplog.setLevel()'",
+ new="'caplog.set_level()'")
+ return self.set_level(level, logger)
-def pytest_funcarg__capturelog(request):
- """Returns a funcarg to access and control log capturing."""
+ def atLevel(self, level, logger=None):
+ self._warn_compat(old="'caplog.atLevel()'",
+ new="'caplog.at_level()'")
+ return self.at_level(level, logger)
+
+
+@pytest.fixture
+def caplog(request):
+ """Access and control log capturing.
+
+ Captured logs are available through the following methods::
+
+ * caplog.text() -> string containing formatted log output
+ * caplog.records() -> list of logging.LogRecord instances
+ * caplog.record_tuples() -> list of (logger_name, level, message) tuples
+ """
+ return CompatLogCaptureFixture(request.node)
- return CatchLogFuncArg(request._pyfuncitem.catch_log_handler)
+capturelog = caplog
diff --git a/setup.py b/setup.py
index ba84092..0a99448 100755
--- a/setup.py
+++ b/setup.py
@@ -21,26 +21,29 @@ setup(name='pytest-catchlog',
version=_get_version(),
description=('py.test plugin to catch log messages.'
' This is a fork of pytest-capturelog.'),
- long_description=_read_text_file('README.rst'),
+ long_description='\n'.join([_read_text_file('README.rst'),
+ _read_text_file('CHANGES.rst'), ]),
author='Arthur Skowronek (Fork Author)', # original author: Meme Dough
author_email='eisensheng@mailbox.org',
url='https://github.com/eisensheng/pytest-catchlog',
py_modules=['pytest_catchlog', ],
- install_requires=['py>=1.1.1', ],
+ install_requires=['py>=1.1.1', 'pytest>=2.6'],
entry_points={'pytest11': ['pytest_catchlog = pytest_catchlog']},
license='MIT License',
zip_safe=False,
- keywords='py.test pytest',
+ keywords='py.test pytest logging',
classifiers=['Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python',
+ 'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
+ 'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: Implementation :: CPython',
'Programming Language :: Python :: Implementation :: PyPy',
'Topic :: Software Development :: Testing'])
diff --git a/tasks.py b/tasks.py
new file mode 100644
index 0000000..e271c99
--- /dev/null
+++ b/tasks.py
@@ -0,0 +1,188 @@
+# -*- coding: utf-8 -*-
+import os
+import re
+import io
+from contextlib import contextmanager
+from datetime import datetime
+
+from invoke import task, run
+
+VERSION_FILE = 'pytest_catchlog.py'
+CHANGE_LOG_FILE = 'CHANGES.rst'
+
+
+def _path_abs_join(*nodes):
+ return os.path.abspath(os.path.join(os.path.dirname(__file__), *nodes))
+
+
+def _path_open(*nodes, **kwargs):
+ return io.open(_path_abs_join(*nodes), **kwargs)
+
+
+def _shell_quote(s):
+ """Quote given string to be suitable as input for bash as argument."""
+ if not s:
+ return "''"
+ if re.search(r'[^\w@%+=:,./-]', s) is None:
+ return s
+ return "'" + s.replace("'", "'\"'\"'") + "'"
+
+
+def _git_do(*commands, **kwargs):
+ """Execute arbitrary git commands."""
+ kwargs.setdefault('hide', 'out')
+ results = [run('git ' + command, **kwargs).stdout.strip('\n')
+ for command in commands]
+ return results if len(commands) > 1 else results[0]
+
+
+def _git_checkout(branch_name):
+ """Switches to the given branch name."""
+ return _git_do('checkout ' + _shell_quote(branch_name))
+
+
+@contextmanager
+def _git_work_on(branch_name):
+ """Work on given branch. Preserves current git branch."""
+ original_branch = _git_do('rev-parse --abbrev-ref HEAD')
+ try:
+ if original_branch != branch_name:
+ _git_checkout(branch_name)
+ yield
+ finally:
+ if original_branch and original_branch != branch_name:
+ _git_checkout(original_branch)
+
+
+def _version_find_existing():
+ """Returns set of existing versions in this repository.
+
+ This information is backed by previously used version tags
+ stored in the git repository.
+ """
+ git_tags = [y.strip() for y in _git_do('tag -l').split('\n')]
+
+ _version_re = re.compile(r'^v?(\d+)(?:\.(\d+)(?:\.(\d+))?)?$')
+ return {tuple(int(n) if n else 0 for n in m.groups())
+ for m in (_version_re.match(t) for t in git_tags if t) if m}
+
+
+def _version_find_latest():
+ """Returns the most recent used version number.
+
+ This information is backed by previously used version tags
+ stored in the git repository.
+ """
+ return max(_version_find_existing())
+
+
+def _version_guess_next(position='minor'):
+ """Guess next version.
+
+ A guess for the next version is determined by incrementing given
+ position or minor level position in latest existing version.
+ """
+ try:
+ latest_version = list(_version_find_latest())
+ except ValueError:
+ latest_version = [0, 0, 0]
+
+ position_index = {'major': 0, 'minor': 1, 'patch': 2}[position]
+ latest_version[position_index] += 1
+ latest_version[position_index + 1:] = [0] * (2 - position_index)
+ return tuple(latest_version)
+
+
+def _version_format(version):
+ """Return version in dotted string format."""
+ return '.'.join(str(x) for x in version)
+
+
+def _patch_file(file_path, line_callback):
+ """Patch given file with result from line callback.
+
+ Each line will be passed to the line callback.
+ The return value of the given callback will determine
+ the new content for the file.
+
+ :param str file_path:
+ The file to patch.
+ :param callable line_callback:
+ The patch function to run over each line.
+ :return:
+ Whenever the file has changed or not.
+ :rtype:
+ bool
+ """
+ new_file_content, file_changed = [], False
+ with _path_open(file_path) as in_stream:
+ for l in (x.strip('\n') for x in in_stream):
+ alt_lines = line_callback(l) or [l]
+ if alt_lines != [l]:
+ file_changed = True
+ new_file_content += (x + u'\n' for x in alt_lines)
+
+ new_file_name = file_path + '.new'
+ with _path_open(new_file_name, mode='w') as out_stream:
+ out_stream.writelines(new_file_content)
+ out_stream.flush()
+ os.fsync(out_stream.fileno())
+ os.rename(new_file_name, file_path)
+
+ return file_changed
+
+
+def _patch_version(new_version):
+ """Patch given version into version file."""
+ _patch_version_re = re.compile(r"""^(\s*__version__\s*=\s*(?:"|'))"""
+ r"""(?:[^'"]*)(?:("|')\s*)$""")
+
+ def __line_callback(line):
+ match = _patch_version_re.match(line)
+ if match:
+ line_head, line_tail = match.groups()
+ return [line_head + new_version + line_tail]
+ return _patch_file(VERSION_FILE, __line_callback)
+
+
+def _patch_change_log(new_version):
+ """Patch given version into change log file."""
+ def __line_callback(line):
+ if line == u'`Unreleased`_':
+ return [u'`{}`_'.format(new_version)]
+ elif line == u'Yet to be released.':
+ return [datetime.utcnow().strftime(u'Released on %F.')]
+ elif line == u'.. %UNRELEASED_SECTION%':
+ return [u'.. %UNRELEASED_SECTION%',
+ u'',
+ u'`Unreleased`_',
+ u'-------------',
+ u'',
+ u'Yet to be released.',
+ u'']
+ return _patch_file(CHANGE_LOG_FILE, __line_callback)
+
+
+@task()
+def mkrelease(position='minor'):
+ """Merge development state into Master Branch and tags a new Release."""
+ next_version = _version_format(_version_guess_next(position))
+ with _git_work_on('develop'):
+ patched_files = []
+ if _patch_version(next_version):
+ patched_files.append(VERSION_FILE)
+
+ if _patch_change_log(next_version):
+ patched_files.append(CHANGE_LOG_FILE)
+
+ if patched_files:
+ patched_files = ' '.join(_shell_quote(x) for x in patched_files)
+ _git_do('diff --color=always -- ' + patched_files,
+ ("commit -m 'Bump Version to {0}' -- {1}"
+ .format(next_version, patched_files)),
+ hide=None)
+
+ with _git_work_on('master'):
+ message = _shell_quote('Release {0}'.format(next_version))
+ _git_do('merge --no-ff --no-edit -m {0} develop'.format(message),
+ "tag -a -m {0} {1}".format(message, next_version))
diff --git a/test_pytest_catchlog.py b/test_pytest_catchlog.py
index 0e9d55f..b7d6b3b 100644
--- a/test_pytest_catchlog.py
+++ b/test_pytest_catchlog.py
@@ -1,6 +1,6 @@
import py
-pytest_plugins = 'pytester', 'catchlog'
+pytest_plugins = 'pytester'
def test_nothing_logged(testdir):
@@ -8,8 +8,6 @@ def test_nothing_logged(testdir):
import sys
import logging
- pytest_plugins = 'catchlog'
-
def test_foo():
sys.stdout.write('text going to stdout')
sys.stderr.write('text going to stderr')
@@ -22,7 +20,7 @@ def test_nothing_logged(testdir):
result.stdout.fnmatch_lines(['*- Captured stderr call -*',
'text going to stderr'])
py.test.raises(Exception, result.stdout.fnmatch_lines,
- ['*- Captured log -*'])
+ ['*- Captured *log call -*'])
def test_messages_logged(testdir):
@@ -30,8 +28,6 @@ def test_messages_logged(testdir):
import sys
import logging
- pytest_plugins = 'catchlog'
-
def test_foo():
sys.stdout.write('text going to stdout')
sys.stderr.write('text going to stderr')
@@ -40,7 +36,7 @@ def test_messages_logged(testdir):
''')
result = testdir.runpytest()
assert result.ret == 1
- result.stdout.fnmatch_lines(['*- Captured log -*',
+ result.stdout.fnmatch_lines(['*- Captured *log call -*',
'*text going to logger*'])
result.stdout.fnmatch_lines(['*- Captured stdout call -*',
'text going to stdout'])
@@ -48,12 +44,50 @@ def test_messages_logged(testdir):
'text going to stderr'])
-def test_change_level(testdir):
+def test_setup_logging(testdir):
+ testdir.makepyfile('''
+ import sys
+ import logging
+
+ def setup_function(function):
+ logging.getLogger().info('text going to logger from setup')
+
+ def test_foo():
+ logging.getLogger().info('text going to logger from call')
+ assert False
+ ''')
+ result = testdir.runpytest()
+ assert result.ret == 1
+ result.stdout.fnmatch_lines(['*- Captured *log setup -*',
+ '*text going to logger from setup*',
+ '*- Captured *log call -*',
+ '*text going to logger from call*'])
+
+
+def test_teardown_logging(testdir):
testdir.makepyfile('''
import sys
import logging
- pytest_plugins = 'catchlog'
+ def test_foo():
+ logging.getLogger().info('text going to logger from call')
+
+ def teardown_function(function):
+ logging.getLogger().info('text going to logger from teardown')
+ assert False
+ ''')
+ result = testdir.runpytest()
+ assert result.ret == 1
+ result.stdout.fnmatch_lines(['*- Captured *log call -*',
+ '*text going to logger from call*',
+ '*- Captured *log teardown -*',
+ '*text going to logger from teardown*'])
+
+
+def test_change_level(testdir):
+ testdir.makepyfile('''
+ import sys
+ import logging
def test_foo(caplog):
caplog.set_level(logging.INFO)
@@ -70,13 +104,13 @@ def test_change_level(testdir):
''')
result = testdir.runpytest()
assert result.ret == 1
- result.stdout.fnmatch_lines(['*- Captured log -*',
+ result.stdout.fnmatch_lines(['*- Captured *log call -*',
'*handler INFO level*',
'*logger CRITICAL level*'])
py.test.raises(Exception, result.stdout.fnmatch_lines,
- ['*- Captured log -*', '*handler DEBUG level*'])
+ ['*- Captured *log call -*', '*handler DEBUG level*'])
py.test.raises(Exception, result.stdout.fnmatch_lines,
- ['*- Captured log -*', '*logger WARNING level*'])
+ ['*- Captured *log call -*', '*logger WARNING level*'])
@py.test.mark.skipif('sys.version_info < (2,5)')
@@ -86,8 +120,6 @@ def test_with_statement(testdir):
import sys
import logging
- pytest_plugins = 'catchlog'
-
def test_foo(caplog):
with caplog.at_level(logging.INFO):
log = logging.getLogger()
@@ -103,13 +135,13 @@ def test_with_statement(testdir):
''')
result = testdir.runpytest()
assert result.ret == 1
- result.stdout.fnmatch_lines(['*- Captured log -*',
+ result.stdout.fnmatch_lines(['*- Captured *log call -*',
'*handler INFO level*',
'*logger CRITICAL level*'])
py.test.raises(Exception, result.stdout.fnmatch_lines,
- ['*- Captured log -*', '*handler DEBUG level*'])
+ ['*- Captured *log call -*', '*handler DEBUG level*'])
py.test.raises(Exception, result.stdout.fnmatch_lines,
- ['*- Captured log -*', '*logger WARNING level*'])
+ ['*- Captured *log call -*', '*logger WARNING level*'])
def test_log_access(testdir):
@@ -117,13 +149,11 @@ def test_log_access(testdir):
import sys
import logging
- pytest_plugins = 'catchlog'
-
def test_foo(caplog):
logging.getLogger().info('boo %s', 'arg')
- assert caplog.records()[0].levelname == 'INFO'
- assert caplog.records()[0].msg == 'boo %s'
- assert 'boo arg' in caplog.text()
+ assert caplog.records[0].levelname == 'INFO'
+ assert caplog.records[0].msg == 'boo %s'
+ assert 'boo arg' in caplog.text
''')
result = testdir.runpytest()
assert result.ret == 0
@@ -139,12 +169,10 @@ def test_record_tuples(testdir):
import sys
import logging
- pytest_plugins = 'catchlog'
-
def test_foo(caplog):
logging.getLogger().info('boo %s', 'arg')
- assert caplog.record_tuples() == [
+ assert caplog.record_tuples == [
('root', logging.INFO, 'boo arg'),
]
''')
@@ -152,13 +180,85 @@ def test_record_tuples(testdir):
assert result.ret == 0
+def test_compat_camel_case_aliases(testdir):
+ testdir.makepyfile('''
+ import logging
+
+ def test_foo(caplog):
+ caplog.setLevel(logging.INFO)
+ logging.getLogger().debug('boo!')
+
+ with caplog.atLevel(logging.WARNING):
+ logging.getLogger().info('catch me if you can')
+ ''')
+ result = testdir.runpytest()
+ assert result.ret == 0
+
+ py.test.raises(Exception, result.stdout.fnmatch_lines,
+ ['*- Captured *log call -*'])
+
+ result = testdir.runpytest('-rw')
+ assert result.ret == 0
+ result.stdout.fnmatch_lines('''
+ =*warning summary*=
+ *WL1*test_compat_camel_case_aliases*caplog.setLevel()*deprecated*
+ *WL1*test_compat_camel_case_aliases*caplog.atLevel()*deprecated*
+ ''')
+
+
+def test_compat_properties(testdir):
+ testdir.makepyfile('''
+ import logging
+
+ def test_foo(caplog):
+ logging.getLogger().info('boo %s', 'arg')
+
+ assert caplog.text == caplog.text() == str(caplog.text)
+ assert caplog.records == caplog.records() == list(caplog.records)
+ assert (caplog.record_tuples ==
+ caplog.record_tuples() == list(caplog.record_tuples))
+ ''')
+ result = testdir.runpytest()
+ assert result.ret == 0
+
+ result = testdir.runpytest('-rw')
+ assert result.ret == 0
+ result.stdout.fnmatch_lines('''
+ =*warning summary*=
+ *WL1*test_compat_properties*caplog.text()*deprecated*
+ *WL1*test_compat_properties*caplog.records()*deprecated*
+ *WL1*test_compat_properties*caplog.record_tuples()*deprecated*
+ ''')
+
+
+def test_compat_records_modification(testdir):
+ testdir.makepyfile('''
+ import logging
+
+ logger = logging.getLogger()
+
+ def test_foo(caplog):
+ logger.info('boo %s', 'arg')
+ assert caplog.records
+ assert caplog.records()
+
+ del caplog.records()[:] # legacy syntax
+ assert not caplog.records
+ assert not caplog.records()
+
+ logger.info('foo %s', 'arg')
+ assert caplog.records
+ assert caplog.records()
+ ''')
+ result = testdir.runpytest()
+ assert result.ret == 0
+
+
def test_disable_log_capturing(testdir):
testdir.makepyfile('''
import sys
import logging
- pytest_plugins = 'catchlog'
-
def test_foo(caplog):
sys.stdout.write('text going to stdout')
logging.getLogger().warning('catch me if you can!')
@@ -173,4 +273,4 @@ def test_disable_log_capturing(testdir):
result.stdout.fnmatch_lines(['*- Captured stderr call -*',
'text going to stderr'])
py.test.raises(Exception, result.stdout.fnmatch_lines,
- ['*- Captured log -*'])
+ ['*- Captured *log call -*'])
diff --git a/tox.ini b/tox.ini
index 3be9fcb..ab154e2 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
[tox]
-envlist = py{27,32,33,34,py,py3}
+envlist = py{26,27,32,33,34,35}, pypy{,3}
[testenv]
deps =