├── .coveragerc ├── .editorconfig ├── .flake8 ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── main.yml ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── CHANGES.rst ├── LICENSE ├── README.rst ├── docs ├── conf.py ├── history.rst └── index.rst ├── mypy.ini ├── ptr └── __init__.py ├── pyproject.toml ├── pytest.ini ├── setup.cfg ├── tests └── test_ptr.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | # leading `*/` for pytest-dev/pytest-cov#456 4 | */.tox/* 5 | 6 | [report] 7 | show_missing = True 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | indent_size = 4 7 | insert_final_newline = true 8 | end_of_line = lf 9 | 10 | [*.py] 11 | indent_style = space 12 | max_line_length = 88 13 | 14 | [*.{yml,yaml}] 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | 4 | # jaraco/skeleton#34 5 | max-complexity = 10 6 | 7 | extend-ignore = 8 | # Black creates whitespace before colon 9 | E203 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | tidelift: pypi/pytest-runner 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | allow: 8 | - dependency-type: "all" 9 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | matrix: 9 | python: 10 | - 3.7 11 | - 3.9 12 | - "3.10" 13 | platform: 14 | - ubuntu-latest 15 | - macos-latest 16 | - windows-latest 17 | runs-on: ${{ matrix.platform }} 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Setup Python 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: ${{ matrix.python }} 24 | - name: Install tox 25 | run: | 26 | python -m pip install tox 27 | - name: Run tests 28 | run: tox 29 | 30 | release: 31 | needs: test 32 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') 33 | runs-on: ubuntu-latest 34 | 35 | steps: 36 | - uses: actions/checkout@v2 37 | - name: Setup Python 38 | uses: actions/setup-python@v2 39 | with: 40 | python-version: "3.10" 41 | - name: Install tox 42 | run: | 43 | python -m pip install tox 44 | - name: Release 45 | run: tox -e release 46 | env: 47 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} 48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 22.1.0 4 | hooks: 5 | - id: black 6 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | python: 3 | install: 4 | - path: . 5 | extra_requirements: 6 | - docs 7 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | v6.0.1 2 | ====== 3 | 4 | * Updated Trove classifier to indicate this project is inactive. 5 | 6 | v6.0.0 7 | ====== 8 | 9 | * #49: Dropped workaround for older setuptools versions. 10 | * Require Python 3.7. 11 | 12 | v5.3.2 13 | ====== 14 | 15 | * #58: Fixed syntax issue in changelog. 16 | 17 | v5.3.1 18 | ====== 19 | 20 | * Refreshed package metadata. 21 | 22 | v5.3.0 23 | ====== 24 | 25 | * Require Python 3.6 or later. 26 | * Refreshed package metadata. 27 | 28 | 5.2 29 | === 30 | 31 | * #50: This project is deprecated. 32 | 33 | 5.1 34 | === 35 | 36 | * #49: Surgically restore support for older setuptools versions. 37 | 38 | 5.0 39 | === 40 | 41 | * #42: Prefer pyproject.toml 42 | * Refresh package metadata. 43 | * This release now intentionally introduces the changes 44 | unintionally brought about in 4.5 and 4.3, where the 45 | adoption of declarative config adds a new requirement 46 | on setuptools 30.4 or later. On systems running older 47 | setuptools, installation of pytest-runner via 48 | ``easy_install`` (or ``setup_requires``), will result 49 | in a ``DistributionNotFound`` exception. 50 | 51 | All projects should pin to ``pytest-runner < 5`` 52 | or upgrade the environment to ``setuptools >= 30.4`` 53 | (prior to invoking setup.py). 54 | 55 | 4.5.1 56 | ===== 57 | 58 | * #48: Revert changes from 4.5 - restoring project to the 59 | state at 4.4. 60 | 61 | 4.5 62 | === 63 | 64 | (Pulled from PyPI due to #43 and #48) 65 | 66 | * Packaging (skeleton) refresh, including adoption of 67 | `black `_ for style. 68 | 69 | 4.4 70 | === 71 | 72 | * #43: Detect condition where declarative config will cause 73 | errors and emit a UserWarning with guidance on necessary 74 | actions. 75 | 76 | 4.3.1 77 | ===== 78 | 79 | * #43: Re-release of 4.2 to supersede the 4.3 release which 80 | proved to be backward-incompatible in that it requires 81 | setuptools 30.4 or possibly later (to install). In the future, a 82 | backward-incompatible release will re-release these changes. 83 | For projects including pytest-runner, particularly as 84 | ``setup_requires``, if support for older setuptools is required, 85 | please pin to ``pytest-runner < 5``. 86 | 87 | 4.3 88 | === 89 | 90 | (Pulled from PyPI due to #43) 91 | 92 | * #42: Update project metadata, including pyproject.toml declaration. 93 | 94 | 4.2 95 | === 96 | 97 | * #40: Remove declared dependency and instead assert it at 98 | run time. 99 | 100 | 4.1 101 | === 102 | 103 | * #40: Declare dependency on Setuptools in package metadata. 104 | 105 | 4.0 106 | === 107 | 108 | * Drop support for Setuptools before Setuptools 27.3.0. 109 | 110 | 3.0.1 111 | ===== 112 | 113 | * #38: Fixed AttributeError when running with ``--dry-run``. 114 | ``PyTest.run()`` no longer stores nor returns the result code. 115 | Based on the commit message for `840ff4c `_, 116 | nothing has ever relied on that value. 117 | 118 | 3.0 119 | === 120 | 121 | * Dropped support for Python 2.6 and 3.1. 122 | 123 | 2.12.2 124 | ====== 125 | 126 | * #33: Packaging refresh. 127 | 128 | 2.12.1 129 | ====== 130 | 131 | * #32: Fix support for ``dependency_links``. 132 | 133 | 2.12 134 | ==== 135 | 136 | * #30: Rework support for ``--allow-hosts`` and 137 | ``--index-url``, removing dependence on 138 | ``setuptools.Distribution``'s private member. 139 | Additionally corrects logic in marker evaluation 140 | along with unit tests! 141 | 142 | 2.11.1 143 | ====== 144 | 145 | * #28: Fix logic in marker evaluation. 146 | 147 | 2.11 148 | ==== 149 | 150 | * #27: Improved wording in the README around configuration 151 | for the distutils command and pytest proper. 152 | 153 | 2.10.1 154 | ====== 155 | 156 | * #21: Avoid mutating dictionary keys during iteration. 157 | 158 | 2.10 159 | ==== 160 | 161 | * #20: Leverage technique in `setuptools 794 162 | `_ 163 | to populate PYTHONPATH during test runs such that 164 | Python subprocesses will have a dependency context 165 | comparable to the test runner. 166 | 167 | 2.9 168 | === 169 | 170 | * Added Trove Classifier indicating this package is part 171 | of the pytest framework. 172 | 173 | 2.8 174 | === 175 | 176 | * #16: Added a license file, required for membership to 177 | pytest-dev. 178 | * Releases are now made automatically by pushing a 179 | tagged release that passes tests on Python 3.5. 180 | 181 | 2.7 182 | === 183 | 184 | * Moved hosting to Github. 185 | 186 | 2.6 187 | === 188 | 189 | * Add support for un-named, environment-specific extras. 190 | 191 | 2.5.1 192 | ===== 193 | 194 | * Restore Python 2.6 compatibility. 195 | 196 | 2.5 197 | === 198 | 199 | * Moved hosting to `pytest-dev 200 | `_. 201 | 202 | 2.4 203 | === 204 | 205 | * Added `documentation `_. 206 | * Use setuptools_scm for version management and file discovery. 207 | * Updated internal packaging technique. README is now included 208 | in the package metadata. 209 | 210 | 2.3 211 | === 212 | 213 | * Use hgdistver for version management and file discovery. 214 | 215 | 2.2 216 | === 217 | 218 | * Honor ``.eggs`` directory for transient downloads as introduced in Setuptools 219 | 7.0. 220 | 221 | 2.1 222 | === 223 | 224 | * The preferred invocation is now the 'pytest' command. 225 | 226 | 2.0 227 | === 228 | 229 | * Removed support for the alternate usage. The recommended usage (as a 230 | distutils command) is now the only supported usage. 231 | * Removed support for the --junitxml parameter to the ptr command. Clients 232 | should pass the same parameter (and all other py.test arguments) to py.test 233 | via the --addopts parameter. 234 | 235 | 1.1 236 | === 237 | 238 | * Added support for --addopts to pass any arguments through to py.test. 239 | * Deprecated support for --junitxml. Use --addopts instead. --junitxml will be 240 | removed in 2.0. 241 | 242 | 1.0 243 | === 244 | 245 | Initial implementation. 246 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Jason R. Coombs 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to 5 | deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | sell copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://img.shields.io/pypi/v/pytest-runner.svg 2 | :target: `PyPI link`_ 3 | 4 | .. image:: https://img.shields.io/pypi/pyversions/pytest-runner.svg 5 | :target: `PyPI link`_ 6 | 7 | .. _PyPI link: https://pypi.org/project/pytest-runner 8 | 9 | .. image:: https://github.com/pytest-dev/pytest-runner/workflows/tests/badge.svg 10 | :target: https://github.com/pytest-dev/pytest-runner/actions?query=workflow%3A%22tests%22 11 | :alt: tests 12 | 13 | .. image:: https://img.shields.io/badge/code%20style-black-000000.svg 14 | :target: https://github.com/psf/black 15 | :alt: Code style: Black 16 | 17 | .. .. image:: https://readthedocs.org/projects/skeleton/badge/?version=latest 18 | .. :target: https://skeleton.readthedocs.io/en/latest/?badge=latest 19 | 20 | .. image:: https://img.shields.io/badge/skeleton-2022-informational 21 | :target: https://blog.jaraco.com/skeleton 22 | 23 | .. image:: https://tidelift.com/badges/package/pypi/pytest-runner 24 | :target: https://tidelift.com/subscription/pkg/pypi-pytest-runner?utm_source=pypi-pytest-runner&utm_medium=readme 25 | 26 | Setup scripts can use pytest-runner to add setup.py test support for pytest 27 | runner. 28 | 29 | Deprecation Notice 30 | ================== 31 | 32 | pytest-runner depends on deprecated features of setuptools and relies on features that break security 33 | mechanisms in pip. For example 'setup_requires' and 'tests_require' bypass ``pip --require-hashes``. 34 | See also `pypa/setuptools#1684 `_. 35 | 36 | It is recommended that you: 37 | 38 | - Remove ``'pytest-runner'`` from your ``setup_requires``, preferably removing the ``setup_requires`` option. 39 | - Remove ``'pytest'`` and any other testing requirements from ``tests_require``, preferably removing the ``tests_requires`` option. 40 | - Select a tool to bootstrap and then run tests such as tox. 41 | 42 | Usage 43 | ===== 44 | 45 | - Add 'pytest-runner' to your 'setup_requires'. Pin to '>=2.0,<3dev' (or 46 | similar) to avoid pulling in incompatible versions. 47 | - Include 'pytest' and any other testing requirements to 'tests_require'. 48 | - Invoke tests with ``setup.py pytest``. 49 | - Pass ``--index-url`` to have test requirements downloaded from an alternate 50 | index URL (unnecessary if specified for easy_install in setup.cfg). 51 | - Pass additional py.test command-line options using ``--addopts``. 52 | - Set permanent options for the ``python setup.py pytest`` command (like ``index-url``) 53 | in the ``[pytest]`` section of ``setup.cfg``. 54 | - Set permanent options for the ``py.test`` run (like ``addopts`` or ``pep8ignore``) in the ``[pytest]`` 55 | section of ``pytest.ini`` or ``tox.ini`` or put them in the ``[tool:pytest]`` 56 | section of ``setup.cfg``. See `pytest issue 567 57 | `_. 58 | - Optionally, set ``test=pytest`` in the ``[aliases]`` section of ``setup.cfg`` 59 | to cause ``python setup.py test`` to invoke pytest. 60 | 61 | Example 62 | ======= 63 | 64 | The most simple usage looks like this in setup.py:: 65 | 66 | setup( 67 | setup_requires=[ 68 | 'pytest-runner', 69 | ], 70 | tests_require=[ 71 | 'pytest', 72 | ], 73 | ) 74 | 75 | Additional dependencies require to run the tests (e.g. mock or pytest 76 | plugins) may be added to tests_require and will be downloaded and 77 | required by the session before invoking pytest. 78 | 79 | Follow `this search on github 80 | `_ 81 | for examples of real-world usage. 82 | 83 | Standalone Example 84 | ================== 85 | 86 | This technique is deprecated - if you have standalone scripts 87 | you wish to invoke with dependencies, `use pip-run 88 | `_. 89 | 90 | Although ``pytest-runner`` is typically used to add pytest test 91 | runner support to maintained packages, ``pytest-runner`` may 92 | also be used to create standalone tests. Consider `this example 93 | failure `_, 94 | reported in `jsonpickle #117 95 | `_ 96 | or `this MongoDB test 97 | `_ 98 | demonstrating a technique that works even when dependencies 99 | are required in the test. 100 | 101 | Either example file may be cloned or downloaded and simply run on 102 | any system with Python and Setuptools. It will download the 103 | specified dependencies and run the tests. Afterward, the the 104 | cloned directory can be removed and with it all trace of 105 | invoking the test. No other dependencies are needed and no 106 | system configuration is altered. 107 | 108 | Then, anyone trying to replicate the failure can do so easily 109 | and with all the power of pytest (rewritten assertions, 110 | rich comparisons, interactive debugging, extensibility through 111 | plugins, etc). 112 | 113 | As a result, the communication barrier for describing and 114 | replicating failures is made almost trivially low. 115 | 116 | Considerations 117 | ============== 118 | 119 | Conditional Requirement 120 | ----------------------- 121 | 122 | Because it uses Setuptools setup_requires, pytest-runner will install itself 123 | on every invocation of setup.py. In some cases, this causes delays for 124 | invocations of setup.py that will never invoke pytest-runner. To help avoid 125 | this contingency, consider requiring pytest-runner only when pytest 126 | is invoked:: 127 | 128 | needs_pytest = {'pytest', 'test', 'ptr'}.intersection(sys.argv) 129 | pytest_runner = ['pytest-runner'] if needs_pytest else [] 130 | 131 | # ... 132 | 133 | setup( 134 | #... 135 | setup_requires=[ 136 | #... (other setup requirements) 137 | ] + pytest_runner, 138 | ) 139 | 140 | For Enterprise 141 | ============== 142 | 143 | Available as part of the Tidelift Subscription. 144 | 145 | This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. 146 | 147 | `Learn more `_. 148 | 149 | Security Contact 150 | ================ 151 | 152 | To report a security vulnerability, please use the 153 | `Tidelift security contact `_. 154 | Tidelift will coordinate the fix and disclosure. 155 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | extensions = ['sphinx.ext.autodoc', 'jaraco.packaging.sphinx', 'rst.linker'] 5 | 6 | master_doc = "index" 7 | 8 | link_files = { 9 | '../CHANGES.rst': dict( 10 | using=dict(GH='https://github.com'), 11 | replace=[ 12 | dict( 13 | pattern=r'(Issue #|\B#)(?P\d+)', 14 | url='{package_url}/issues/{issue}', 15 | ), 16 | dict( 17 | pattern=r'(?m:^((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n)', 18 | with_scm='{text}\n{rev[timestamp]:%d %b %Y}\n', 19 | ), 20 | dict( 21 | pattern=r'PEP[- ](?P\d+)', 22 | url='https://www.python.org/dev/peps/pep-{pep_number:0>4}/', 23 | ), 24 | dict( 25 | pattern=r'Setuptools #(?P\d+)', 26 | url='https://github.com/pypa/setuptools' '/issues/{setuptools_issue}/', 27 | ), 28 | ], 29 | ) 30 | } 31 | 32 | # Be strict about any broken references: 33 | nitpicky = True 34 | 35 | # Include Python intersphinx mapping to prevent failures 36 | # jaraco/skeleton#51 37 | extensions += ['sphinx.ext.intersphinx'] 38 | intersphinx_mapping = { 39 | 'python': ('https://docs.python.org/3', None), 40 | } 41 | 42 | extensions += ['jaraco.tidelift'] 43 | -------------------------------------------------------------------------------- /docs/history.rst: -------------------------------------------------------------------------------- 1 | :tocdepth: 2 2 | 3 | .. _changes: 4 | 5 | History 6 | ******* 7 | 8 | .. include:: ../CHANGES (links).rst 9 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to |project| documentation! 2 | =================================== 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | history 8 | 9 | .. tidelift-referral-banner:: 10 | 11 | .. include:: ../README.rst 12 | 13 | Indices and tables 14 | ================== 15 | 16 | * :ref:`genindex` 17 | * :ref:`modindex` 18 | * :ref:`search` 19 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | ignore_missing_imports = True 3 | -------------------------------------------------------------------------------- /ptr/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implementation 3 | """ 4 | 5 | import os as _os 6 | import shlex as _shlex 7 | import contextlib as _contextlib 8 | import sys as _sys 9 | import operator as _operator 10 | import itertools as _itertools 11 | import warnings as _warnings 12 | 13 | import pkg_resources 14 | import setuptools.command.test as orig 15 | from setuptools import Distribution 16 | 17 | 18 | @_contextlib.contextmanager 19 | def _save_argv(repl=None): 20 | saved = _sys.argv[:] 21 | if repl is not None: 22 | _sys.argv[:] = repl 23 | try: 24 | yield saved 25 | finally: 26 | _sys.argv[:] = saved 27 | 28 | 29 | class CustomizedDist(Distribution): 30 | 31 | allow_hosts = None 32 | index_url = None 33 | 34 | def fetch_build_egg(self, req): 35 | """Specialized version of Distribution.fetch_build_egg 36 | that respects respects allow_hosts and index_url.""" 37 | from setuptools.command.easy_install import easy_install 38 | 39 | dist = Distribution({'script_args': ['easy_install']}) 40 | dist.parse_config_files() 41 | opts = dist.get_option_dict('easy_install') 42 | keep = ( 43 | 'find_links', 44 | 'site_dirs', 45 | 'index_url', 46 | 'optimize', 47 | 'site_dirs', 48 | 'allow_hosts', 49 | ) 50 | for key in list(opts): 51 | if key not in keep: 52 | del opts[key] # don't use any other settings 53 | if self.dependency_links: 54 | links = self.dependency_links[:] 55 | if 'find_links' in opts: 56 | links = opts['find_links'][1].split() + links 57 | opts['find_links'] = ('setup', links) 58 | if self.allow_hosts: 59 | opts['allow_hosts'] = ('test', self.allow_hosts) 60 | if self.index_url: 61 | opts['index_url'] = ('test', self.index_url) 62 | install_dir_func = getattr(self, 'get_egg_cache_dir', _os.getcwd) 63 | install_dir = install_dir_func() 64 | cmd = easy_install( 65 | dist, 66 | args=["x"], 67 | install_dir=install_dir, 68 | exclude_scripts=True, 69 | always_copy=False, 70 | build_directory=None, 71 | editable=False, 72 | upgrade=False, 73 | multi_version=True, 74 | no_report=True, 75 | user=False, 76 | ) 77 | cmd.ensure_finalized() 78 | return cmd.easy_install(req) 79 | 80 | 81 | class PyTest(orig.test): 82 | """ 83 | >>> import setuptools 84 | >>> dist = setuptools.Distribution() 85 | >>> cmd = PyTest(dist) 86 | """ 87 | 88 | user_options = [ 89 | ('extras', None, "Install (all) setuptools extras when running tests"), 90 | ( 91 | 'index-url=', 92 | None, 93 | "Specify an index url from which to retrieve dependencies", 94 | ), 95 | ( 96 | 'allow-hosts=', 97 | None, 98 | "Whitelist of comma-separated hosts to allow " 99 | "when retrieving dependencies", 100 | ), 101 | ( 102 | 'addopts=', 103 | None, 104 | "Additional options to be passed verbatim to the pytest runner", 105 | ), 106 | ] 107 | 108 | def initialize_options(self): 109 | self.extras = False 110 | self.index_url = None 111 | self.allow_hosts = None 112 | self.addopts = [] 113 | self.ensure_setuptools_version() 114 | 115 | @staticmethod 116 | def ensure_setuptools_version(): 117 | """ 118 | Due to the fact that pytest-runner is often required (via 119 | setup-requires directive) by toolchains that never invoke 120 | it (i.e. they're only installing the package, not testing it), 121 | instead of declaring the dependency in the package 122 | metadata, assert the requirement at run time. 123 | """ 124 | pkg_resources.require('setuptools>=27.3') 125 | 126 | def finalize_options(self): 127 | if self.addopts: 128 | self.addopts = _shlex.split(self.addopts) 129 | 130 | @staticmethod 131 | def marker_passes(marker): 132 | """ 133 | Given an environment marker, return True if the marker is valid 134 | and matches this environment. 135 | """ 136 | return ( 137 | not marker 138 | or not pkg_resources.invalid_marker(marker) 139 | and pkg_resources.evaluate_marker(marker) 140 | ) 141 | 142 | def install_dists(self, dist): 143 | """ 144 | Extend install_dists to include extras support 145 | """ 146 | return _itertools.chain( 147 | orig.test.install_dists(dist), self.install_extra_dists(dist) 148 | ) 149 | 150 | def install_extra_dists(self, dist): 151 | """ 152 | Install extras that are indicated by markers or 153 | install all extras if '--extras' is indicated. 154 | """ 155 | extras_require = dist.extras_require or {} 156 | 157 | spec_extras = ( 158 | (spec.partition(':'), reqs) for spec, reqs in extras_require.items() 159 | ) 160 | matching_extras = ( 161 | reqs 162 | for (name, sep, marker), reqs in spec_extras 163 | # include unnamed extras or all if self.extras indicated 164 | if (not name or self.extras) 165 | # never include extras that fail to pass marker eval 166 | and self.marker_passes(marker) 167 | ) 168 | results = list(map(dist.fetch_build_eggs, matching_extras)) 169 | return _itertools.chain.from_iterable(results) 170 | 171 | @staticmethod 172 | def _warn_old_setuptools(): 173 | msg = ( 174 | "pytest-runner will stop working on this version of setuptools; " 175 | "please upgrade to setuptools 30.4 or later or pin to " 176 | "pytest-runner < 5." 177 | ) 178 | ver_str = pkg_resources.get_distribution('setuptools').version 179 | ver = pkg_resources.parse_version(ver_str) 180 | if ver < pkg_resources.parse_version('30.4'): 181 | _warnings.warn(msg) 182 | 183 | def run(self): 184 | """ 185 | Override run to ensure requirements are available in this session (but 186 | don't install them anywhere). 187 | """ 188 | self._warn_old_setuptools() 189 | dist = CustomizedDist() 190 | for attr in 'allow_hosts index_url'.split(): 191 | setattr(dist, attr, getattr(self, attr)) 192 | for attr in ( 193 | 'dependency_links install_requires tests_require extras_require ' 194 | ).split(): 195 | setattr(dist, attr, getattr(self.distribution, attr)) 196 | installed_dists = self.install_dists(dist) 197 | if self.dry_run: 198 | self.announce('skipping tests (dry run)') 199 | return 200 | paths = map(_operator.attrgetter('location'), installed_dists) 201 | with self.paths_on_pythonpath(paths): 202 | with self.project_on_sys_path(): 203 | return self.run_tests() 204 | 205 | @property 206 | def _argv(self): 207 | return ['pytest'] + self.addopts 208 | 209 | def run_tests(self): 210 | """ 211 | Invoke pytest, replacing argv. Return result code. 212 | """ 213 | with _save_argv(_sys.argv[:1] + self.addopts): 214 | result_code = __import__('pytest').main() 215 | if result_code: 216 | raise SystemExit(result_code) 217 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=56", "setuptools_scm[toml]>=3.4.1"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.black] 6 | skip-string-normalization = true 7 | 8 | [tool.setuptools_scm] 9 | 10 | [pytest.enabler.black] 11 | addopts = "--black" 12 | 13 | [pytest.enabler.mypy] 14 | addopts = "--mypy" 15 | 16 | [pytest.enabler.flake8] 17 | addopts = "--flake8" 18 | 19 | [pytest.enabler.cov] 20 | addopts = "--cov" 21 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | norecursedirs=dist build .tox .eggs 3 | addopts=--doctest-modules 4 | doctest_optionflags=ALLOW_UNICODE ELLIPSIS 5 | filterwarnings= 6 | # Suppress deprecation warning in flake8 7 | ignore:SelectableGroups dict interface is deprecated::flake8 8 | 9 | # shopkeep/pytest-black#55 10 | ignore: is not using a cooperative constructor:pytest.PytestDeprecationWarning 11 | ignore:The \(fspath. py.path.local\) argument to BlackItem is deprecated.:pytest.PytestDeprecationWarning 12 | ignore:BlackItem is an Item subclass and should not be a collector:pytest.PytestWarning 13 | 14 | # tholo/pytest-flake8#83 15 | ignore: is not using a cooperative constructor:pytest.PytestDeprecationWarning 16 | ignore:The \(fspath. py.path.local\) argument to Flake8Item is deprecated.:pytest.PytestDeprecationWarning 17 | ignore:Flake8Item is an Item subclass and should not be a collector:pytest.PytestWarning 18 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = pytest-runner 3 | author = Jason R. Coombs 4 | author_email = jaraco@jaraco.com 5 | description = Invoke py.test as distutils command with dependency resolution 6 | long_description = file:README.rst 7 | url = https://github.com/pytest-dev/pytest-runner/ 8 | classifiers = 9 | Development Status :: 7 - Inactive 10 | Intended Audience :: Developers 11 | License :: OSI Approved :: MIT License 12 | Programming Language :: Python :: 3 13 | Programming Language :: Python :: 3 :: Only 14 | Framework :: Pytest 15 | 16 | [options] 17 | packages = find_namespace: 18 | include_package_data = true 19 | python_requires = >=3.7 20 | install_requires = 21 | # setuptools 27.3 is required at run time 22 | 23 | [options.packages.find] 24 | exclude = 25 | build* 26 | dist* 27 | docs* 28 | tests* 29 | 30 | [options.extras_require] 31 | testing = 32 | # upstream 33 | pytest >= 6 34 | pytest-checkdocs >= 2.4 35 | pytest-flake8 36 | pytest-black >= 0.3.7; \ 37 | # workaround for jaraco/skeleton#22 38 | python_implementation != "PyPy" 39 | pytest-cov 40 | pytest-mypy >= 0.9.1; \ 41 | # workaround for jaraco/skeleton#22 42 | python_implementation != "PyPy" 43 | pytest-enabler >= 1.0.1 44 | 45 | # local 46 | pytest-virtualenv 47 | types-setuptools 48 | 49 | docs = 50 | # upstream 51 | sphinx 52 | jaraco.packaging >= 9 53 | rst.linker >= 1.9 54 | jaraco.tidelift >= 1.4 55 | 56 | # local 57 | 58 | [options.entry_points] 59 | distutils.commands = 60 | ptr = ptr:PyTest 61 | pytest = ptr:PyTest 62 | -------------------------------------------------------------------------------- /tests/test_ptr.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import shutil 4 | import sys 5 | import tarfile 6 | import textwrap 7 | import time 8 | import itertools 9 | 10 | import pytest 11 | 12 | 13 | def DALS(s): 14 | "dedent and left-strip" 15 | return textwrap.dedent(s).lstrip() 16 | 17 | 18 | def make_sdist(dist_path, files): 19 | """ 20 | Create a simple sdist tarball at dist_path, containing the files 21 | listed in ``files`` as ``(filename, content)`` tuples. 22 | """ 23 | 24 | with tarfile.open(dist_path, 'w:gz') as dist: 25 | for filename, content in files: 26 | file_bytes = io.BytesIO(content.encode('utf-8')) 27 | file_info = tarfile.TarInfo(name=filename) 28 | file_info.size = len(file_bytes.getvalue()) 29 | file_info.mtime = int(time.time()) 30 | dist.addfile(file_info, fileobj=file_bytes) 31 | 32 | 33 | @pytest.fixture 34 | def venv(virtualenv): 35 | yield virtualenv 36 | # Workaround virtualenv not cleaning itself as it should... 37 | virtualenv.delete = True 38 | virtualenv.teardown() 39 | 40 | 41 | setuptools_reqs = ( 42 | ['setuptools', 'setuptools==38.4.1'] 43 | if sys.version_info < (3, 10) 44 | else ['setuptools', 'setuptools==49.0.0'] 45 | ) 46 | args_variants = ['', '--extras'] 47 | 48 | 49 | @pytest.mark.xfail('platform.system() == "Windows"') 50 | @pytest.mark.parametrize( 51 | 'setuptools_req, test_args', itertools.product(setuptools_reqs, args_variants) 52 | ) 53 | def test_egg_fetcher(venv, setuptools_req, test_args): 54 | test_args = test_args.split() 55 | # Install pytest & pytest-runner. 56 | venv.run('pip install pytest .', cwd=os.getcwd()) 57 | # Install setuptools version. 58 | venv.run('pip install -U'.split() + [setuptools_req]) 59 | # For debugging purposes. 60 | venv.run('pip freeze --all') 61 | # Prepare fake index. 62 | index_dir = (venv.workspace / 'index').mkdir() 63 | for n in range(5): 64 | dist_name = 'barbazquux' + str(n + 1) 65 | dist_version = '0.1' 66 | dist_sdist = '%s-%s.tar.gz' % (dist_name, dist_version) 67 | dist_dir = (index_dir / dist_name).mkdir() 68 | make_sdist( 69 | dist_dir / dist_sdist, 70 | ( 71 | ( 72 | 'setup.py', 73 | textwrap.dedent( 74 | ''' 75 | from setuptools import setup 76 | setup( 77 | name={dist_name!r}, 78 | version={dist_version!r}, 79 | py_modules=[{dist_name!r}], 80 | ) 81 | ''' 82 | ).format(dist_name=dist_name, dist_version=dist_version), 83 | ), 84 | (dist_name + '.py', ''), 85 | ), 86 | ) 87 | with (dist_dir / 'index.html').open('w') as fp: 88 | fp.write( 89 | DALS( 90 | ''' 91 | 92 | {dist_sdist}
93 | 94 | ''' 95 | ).format(dist_sdist=dist_sdist) 96 | ) 97 | # Move barbazquux1 out of the index. 98 | shutil.move(index_dir / 'barbazquux1', venv.workspace) 99 | barbazquux1_link = ( 100 | 'file://' 101 | + str(venv.workspace.abspath()) 102 | + '/barbazquux1/barbazquux1-0.1.tar.gz' 103 | + '#egg=barbazquux1-0.1' 104 | ) 105 | # Prepare fake project. 106 | project_dir = (venv.workspace / 'project-0.1').mkdir() 107 | with open(project_dir / 'setup.py', 'w') as fp: 108 | fp.write( 109 | DALS( 110 | ''' 111 | from setuptools import setup 112 | setup( 113 | name='project', 114 | version='0.1', 115 | dependency_links = [ 116 | {barbazquux1_link!r}, 117 | ], 118 | setup_requires=[ 119 | 'pytest-runner', 120 | ], 121 | install_requires=[ 122 | 'barbazquux1', 123 | ], 124 | tests_require=[ 125 | 'pytest', 126 | 'barbazquux2', 127 | ], 128 | extras_require={{ 129 | ':"{sys_platform}" in sys_platform': 'barbazquux3', 130 | ':"barbazquux" in sys_platform': 'barbazquux4', 131 | 'extra': 'barbazquux5', 132 | }} 133 | ) 134 | ''' 135 | ).format(sys_platform=sys.platform, barbazquux1_link=barbazquux1_link) 136 | ) 137 | with open(project_dir / 'setup.cfg', 'w') as fp: 138 | fp.write( 139 | DALS( 140 | ''' 141 | [easy_install] 142 | index_url = . 143 | ''' 144 | ) 145 | ) 146 | with open(project_dir / 'test_stuff.py', 'w') as fp: 147 | fp.write( 148 | DALS( 149 | ''' 150 | import pytest 151 | 152 | def test_stuff(): 153 | import barbazquux1 154 | import barbazquux2 155 | import barbazquux3 156 | with pytest.raises(ImportError): 157 | import barbazquux4 158 | if {importable_barbazquux5}: 159 | import barbazquux5 160 | else: 161 | with pytest.raises(ImportError): 162 | import barbazquux5 163 | ''' 164 | ).format(importable_barbazquux5=('--extras' in test_args)) 165 | ) 166 | # Run fake project tests. 167 | cmd = 'python setup.py pytest'.split() 168 | cmd += ['--index-url=' + index_dir.abspath()] 169 | cmd += test_args 170 | venv.run(cmd, cwd=project_dir) 171 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = python 3 | minversion = 3.2 4 | # https://github.com/jaraco/skeleton/issues/6 5 | tox_pip_extensions_ext_venv_update = true 6 | toxworkdir={env:TOX_WORK_DIR:.tox} 7 | 8 | 9 | [testenv] 10 | deps = 11 | commands = 12 | pytest {posargs} 13 | usedevelop = True 14 | extras = testing 15 | 16 | [testenv:docs] 17 | extras = 18 | docs 19 | testing 20 | changedir = docs 21 | commands = 22 | python -m sphinx -W --keep-going . {toxinidir}/build/html 23 | 24 | [testenv:release] 25 | skip_install = True 26 | deps = 27 | build 28 | twine>=3 29 | jaraco.develop>=7.1 30 | passenv = 31 | TWINE_PASSWORD 32 | GITHUB_TOKEN 33 | setenv = 34 | TWINE_USERNAME = {env:TWINE_USERNAME:__token__} 35 | commands = 36 | python -c "import shutil; shutil.rmtree('dist', ignore_errors=True)" 37 | python -m build 38 | python -m twine upload dist/* 39 | python -m jaraco.develop.create-github-release 40 | --------------------------------------------------------------------------------