├── .flake8 ├── .gitignore ├── .travis.yml ├── CHANGELOG ├── LICENSE ├── MANIFEST.in ├── README.rst ├── bin ├── check ├── release └── test ├── pytest_reqs.py ├── requirements ├── requirements.txt └── test.txt ├── setup.py ├── test_reqs.py └── tox.ini /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | matrix: 3 | include: 4 | - env: TOX_ENV=lint 5 | - python: 3.7 6 | dist: xenial 7 | - python: 3.6 8 | - python: 3.5 9 | - python: 2.7 10 | - env: TOX_ENV=test-pytest-xdist 11 | 12 | install: 13 | - pip install tox 14 | - export TOX_ENV=${TOX_ENV:-`tox --listenvs | grep "py${TRAVIS_PYTHON_VERSION/./}" | tr '\n' ','`} 15 | 16 | script: tox -e $TOX_ENV 17 | 18 | before_cache: 19 | - rm -rf $HOME/.cache/pip/log 20 | 21 | cache: 22 | directories: 23 | - $HOME/.cache/pip 24 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 0.2.1 2 | ---------------------------------------------- 3 | 4 | - Drop support for Python 3.4 5 | - Add support for pip 19.0 through 19.1.1 6 | - Fix issue with wrong attribute name (#43) 7 | 8 | 9 | 0.2.0 10 | ---------------------------------------------- 11 | 12 | - Remove dependency on internal pip APIs 13 | - Add support for Python 3.7 14 | - Add support for pip 9.0.3, 10.0.0, 10.0.1, 18.0 and 18.1 15 | 16 | 0.1.0 17 | ---------------------------------------------- 18 | 19 | - Add a check for max supported pip version 20 | - Drop support for pip<7.1.0 21 | 22 | 0.0.7 23 | ---------------------------------------------- 24 | 25 | - Add reqs-outdate flag to check for out of date dependencies 26 | 27 | 0.0.6 28 | ---------------------------------------------- 29 | 30 | - Address test ordering issues 31 | 32 | 0.0.5 33 | ---------------------------------------------- 34 | 35 | - Adds reqsfilenamepatterns config option 36 | 37 | 0.0.4 38 | ---------------------------------------------- 39 | 40 | - Fixed bug where cmd-line option was not checked 41 | 42 | 0.0.3 43 | ---------------------------------------------- 44 | 45 | - Fixed bug where project name could be non-lower 46 | 47 | 0.0.2 48 | ---------------------------------------------- 49 | 50 | - Adds reqsignorelocal config option 51 | 52 | 0.0.1 53 | ---------------------------------------------- 54 | 55 | - Initial release 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy of 2 | this software and associated documentation files (the "Software"), to deal in 3 | the Software without restriction, including without limitation the rights to 4 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 5 | of the Software, and to permit persons to whom the Software is furnished to do 6 | so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all 9 | copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 17 | SOFTWARE. 18 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG 2 | include README.rst 3 | include setup.py 4 | include tox.ini 5 | include LICENSE 6 | include test_reqs.py 7 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | py.test plugin for checking requirements files 2 | ================================================== 3 | 4 | Description 5 | ----------- 6 | 7 | This plugin checks your requirements files for specific versions, and compares 8 | those versions with the installed libraries in your environment, failing your 9 | test suite if any are invalid or out of date. 10 | 11 | This is useful for keeping virtual environments up-to-date, and ensuring that 12 | your test suite is always being passed with the requirements you have 13 | specified. 14 | 15 | It also has the added bonus of verifying that your requirements files are 16 | syntatically valid, and can check if there are new releases of your 17 | dependencies available. 18 | 19 | Usage 20 | ----- 21 | 22 | install via:: 23 | 24 | pip install pytest-reqs 25 | 26 | if you then type:: 27 | 28 | py.test --reqs 29 | 30 | by default it will search for dependencies in the files matching: 31 | 32 | - ``req*.txt`` 33 | - ``req*.pip`` 34 | - ``requirements/*.txt`` 35 | - ``requirements/*.pip`` 36 | 37 | and the declared dependencies will be checked against the current environment. 38 | 39 | A little example 40 | ---------------- 41 | 42 | If your environment has dependencies installed like this:: 43 | 44 | $ pip freeze 45 | foo==0.9.9 46 | 47 | But you have a ``requirements.txt`` file like this:: 48 | 49 | $ cat requirements.txt 50 | foo==1.0.0 51 | 52 | you can run ``py.test`` with the plugin installed:: 53 | 54 | $ py.test --reqs 55 | =================================== FAILURES =================================== 56 | ______________________________ requirements-check ______________________________ 57 | Distribution "foo" requires foo==1.0.0 (from -r requirements.txt (line 1)) but 58 | 0.9.9 is installed 59 | 60 | It also handles ``pip``'s version containment syntax (e.g, ``foo<=1.0.0``, 61 | ``foo>=1.0.0``, etc):: 62 | 63 | $ py.test --reqs 64 | =================================== FAILURES =================================== 65 | ______________________________ requirements-check ______________________________ 66 | Distribution "foo" requires foo>=1.0.0 (from -r requirements.txt (line 1)) but 67 | 0.9.9 is installed 68 | 69 | Furthermore, it will tell you if your requirements file is invalid (for 70 | example, if there is not enough ``=`` symbols):: 71 | 72 | $ py.test --reqs 73 | ______________________________ requirements-check ______________________________ 74 | Invalid requirement: 'foo=1.0.0' (from -r requirements.txt) 75 | 76 | 77 | Configuring options 78 | ------------------- 79 | 80 | Ignoring local projects 81 | ~~~~~~~~~~~~~~~~~~~~~~~ 82 | 83 | You might have requirements files with paths to local projects, e.g. for local 84 | development:: 85 | 86 | $ cat requirements/local_development.txt 87 | -e ../foo 88 | 89 | However, testing these requirements will fail if the test environment is 90 | missing the local project (e.g., on a CI build):: 91 | 92 | =================================== FAILURES =================================== 93 | ______________________________ requirements-check ______________________________ 94 | ../foo should either be a path to a local project or a VCS url beginning with 95 | svn+, git+, hg+, or bzr+ (from -r requirements.txt) 96 | 97 | To get around this, you can disable checking for local projects with the 98 | following ``pytest`` option:: 99 | 100 | # content of setup.cfg 101 | [pytest] 102 | reqsignorelocal = True 103 | 104 | Declaring your own filename patterns 105 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 106 | 107 | You might have requirements files in files other than the default filename 108 | patterns: 109 | 110 | - ``req*.txt`` 111 | - ``req*.pip`` 112 | - ``requirements/*.txt`` 113 | - ``requirements/*.pip`` 114 | 115 | While there aren't any restrictions on what filenames are or are not valid for 116 | requirements files, the patterns which are currently supported by 117 | ``pytest-reqs`` are the same common patterns supported by other automated tools 118 | around requirements files. 119 | 120 | However, you can override these default patterns with the following ``pytest`` 121 | option:: 122 | 123 | # content of setup.cfg 124 | [pytest] 125 | reqsfilenamepatterns = 126 | mycustomrequirementsfile.txt 127 | someotherfilename.ext 128 | 129 | Running ``pytest-reqs`` before any other tests 130 | ---------------------------------------------- 131 | 132 | Currently there is no way to define the order of pytest plugins (see 133 | `pytest-dev/pytest#935 `__) 134 | 135 | This means that if you don't use any other plugins, ``pytest-reqs`` will run 136 | it's tests last. If you do use other plugins, there is no way to guarantee when 137 | the ``pytest-reqs`` tests will be run. 138 | 139 | If you absolutely need to run ``pytest-reqs`` before any other tests and 140 | plugins, instead of using the ``--reqs`` flag, you can define a 141 | ``tests/conftest.py`` file as follows: 142 | 143 | .. code-block:: python 144 | 145 | from pytest_reqs import check_requirements 146 | 147 | def pytest_collection_modifyitems(config, session, items): 148 | check_requirements(config, session, items) 149 | 150 | Running requirements checks and no other tests 151 | ---------------------------------------------- 152 | 153 | You can also restrict your test run to only perform "reqs" tests and not any 154 | other tests by typing:: 155 | 156 | py.test --reqs -m reqs 157 | 158 | This will only run test items with the "reqs" marker which this plugin adds 159 | dynamically. 160 | 161 | Checking for out-of-date dependencies 162 | ------------------------------------- 163 | 164 | You can use the ``--reqs-outdated`` flag to determine if any of your 165 | dependencies are out-of-date:: 166 | 167 | $ py.test --reqs-outdated 168 | ______________________________ requirements-check ______________________________ 169 | Distribution "foo" is outdated (from -r requirements.txt (line 1)), 170 | latest version is foo==1.0.1 171 | 172 | This feature is only available with ``pip>=9.0.0``. 173 | 174 | Authors 175 | ------- 176 | 177 | - `Dustin Ingram `__ 178 | - `Victor Titor `__ 179 | 180 | License 181 | ------- 182 | 183 | Open source MIT license. 184 | 185 | Notes 186 | ----- 187 | 188 | The repository of this plugin is at http://github.com/di/pytest-reqs. 189 | 190 | For more info on py.test see http://pytest.org. 191 | -------------------------------------------------------------------------------- /bin/check: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | python3 setup.py check -r -s 4 | -------------------------------------------------------------------------------- /bin/release: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | ./bin/check 6 | git checkout master 7 | git pull 8 | python3 setup.py sdist bdist_wheel 9 | twine upload dist/* -r pypi --skip-existing 10 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | tox 4 | -------------------------------------------------------------------------------- /pytest_reqs.py: -------------------------------------------------------------------------------- 1 | from distutils.util import strtobool 2 | from glob import glob 3 | from itertools import chain 4 | from json import loads 5 | from os import devnull 6 | from subprocess import check_output 7 | from sys import executable 8 | from warnings import warn 9 | 10 | import packaging.utils 11 | import packaging.version 12 | import pip_api 13 | from pkg_resources import get_distribution 14 | import pytest 15 | 16 | max_version = packaging.version.parse("18.1.0") 17 | pip_version = packaging.version.parse(pip_api.version()) 18 | if pip_version > max_version: 19 | warn( 20 | "Version pip=={} is possibly incompatible, highest " 21 | "known compatible version is {}.".format(pip_version, max_version) 22 | ) 23 | 24 | __version__ = "0.0.4" 25 | 26 | DEFAULT_PATTERNS = ["req*.txt", "req*.pip", "requirements/*.txt", "requirements/*.pip"] 27 | 28 | 29 | def pytest_configure(config): 30 | config.addinivalue_line( 31 | "markers", "reqs: check requirements files against what is installed" 32 | ) 33 | config.addinivalue_line( 34 | "markers", "reqs-outdated: check requirements files for updates" 35 | ) 36 | 37 | 38 | def pytest_addoption(parser): 39 | group = parser.getgroup("general") 40 | group.addoption( 41 | "--reqs", 42 | action="store_true", 43 | help="check requirements files against what is installed", 44 | ) 45 | group.addoption( 46 | "--reqs-outdated", 47 | action="store_true", 48 | help="check requirements files for updates", 49 | ) 50 | parser.addini("reqsignorelocal", help="ignore local requirements (default: False)") 51 | parser.addini( 52 | "reqsfilenamepatterns", 53 | help="Override the default filename patterns to search (default:" 54 | "req*.txt, req*.pip, requirements/*.txt, requirements/*.pip)", 55 | type="linelist", 56 | ) 57 | 58 | 59 | def pytest_sessionstart(session): 60 | config = session.config 61 | if not hasattr(config, "ignore_local"): 62 | ignore_local = config.getini("reqsignorelocal") or "no" 63 | config.ignore_local = strtobool(ignore_local) 64 | if not hasattr(config, "patterns"): 65 | config.patterns = config.getini("reqsfilenamepatterns") 66 | 67 | 68 | def pytest_collection_modifyitems(config, session, items): 69 | if config.option.reqs: 70 | check_requirements(config, session, items) 71 | if config.option.reqs_outdated: 72 | check_outdated_requirements(config, session, items) 73 | 74 | 75 | def get_reqs_filenames(config): 76 | patterns = config.patterns or DEFAULT_PATTERNS 77 | return set(chain.from_iterable(map(glob, patterns))) 78 | 79 | 80 | def check_requirements(config, session, items): 81 | installed_distributions = dict( 82 | [ 83 | (packaging.utils.canonicalize_name(name), req) 84 | for name, req in pip_api.installed_distributions().items() 85 | ] 86 | ) 87 | 88 | items.extend( 89 | ReqsItem(filename, installed_distributions, config, session) 90 | for filename in get_reqs_filenames(config) 91 | ) 92 | 93 | 94 | def check_outdated_requirements(config, session, items): 95 | local_pip_version = packaging.version.parse(get_distribution("pip").version) 96 | required_pip_version = packaging.version.parse("9.0.0") 97 | 98 | if local_pip_version >= required_pip_version: 99 | with open(devnull, "w") as DEVNULL: 100 | pip_outdated_dists = loads( 101 | check_output( 102 | [executable, "-m", "pip", "list", "-o", "--format", "json"], 103 | stderr=DEVNULL, 104 | ) 105 | ) 106 | 107 | items.extend( 108 | OutdatedReqsItem(filename, pip_outdated_dists, config, session) 109 | for filename in get_reqs_filenames(config) 110 | ) 111 | 112 | 113 | class PipOption: 114 | def __init__(self, config): 115 | self.skip_requirements_regex = "^-e" if config.ignore_local else "" 116 | 117 | 118 | class ReqsError(Exception): 119 | """ indicates an error during requirements checks. """ 120 | 121 | 122 | class ReqsItem(pytest.Item, pytest.File): 123 | def __init__(self, filename, installed_distributions, config, session): 124 | super(ReqsItem, self).__init__(filename, config=config, session=session) 125 | self.add_marker("reqs") 126 | self.filename = filename 127 | self.installed_distributions = installed_distributions 128 | self.config = config 129 | 130 | def get_requirements(self): 131 | try: 132 | return { 133 | packaging.utils.canonicalize_name(name): req 134 | for name, req in pip_api.parse_requirements( 135 | self.filename, options=PipOption(self.config) 136 | ).items() 137 | } 138 | except pip_api.exceptions.PipError as e: 139 | raise ReqsError( 140 | "%s (from -r %s)" % (e.args[0].split("\n")[0], self.filename) 141 | ) 142 | 143 | def runtest(self): 144 | for name, req in self.get_requirements().items(): 145 | try: 146 | installed_distribution = self.installed_distributions[name] 147 | except KeyError: 148 | raise ReqsError('Distribution "%s" is not installed' % (name)) 149 | if not req.specifier.contains(installed_distribution.version): 150 | raise ReqsError( 151 | 'Distribution "%s" requires %s but %s is installed' 152 | % (installed_distribution.name, req, installed_distribution.version) 153 | ) 154 | 155 | def repr_failure(self, excinfo): 156 | if excinfo.errisinstance(ReqsError): 157 | return excinfo.value.args[0] 158 | return super(ReqsItem, self).repr_failure(excinfo) 159 | 160 | def reportinfo(self): 161 | return (self.fspath, -1, "requirements-check") 162 | 163 | 164 | class OutdatedReqsItem(ReqsItem): 165 | def __init__(self, filename, pip_outdated_dists, config, session): 166 | super(ReqsItem, self).__init__(filename, config=config, session=session) 167 | self.add_marker("reqs-outdated") 168 | self.filename = filename 169 | self.pip_outdated_dists = pip_outdated_dists 170 | self.config = config 171 | 172 | def runtest(self): 173 | for name, req in self.get_requirements().items(): 174 | for dist in self.pip_outdated_dists: 175 | if name == dist["name"]: 176 | raise ReqsError( 177 | 'Distribution "%s" is outdated (from %s), ' 178 | "latest version is %s==%s" 179 | % (name, req.comes_from, name, dist["latest_version"]) 180 | ) 181 | -------------------------------------------------------------------------------- /requirements/requirements.txt: -------------------------------------------------------------------------------- 1 | pip>=6.0 2 | -------------------------------------------------------------------------------- /requirements/test.txt: -------------------------------------------------------------------------------- 1 | pretend==1.0.9 2 | pytest==4.5.0 3 | -e . 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | __version__ = "0.2.1" 4 | 5 | setup( 6 | name="pytest-reqs", 7 | description="pytest plugin to check pinned requirements", 8 | long_description=open("README.rst").read(), 9 | license="MIT license", 10 | version=__version__, 11 | author="Dustin Ingram", 12 | author_email="github@dustingram.com", 13 | url="https://github.com/di/pytest-reqs", 14 | py_modules=["pytest_reqs"], 15 | entry_points={"pytest11": ["reqs = pytest_reqs"]}, 16 | install_requires=["pytest>=2.4.2", "packaging>=17.1", "pip_api>=0.0.2"], 17 | tests_require=["pytest>=2.4.2", "pretend"], 18 | classifiers=[ 19 | "Framework :: Pytest", 20 | "Programming Language :: Python", 21 | "Programming Language :: Python :: 2.7", 22 | "Programming Language :: Python :: 3.5", 23 | "Programming Language :: Python :: 3.6", 24 | "Programming Language :: Python :: 3.7", 25 | ], 26 | ) 27 | -------------------------------------------------------------------------------- /test_reqs.py: -------------------------------------------------------------------------------- 1 | from distutils.version import LooseVersion 2 | from pip_api._installed_distributions import Distribution 3 | from pkg_resources import get_distribution 4 | 5 | from pretend import stub 6 | import pytest 7 | 8 | pytest_plugins = ("pytester",) 9 | 10 | 11 | def test_version(): 12 | import pytest_reqs 13 | 14 | assert pytest_reqs.__version__ 15 | 16 | 17 | @pytest.fixture 18 | def mock_dist(): 19 | return Distribution(name="foo", version="1.0") 20 | 21 | 22 | @pytest.mark.parametrize( 23 | "requirements", ["foo", "Foo", "foo==1.0", "foo>=1.0", "foo<=1.0", "# comment"] 24 | ) 25 | def test_existing_requirement(requirements, mock_dist, testdir, monkeypatch): 26 | testdir.makefile(".txt", requirements=requirements) 27 | monkeypatch.setattr( 28 | "pytest_reqs.pip_api.installed_distributions", 29 | lambda: {mock_dist.name: mock_dist}, 30 | ) 31 | 32 | result = testdir.runpytest("--reqs") 33 | assert "passed" in result.stdout.str() 34 | 35 | 36 | @pytest.mark.parametrize( 37 | "requirements, dist", 38 | [ 39 | ("foo-bar", stub(name="foo-bar", version="1.0")), 40 | ("foo-bar==1.0", stub(name="foo-bar", version="1.0")), 41 | # Capitalization 42 | ("Foo-bar", stub(name="foo-bar", version="1.0")), 43 | ("foo-bar", stub(name="Foo-bar", version="1.0")), 44 | # Periods 45 | ("foo.bar", stub(name="foo-bar", version="1.0")), 46 | ("foo-bar", stub(name="foo.bar", version="1.0")), 47 | # Underscores 48 | ("foo_bar", stub(name="foo-bar", version="1.0")), 49 | ("foo-bar", stub(name="foo_bar", version="1.0")), 50 | ], 51 | ) 52 | def test_canonicalization(requirements, dist, testdir, monkeypatch): 53 | testdir.makefile(".txt", requirements=requirements) 54 | monkeypatch.setattr( 55 | "pytest_reqs.pip_api.installed_distributions", lambda: {dist.name: dist} 56 | ) 57 | 58 | result = testdir.runpytest("--reqs") 59 | assert "passed" in result.stdout.str() 60 | 61 | 62 | def test_missing_requirement(mock_dist, testdir, monkeypatch): 63 | testdir.makefile(".txt", requirements="foo") 64 | monkeypatch.setattr("pytest_reqs.pip_api.installed_distributions", lambda: {}) 65 | 66 | result = testdir.runpytest("--reqs") 67 | result.stdout.fnmatch_lines(['*Distribution "foo" is not installed*', "*1 failed*"]) 68 | assert "passed" not in result.stdout.str() 69 | 70 | 71 | @pytest.mark.parametrize("requirements", ["foo==2.0", "foo>1.0", "foo<1.0"]) 72 | def test_wrong_version(requirements, mock_dist, testdir, monkeypatch): 73 | testdir.makefile(".txt", requirements=requirements) 74 | monkeypatch.setattr( 75 | "pytest_reqs.pip_api.installed_distributions", 76 | lambda: {mock_dist.name: mock_dist}, 77 | ) 78 | 79 | result = testdir.runpytest("--reqs") 80 | result.stdout.fnmatch_lines( 81 | ['*Distribution "foo" requires %s*' % (requirements), "*1 failed*"] 82 | ) 83 | assert "passed" not in result.stdout.str() 84 | 85 | 86 | @pytest.mark.parametrize("requirements", ["foo=1.0", "foo=>1.0"]) 87 | def test_invalid_requirement(requirements, mock_dist, testdir, monkeypatch): 88 | testdir.makefile(".txt", requirements=requirements) 89 | monkeypatch.setattr( 90 | "pytest_reqs.pip_api.installed_distributions", 91 | lambda: {mock_dist.name: mock_dist}, 92 | ) 93 | 94 | result = testdir.runpytest("--reqs") 95 | result.stdout.fnmatch_lines(["*Invalid requirement*", "*1 failed*"]) 96 | 97 | assert "passed" not in result.stdout.str() 98 | 99 | 100 | def test_missing_local_requirement(testdir, monkeypatch): 101 | testdir.makefile(".txt", requirements="-e ../foo") 102 | monkeypatch.setattr("pytest_reqs.pip_api.installed_distributions", lambda: {}) 103 | 104 | result = testdir.runpytest("--reqs") 105 | result.stdout.fnmatch_lines(["*foo should either be a path to a local project*"]) 106 | assert "passed" not in result.stdout.str() 107 | 108 | 109 | def test_local_requirement_ignored(testdir, monkeypatch): 110 | testdir.makefile(".txt", requirements="-e ../foo") 111 | testdir.makeini("[pytest]\nreqsignorelocal=True") 112 | monkeypatch.setattr("pytest_reqs.pip_api.installed_distributions", lambda: {}) 113 | 114 | result = testdir.runpytest("--reqs") 115 | assert "passed" in result.stdout.str() 116 | 117 | 118 | def test_local_requirement_ignored_using_dynamic_config(testdir, monkeypatch): 119 | testdir.makefile(".txt", requirements="-e ../foo") 120 | testdir.makeconftest( 121 | """ 122 | def pytest_configure(config): 123 | config.ignore_local = True 124 | """ 125 | ) 126 | monkeypatch.setattr("pytest_reqs.pip_api.installed_distributions", lambda: {}) 127 | 128 | result = testdir.runpytest("--reqs") 129 | assert "passed" in result.stdout.str() 130 | 131 | 132 | def test_no_option(testdir, monkeypatch): 133 | testdir.makefile(".txt", requirements="foo") 134 | monkeypatch.setattr("pytest_reqs.pip_api.installed_distributions", lambda: {}) 135 | 136 | result = testdir.runpytest() 137 | assert "collected 0 items" in result.stdout.str() 138 | 139 | 140 | def test_override_filenamepatterns(testdir, monkeypatch): 141 | testdir.makefile(".txt", a="foo") 142 | testdir.makefile(".txt", b="bar") 143 | testdir.makeini("[pytest]\nreqsfilenamepatterns=\n a.txt\n b.txt") 144 | monkeypatch.setattr( 145 | "pytest_reqs.pip_api.installed_distributions", 146 | lambda: { 147 | "bar": stub(name="bar", version="1.0"), 148 | "foo": stub(name="foo", version="1.0"), 149 | }, 150 | ) 151 | 152 | result = testdir.runpytest("--reqs") 153 | assert "passed" in result.stdout.str() 154 | 155 | 156 | def test_override_filenamepatterns_using_dynamic_config(testdir, monkeypatch): 157 | testdir.makefile(".txt", a="foo") 158 | testdir.makefile(".txt", b="bar") 159 | testdir.makeconftest( 160 | """ 161 | def pytest_configure(config): 162 | config.patterns = ['a.txt', 'b.txt'] 163 | """ 164 | ) 165 | monkeypatch.setattr( 166 | "pytest_reqs.pip_api.installed_distributions", 167 | lambda: { 168 | "bar": stub(name="bar", version="1.0"), 169 | "foo": stub(name="foo", version="1.0"), 170 | }, 171 | ) 172 | 173 | result = testdir.runpytest("--reqs") 174 | assert "passed" in result.stdout.str() 175 | 176 | 177 | @pytest.mark.skipif( 178 | LooseVersion("9.0.0") > LooseVersion(get_distribution("pip").version), 179 | reason="incompatible pip version", 180 | ) 181 | @pytest.mark.parametrize("requirements", ["foo", "foo==1.0"]) 182 | def test_outdated_version(requirements, testdir, monkeypatch): 183 | testdir.makefile(".txt", requirements=requirements) 184 | pip_outdated_dists_output = '[{"name": "foo", "latest_version": "1.0.1"}]' 185 | monkeypatch.setattr( 186 | "pytest_reqs.check_output", lambda *_, **__: pip_outdated_dists_output 187 | ) 188 | 189 | result = testdir.runpytest("--reqs-outdated") 190 | result.stdout.fnmatch_lines( 191 | [ 192 | '*Distribution "foo" is outdated (from -r requirements.txt (line 1)), ' 193 | "latest version is foo==1.0.1*", 194 | "*1 failed*", 195 | ] 196 | ) 197 | assert "passed" not in result.stdout.str() 198 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist= 3 | py27-pip{1911,191,1903,1902,1901,190,181,180,101,100,903,902,901,900,812,811,810,803,802,801,800,712,711,710,703,702,701,700,611,610,608,607,606,605,604,603,602,601,60}, 4 | py35-pip{1911,191,1903,1902,1901,190,181,180,101,100,903,902,901,900,812,811,810,803,802,801,800,712,711,710,703,702,701,700,611,610,608,607,606,605,604,603,602,601,60}, 5 | py36-pip{1911,191,1903,1902,1901,190,181,180,101,100,903,902,901,900,812,811,810,803,802,801,800,712,711,710}, 6 | py37-pip{1911,191,1903,1902,1901,190,181,180,101,100,903,902,901,900,812,811,810,803,802,801,800,712,711,710}, 7 | py27-pytesttrunk, 8 | test-pytest-xdist, 9 | lint 10 | 11 | [testenv] 12 | deps= 13 | -rrequirements/test.txt 14 | pip1911: pip==19.1.1 15 | pip191: pip==19.1 16 | pip1903: pip==19.0.3 17 | pip1902: pip==19.0.2 18 | pip1901: pip==19.0.1 19 | pip190: pip==19.0 20 | pip181: pip==18.1 21 | pip180: pip==18.0 22 | pip101: pip==10.0.1 23 | pip100: pip==10.0.0 24 | pip903: pip==9.0.3 25 | pip902: pip==9.0.2 26 | pip901: pip==9.0.1 27 | pip900: pip==9.0.0 28 | pip812: pip==8.1.2 29 | pip811: pip==8.1.1 30 | pip810: pip==8.1.0 31 | pip803: pip==8.0.3 32 | pip802: pip==8.0.2 33 | pip801: pip==8.0.1 34 | pip800: pip==8.0.0 35 | pip712: pip==7.1.2 36 | pip711: pip==7.1.1 37 | pip710: pip==7.1.0 38 | 39 | commands= 40 | py.test {posargs} 41 | 42 | [testenv:lint] 43 | deps= 44 | black 45 | commands= 46 | black --check . 47 | 48 | [testenv:test-pytest-xdist] 49 | basepython=python 50 | deps= 51 | -rrequirements/test.txt 52 | pytest-xdist 53 | commands= 54 | py.test -n3 {posargs} 55 | 56 | [pytest] 57 | addopts=--reqs 58 | --------------------------------------------------------------------------------