pax_global_header00006660000000000000000000000064132621462450014517gustar00rootroot0000000000000052 comment=02844c0a8588b9dd332a692b57cbb9b91dbd56c9 pytest-toolbox-0.4/000077500000000000000000000000001326214624500143765ustar00rootroot00000000000000pytest-toolbox-0.4/.coveragerc000066400000000000000000000003011326214624500165110ustar00rootroot00000000000000[run] source = pytest_toolbox branch = True concurrency = multiprocessing parallel = True [report] exclude_lines = pragma: no cover raise NotImplementedError raise NotImplemented pytest-toolbox-0.4/.gitignore000066400000000000000000000005761326214624500163760ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging env/ venv/ build/ dist/ eggs/ lib/ sdist/ *.egg-info/ .installed.cfg *.egg # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .coverage .cache .coverage.* # Sphinx documentation docs/_build/ .idea .eggs /.pytest_cache/ pytest-toolbox-0.4/.pyup.yml000066400000000000000000000000301326214624500161650ustar00rootroot00000000000000schedule: 'every month' pytest-toolbox-0.4/.travis.yml000066400000000000000000000022151326214624500165070ustar00rootroot00000000000000language: python cache: pip python: - '3.6' - 'nightly' matrix: allow_failures: - python: 'nightly' install: - make install script: - make test - make lint - coverage run pytest_toolbox/__init__.py - ./tests/check_tag.py after_success: - ls -lha - coverage combine - coverage report - bash <(curl -s https://codecov.io/bash) deploy: provider: pypi user: samuelcolvin password: secure: "jS0D2qe3TtRMAXv1RMa8rtPY7EXZ6qOkowEZOc+MDfelMd7G7UjP+bZuLQigm0ZOW0saH3DYxlnGM+haDu6HqTHaiJmxm2UOHboZxy6kgzLP0TlR1ZQ4JQls9nwPBNJD5HSMFXYMrWCS0WrOq+DByN/NTXfnIHGasNHZ5+LKhe7aXAu0sPp1SX9MqiuPPNxVH2ujbJnvtePo0MK49el2MJFL7NYyGfCapdkZGZzZSvyeJUOCWI7XvwyojjOthHstnTlDL/Hl0+y40FdDfGEQ1Y0b2BYuciDjdMqXh9Etk1ERXJncUCKW1zMBftuzQGtODJ+R12yNMOfA0aAA4/PpptntS9qWmqax+4AatAFhWprUKKywuWIbJMJ5Kx78oejnvI7miCC/q35DLFdNyXEwZMF01uri6kJnMNw7Jawu2s1RETCltFeR//VCHW24F5oNbctjba7EkJTTFTPJ75TfyRFQfpcQ1UB58RvXdf+BqaDOtyZD1Soh+jf75z+wdt0ad0/FpXQ2uFx73+kERWWFFKNP3MLfVKgUbW5zC+oJiWIUNfgDILajYKRrF5Rar0ZmiBwQmEqTmoe2CdSQEA6j3Gy64+ovPnVmOI8yS8KFrMB4+ABc0F6wsK27RD7ZKy9bqBwvsc1tHF7l+DSHWx2yXkf0jKdbgjzhx+Qo1c5cVK0=" distributions: sdist bdist_wheel on: tags: true python: 3.6 pytest-toolbox-0.4/HISTORY.rst000066400000000000000000000006561326214624500163000ustar00rootroot00000000000000.. :changelog: History ------- 0.4.0 (2018-04-07) ------------------ * add comparison classes * rename ``caplog`` > ``smart_caplog`` * rename ``debug`` > ``print_logs`` * updates * remove python 3.5 support, now 3.6 only due to pydantic requirement 0.3.0 (2017-03-11) ------------------ * check for un-awaited coroutines 0.2.0 (2017-02-11) ------------------ * tweaks to logging * cleaner loop teardown * update dependencies pytest-toolbox-0.4/LICENSE000066400000000000000000000020761326214624500154100ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2017, 2018 Samuel Colvin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pytest-toolbox-0.4/Makefile000066400000000000000000000010541326214624500160360ustar00rootroot00000000000000.DEFAULT_GOAL := all .PHONY: install install: pip install -U pip setuptools pip install -r tests/requirements.txt pip install -e . .PHONY: isort isort: isort -rc -w 120 pytest_toolbox isort -rc -w 120 tests .PHONY: lint lint: flake8 pytest_toolbox/ tests/ pytest pytest_toolbox -p no:sugar -q --cache-clear coverage run setup.py check -rms .PHONY: test test: py.test --cov=pytest_toolbox .PHONY: testcov testcov: py.test --cov=pytest_toolbox && (echo "building coverage html"; coverage combine; coverage html) .PHONY: all all: testcov lint pytest-toolbox-0.4/README.rst000066400000000000000000000045031326214624500160670ustar00rootroot00000000000000pytest-toolbox ============== |Build Status| |codecov.io| |PyPI Status| |license| Copyright (C) 2016 Samuel Colvin Numerous useful plugins for pytest. Fixtures -------- ``tmpworkdir`` Run the test with the working directory set to a temporary directory. Similar to the pytest plugin ``tmpdir`` except working directory is changed. ``smart_caplog`` capture logs with a smarter interface than pytest's native ``caplog`` ``print_logs`` print all logs. ``loop`` asyncio loop. Methods ------- (See below for usage examples). ``mktree`` Create a tree of files from a dictionary. ``gettree`` Return a dictionary depicting a directory tree. Comparison Objects ------------------ All can be imported from ``pytest_toolbox.comparison``. ``CloseToNow`` check that a date (or date-like object) is close to now ``AnyInt`` check tests that an object is an int ``RegexStr`` check that a string matches the regex ``IsUUID`` that that an object is an instance of ``UUID``. Used with equals as in ``my_date == CloseToNow()``, these are useful when checking objects which contain a few unknown values are as expected Eg. .. code-block:: python assert { 'details': { 'user': 'foobar@example.com', 'id': AnyInt(), 'published': False, 'event': 'an example', 'created_ts': CloseToNow(), }, 'other_thing': [ ... ], ... } == obj Usage ----- .. code:: python from pytest_toolbox import gettree, mktree def test_whatever(tmpworkdir): mktree(tmpworkdir, { 'foobar.txt': 'has this content' }) assert gettree(tmpworkdir) = {'foobar.txt': 'has this content'} **TODO** .. |Build Status| image:: https://travis-ci.org/samuelcolvin/pytest-toolbox.svg?branch=master :target: https://travis-ci.org/samuelcolvin/pytest-toolbox .. |codecov.io| image:: http://codecov.io/github/samuelcolvin/pytest-toolbox/coverage.svg?branch=master :target: http://codecov.io/github/samuelcolvin/pytest-toolbox?branch=master .. |PyPI Status| image:: https://img.shields.io/pypi/v/pytest-toolbox.svg?style=flat :target: https://pypi.python.org/pypi/pytest-toolbox .. |license| image:: https://img.shields.io/pypi/l/pytest-toolbox.svg :target: https://github.com/samuelcolvin/pytest-toolbox pytest-toolbox-0.4/pytest_toolbox/000077500000000000000000000000001326214624500174745ustar00rootroot00000000000000pytest-toolbox-0.4/pytest_toolbox/__init__.py000066400000000000000000000001271326214624500216050ustar00rootroot00000000000000from pytest_toolbox.main import * # noqa from pytest_toolbox.version import * # noqa pytest-toolbox-0.4/pytest_toolbox/comparison.py000066400000000000000000000043011326214624500222160ustar00rootroot00000000000000import re from datetime import datetime, timezone from uuid import UUID # TODO use pytest_assertrepr_compare class CloseToNow: def __init__(self, delta=2): self.delta: float = delta self.now = datetime.utcnow() self.match = False self.other = None def __eq__(self, other): self.other = other if not isinstance(other, datetime): try: from pydantic.datetime_parse import parse_datetime except ImportError: # pragma: no cover raise ImportError('pydantic is required to use CloseToNow, please run `pip install pydantic`') other = parse_datetime(other) if other.tzinfo: self.now = self.now.replace(tzinfo=timezone.utc) self.match = -self.delta < (self.now - other).total_seconds() < self.delta return self.match def __repr__(self): if self.match: # if we've got the correct value return it to aid in diffs return repr(self.other) else: # else return something which explains what's going on. return f'' class AnyInt: def __init__(self): self.v = None def __eq__(self, other): if type(other) == int: self.v = other return True def __repr__(self): if self.v is None: return '' else: return repr(self.v) class RegexStr: re = re def __init__(self, regex, flags=re.S): self._regex = re.compile(regex, flags=flags) self.v = None def __eq__(self, other): if self._regex.fullmatch(other): self.v = other return True return False def __repr__(self): if self.v is None: return f'' else: return repr(self.v) class IsUUID: def __init__(self): self.v = None def __eq__(self, other): if isinstance(other, UUID): self.v = other return True # could also check for regex def __repr__(self): return repr(self.v) if self.v else '' pytest-toolbox-0.4/pytest_toolbox/main.py000066400000000000000000000136401326214624500207760ustar00rootroot00000000000000import asyncio import contextlib import io import logging import os import re from copy import copy import pytest from py._path.local import LocalPath @contextlib.contextmanager def loop_context(existing_loop=None): """ context manager which creates an asyncio loop. :param existing_loop: if supplied this loop is passed straight through and no new loop is created. """ if existing_loop: # loop already exists, pass it straight through yield existing_loop else: _loop = asyncio.new_event_loop() asyncio.set_event_loop(None) yield _loop if not _loop.is_closed(): # pragma: no branch _loop.call_soon(_loop.stop) _loop.run_forever() _loop.close() asyncio.set_event_loop(None) def pytest_pycollect_makeitem(collector, name, obj): """ Fix pytest collecting for coroutines. """ if collector.funcnamefilter(name) and asyncio.iscoroutinefunction(obj): return list(collector._genfunctions(name, obj)) def pytest_pyfunc_call(pyfuncitem): """ Run coroutines in an event loop instead of a normal function call. """ if asyncio.iscoroutinefunction(pyfuncitem.function): existing_loop = pyfuncitem.funcargs.get('loop', None) with loop_context(existing_loop) as _loop: testargs = {arg: pyfuncitem.funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames} task = _loop.create_task(pyfuncitem.obj(**testargs)) _loop.run_until_complete(task) return True @pytest.yield_fixture def loop(): """ Yield fixture using loop_context() """ with loop_context() as _loop: yield _loop @pytest.yield_fixture def tmpworkdir(tmpdir): """ Create a temporary working working directory. """ cwd = os.getcwd() os.chdir(tmpdir.strpath) yield tmpdir os.chdir(cwd) def mktree(lp: LocalPath, d): """ Create a tree of files from a dictionary of name > content lookups. """ for name, content in d.items(): _lp = copy(lp) parts = list(filter(bool, name.split('/'))) for part in parts[:-1]: _lp = _lp.mkdir(part) _lp = _lp.join(parts[-1]) if isinstance(content, dict): _lp.mkdir() mktree(_lp, content) else: _lp.write(content) def gettree(lp: LocalPath, max_len=120): """ Get a dict representing the file tree for a directory """ assert lp.check() if lp.isdir(): return {df.basename: gettree(df, max_len=max_len) for df in lp.listdir()} else: assert lp.isfile() content = lp.read_text('utf8') if max_len and len(content) > max_len: content = content[:max_len - 3] + '...' return content class StreamLog: """ Log stream object which allows one or more logs to be captured and tested. """ def __init__(self, *log_names, level=logging.INFO, fmt='%(name)s %(levelname)s: %(message)s'): self.handler = None self.stream = io.StringIO() self.handler = logging.StreamHandler(stream=self.stream) self.loggers = [] self._log_names = log_names or ('',) self._level = level self._fmt = fmt self.set_loggers() def set_loggers(self, *, log_names=None, level=None, fmt=None): if self.loggers: self.finish() if log_names is not None: self._log_names = log_names or ('',) if level is not None: self._level = level if fmt is not None: self._fmt = fmt self.loggers = [logging.getLogger(log_name) for log_name in self._log_names] self.handler.setFormatter(logging.Formatter(self._fmt)) for logger in self.loggers: logger.disabled = False logger.addHandler(self.handler) self.set_level(self._level) def set_level(self, level): for logger in self.loggers: logger.setLevel(level) @classmethod def set_different_level(cls, **levels): for log_name, level in levels.items(): logger = logging.getLogger(log_name) logger.setLevel(level) @property def log(self): self.stream.seek(0) return self.stream.read() def finish(self): for logger in self.loggers: logger.removeHandler(self.handler) def __call__(self, *normalisers): log = str(self) for pattern, repl in normalisers: log = re.sub(pattern, repl, log) return log def __eq__(self, other): return self.log == other def __contains__(self, item): return item in self.log def __str__(self): return self.log def __repr__(self): return '< caplog: {!r}>'.format(self.log) @pytest.yield_fixture def smart_caplog(): """ Similar to pytest's "capsys" except logs are captured not stdout and stderr See StreamLog for details on configuration and tests for examples of usage. """ stream_log = StreamLog() yield stream_log stream_log.finish() @pytest.yield_fixture def print_logs(): """ fixture which causes all arq logs to display. For debugging purposes only, should always be removed before committing. """ # TODO: could be extended to also work as a context manager and allow more control. logger = logging.getLogger() try: root_handler = logger.handlers[0] except IndexError: root_handler = None else: prev_level = logger.level handler = logging.StreamHandler() handler.setLevel(logging.DEBUG) handler.setFormatter(logging.Formatter('%(asctime)s %(name)18s %(levelname)7s: %(message)s', datefmt='%H:%M:%S')) logger.addHandler(handler) logger.setLevel(logging.DEBUG) logger.removeHandler(root_handler) yield logger.removeHandler(handler) if root_handler: logger.addHandler(root_handler) logger.setLevel(prev_level) pytest-toolbox-0.4/pytest_toolbox/version.py000066400000000000000000000001451326214624500215330ustar00rootroot00000000000000from distutils.version import StrictVersion __all__ = ['VERSION'] VERSION = StrictVersion('0.4.0') pytest-toolbox-0.4/setup.cfg000066400000000000000000000003051326214624500162150ustar00rootroot00000000000000[tool:pytest] testpaths = tests addopts = --isort timeout = 10 filterwarnings = error [flake8] max-complexity = 10 max-line-length = 120 [bdist_wheel] python-tag = py35+ [isort] line_length=120 pytest-toolbox-0.4/setup.py000066400000000000000000000026461326214624500161200ustar00rootroot00000000000000from importlib.machinery import SourceFileLoader from pathlib import Path from setuptools import setup description = 'Numerous useful plugins for pytest.' long_description = Path(__file__).resolve().parent.joinpath('README.rst').read_text() # importing just this file avoids importing the full package with external dependencies which might not be installed version = SourceFileLoader('version', 'pytest_toolbox/version.py').load_module() setup( name='pytest-toolbox', version=str(version.VERSION), description=description, long_description=long_description, classifiers=[ 'Environment :: Console', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Topic :: Software Development :: Testing', 'Framework :: Pytest', ], keywords='pytest,plugin,toolbox', author='Samuel Colvin', license='MIT', author_email='S@muelColvin.com', url='https://github.com/samuelcolvin/pytest-toolbox', packages=['pytest_toolbox'], include_package_data=True, zip_safe=True, platforms='any', python_requires='>=3.5', install_requires=[ 'pytest>=3.5.0', ], entry_points={ 'pytest11': ['toolbox = pytest_toolbox'], }, ) pytest-toolbox-0.4/tests/000077500000000000000000000000001326214624500155405ustar00rootroot00000000000000pytest-toolbox-0.4/tests/__init__.py000066400000000000000000000000001326214624500176370ustar00rootroot00000000000000pytest-toolbox-0.4/tests/check_tag.py000077500000000000000000000007161326214624500200310ustar00rootroot00000000000000#!/usr/bin/env python3 import os import sys from pytest_toolbox.version import VERSION git_tag = os.getenv('TRAVIS_TAG') if git_tag: if git_tag.lower().lstrip('v') != str(VERSION).lower(): print('✖ "TRAVIS_TAG" environment variable does not match package version: "%s" vs. "%s"' % (git_tag, VERSION)) sys.exit(1) else: print('✓ "TRAVIS_TAG" environment variable matches package version: "%s" vs. "%s"' % (git_tag, VERSION)) pytest-toolbox-0.4/tests/requirements.txt000066400000000000000000000003111326214624500210170ustar00rootroot00000000000000Pygments==2.2.0 coverage==4.5.1 docutils==0.14 flake8==3.5.0 pydantic==0.8 pycodestyle==2.3.1 pyflakes==1.6.0 pytest==3.5.0 pytest-cov==2.5.1 pytest-isort==0.1.0 pytest-mock==1.7.1 pytest-sugar==0.9.1 pytest-toolbox-0.4/tests/test_comparison.py000066400000000000000000000033021326214624500213210ustar00rootroot00000000000000import uuid from datetime import datetime, timedelta, timezone import pytest from pytest_toolbox.comparison import AnyInt, CloseToNow, IsUUID, RegexStr def test_close_to_now_true(): c2n = CloseToNow() dt = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S') assert dt == c2n assert str(c2n) == repr(dt) def test_close_to_now_true_dt(): assert datetime.utcnow() == CloseToNow() def test_close_to_now_false(): c2n = CloseToNow() with pytest.raises(AssertionError): assert datetime(2000, 1, 1).strftime('%Y-%m-%dT%H:%M:%S') == c2n assert str(c2n).startswith('' def test_regex_true(): assert 'whatever' == RegexStr('whatever') reg = RegexStr('wh.*er') assert 'whatever' == reg assert str(reg) == "'whatever'" def test_regex_false(): reg = RegexStr('wh.*er') with pytest.raises(AssertionError): assert 'WHATEVER' == reg assert str(reg) == "" def test_is_uuid_true(): is_uuid = IsUUID() uuid_ = uuid.uuid4() assert uuid_ == is_uuid assert str(is_uuid) == repr(uuid_) def test_is_uuid_false(): is_uuid = IsUUID() with pytest.raises(AssertionError): assert '123' == is_uuid assert str(is_uuid) == '' pytest-toolbox-0.4/tests/test_methods.py000066400000000000000000000056051326214624500206220ustar00rootroot00000000000000import logging import os from pytest_toolbox import gettree, mktree pytest_plugins = 'pytester' def test_mktree(tmpdir): mktree(tmpdir, { 'foo': 'bar', 'a_dir': { 'f': 'X' } }) assert tmpdir.join('foo').exists() assert tmpdir.join('foo').read_text('utf') == 'bar' assert tmpdir.join('a_dir').isdir() assert tmpdir.join('a_dir/f').read_text('utf') == 'X' def test_deep(tmpdir): mktree(tmpdir, { 'foo/bar': { 'f': 'X' } }) assert tmpdir.join('foo').isdir() assert tmpdir.join('foo/bar').isdir() assert tmpdir.join('foo/bar/f').read_text('utf') == 'X' def test_gettree(tmpdir): tmpdir.join('foo').write_text('bar', 'utf8') assert { 'foo': 'bar' } == gettree(tmpdir) def test_gettree_truncate(tmpdir): tmpdir.join('foo').write_text('123456789', 'utf8') assert { 'foo': '12...' } == gettree(tmpdir, max_len=5) def test_smart_caplog(smart_caplog): logger = logging.getLogger('foobar') logger.info('this is to info') logger.debug('this is to debug') assert 'foobar INFO: this is to info\n' == smart_caplog == str(smart_caplog) assert 'foobar INFO: that is to info\n' == smart_caplog(('this', 'that')) assert 'to info' in smart_caplog assert repr(smart_caplog) == "< caplog: 'foobar INFO: this is to info\\n'>" def test_caplog_change(smart_caplog): smart_caplog.set_loggers(log_names=('foo',), level=logging.WARNING, fmt='%(message)s') logger_foo = logging.getLogger('foo') logger_foo.info('this is to foo info') logger_foo.warning('this is to foo warning') logger_bar = logging.getLogger('bar') logger_bar.info('this is to bar info') logger_bar.warning('this is to bar warning') assert 'this is to foo warning\n' == smart_caplog def test_caplog_debug(smart_caplog): smart_caplog.set_different_level(foobar=logging.DEBUG) logger = logging.getLogger('foobar') logger.info('this is to info') logger.debug('this is to debug') assert 'foobar INFO: this is to info\nfoobar DEBUG: this is to debug\n' == smart_caplog def test_tmpworkdir(tmpworkdir): assert os.getcwd() == str(tmpworkdir) def test_async_tests(testdir): testdir.makepyfile("""\ import asyncio async def get_4(): return 4 async def test_async(loop): assert isinstance(loop, asyncio.AbstractEventLoop) v = await get_4() assert v == 4 """) result = testdir.runpytest('-p', 'no:sugar') result.assert_outcomes(passed=1) def test_debug(testdir, capsys): testdir.makepyfile("""\ import logging def test_print_logs(print_logs): logger = logging.getLogger('foobar') logger.info('this is to info') logger.debug('this is to debug') """) result = testdir.runpytest('-p', 'no:sugar', '-s') result.assert_outcomes(passed=1) _, stderr = capsys.readouterr() assert 'this is to debug' in stderr