diff --git a/.circleci/config.yml b/.circleci/config.yml index 4d312e13..1f0ff779 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,3 +1,40 @@ +orbs: + win: circleci/windows@2.2.0 + +windows-tmpl: &windows-tmpl + parameters: + python-version: + type: string + executor: + name: win/default + shell: bash.exe + steps: + - checkout + - run: + name: Set up Python + command: | + set -e + . install.sh + install_windows_make + install_windows_python << parameters.python_version >> + init_venv python + - run: + name: Install dependencies + command: | + python --version + make develop + - run: + name: Run tests + command: | + export DEBUG=1 + export SERVER_FIXTURES_JENKINS_WAR= + export PACKAGES=$(./foreach.sh --quiet 'grep -q Windows setup.py && echo $PKG || true') + make test-ci + - store_test_results: + path: junit + - run: + name: Check for failures + command: make list-test-failures test-tmpl: &test-tmpl command: | @@ -6,7 +43,8 @@ test-tmpl: &test-tmpl export DEBUG=1 export SERVER_FIXTURES_HOSTNAME=127.0.0.1 export SERVER_FIXTURES_JENKINS_WAR= - cat *.egg-info/top_level.txt | xargs -Ipysrc coverage run -p --source=pysrc setup.py test -sv -ra || touch ../FAILED-$(basename $PWD) + set -x + cat *.egg-info/top_level.txt | xargs -Ipysrc coverage run -p --source=pysrc -m pytest --junitxml junit.xml -svvvv -ra || touch ../FAILED-$(basename $PWD) job-tmpl: &job-tmpl machine: @@ -43,6 +81,9 @@ job-tmpl: &job-tmpl - run: name: Install Mongodb command: sudo bash -c "source ./install.sh && install_mongodb" + - run: + name: Install Graphviz + command: sudo bash -c "source ./install.sh && install_graphviz" - run: name: Install Apache command: sudo bash -c "source ./install.sh && install_apache" @@ -148,17 +189,22 @@ job-tmpl: &job-tmpl - ./* - ./dist/* -version: 2 +version: 2.1 jobs: - py36: + python-ubuntu: <<: *job-tmpl + parameters: + python_version: + type: string environment: - PYTHON: "python3.6" - - py37: - <<: *job-tmpl + PYTHON: << parameters.python_version >> + python-windows: + <<: *windows-tmpl + parameters: + python_version: + type: string environment: - PYTHON: "python3.7" + PYTHON: << parameters.python_version >> pypi-release: docker: @@ -212,27 +258,48 @@ jobs: -n ${VERSION} \ -b "${CHANGES}" \ -soft \ - ${VERSION} /tmp/to-release/dist + "v${VERSION}" /tmp/to-release/dist workflows: version: 2 pytest-plugins: jobs: - - py36 - - py37 + - python-windows: + matrix: + parameters: + python_version: + - "python3.6" + - "python3.7" + - "python3.8" + - "python3.9" + - "python3.10" + - "python3.11" + - "python3.12" + - python-ubuntu: + matrix: + parameters: + python_version: + - "python3.6" + - "python3.7" + - "python3.8" + - "python3.9" + - "python3.10" + - "python3.11" + - "python3.12" + - "python3.13" - pypi-release: requires: - - py36 - - py37 + - python-ubuntu + - python-windows filters: branches: only: - master - publish-github-release: requires: - - py36 - - py37 + - python-ubuntu + - python-windows filters: branches: only: diff --git a/.travis.yml b/.travis.yml deleted file mode 100755 index 95edb670..00000000 --- a/.travis.yml +++ /dev/null @@ -1,66 +0,0 @@ -language: sh - -os: - - windows - -python: - - "2.7" - - "3.5" - - "3.6" - - "3.7" - -jobs: - include: - - name: "Python 2.7 on Windows" - python: "2.7" - before_install: - - set -e - - . install.sh - - install_windows_make - - install_windows_py27 - - init_venv python - - name: "Python 3.5 on Windows" - python: "3.5" - before_install: - - set -e - - . install.sh - - install_windows_make - - install_windows_py35 - - init_venv python - - name: "Python 3.6 on Windows" - python: "3.6" - before_install: - - set -e - - . install.sh - - install_windows_make - - install_windows_py36 - - init_venv python - - name: "Python 3.7 on Windows" - python: "3.7" - before_install: - - set -e - - . install.sh - - install_windows_make - - install_windows_py37 - - init_venv python - -install: - - python --version - - make develop -before_script: - - export DEBUG=1 - - export SERVER_FIXTURES_JENKINS_WAR= - # Select all packages with the Windows classifier - - export PACKAGES=$(./foreach.sh --quiet 'grep -q Windows setup.py && echo $PKG || true') -script: - - make test-ci -after_script: - - bash -c "! compgen -G 'FAILED-*'" - -cache: - directories: - - ~/AppData/Local/pip/Cache - -git: - depth: false - submodules: false diff --git a/CHANGES.md b/CHANGES.md index 2ab4659f..7b65a22e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,9 @@ ## Changelog -### 1.8.0 (2024-10-??) +### 1.8.1 (2024-10-??) + * All: Windows builds added to CircleCI + * All: Started building py3.6-py3.13 in CircleCI + +### 1.8.0 (2024-10-17) * All: Drop support for Python 2 and <3.6, removing compatibility code. * All: Use stdlib unittest.mock instead of mock package. * All: Removed usage of path.py and path in favour of pathlib. #174 #224 diff --git a/Makefile b/Makefile index 318caad2..0e8ed168 100644 --- a/Makefile +++ b/Makefile @@ -54,7 +54,18 @@ test: test-ci: rm -f FAILED-* - ./foreach.sh 'cat *.egg-info/top_level.txt | xargs -Ipysrc coverage run -p --source=pysrc setup.py test -sv -ra --timeout 120 || touch ../FAILED-$$PKG' + mkdir junit + ./foreach.sh 'cat *.egg-info/top_level.txt | xargs -Ipysrc coverage run -p --source=pysrc -m pytest --junitxml junit.xml -svvvv -ra || touch ../FAILED-$$PKG' + ./foreach.sh 'cp junit.xml ../junit/junit-$PKG.xml || true' + +list-test-failures: + @if compgen -G 'FAILED-*' > /dev/null; then \ + echo "Error: Found failure artifacts:"; \ + compgen -G 'FAILED-*'; \ + exit 1; \ + else \ + echo "No failure artifacts found."; \ + fi upload: pip install twine diff --git a/README.md b/README.md index 5982405a..e8af886f 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # A goody-bag of nifty plugins for [pytest](https://pytest.org) -OS | Build | Coverage | - ------ | ----- | -------- | - ![Linux](img/linux.png) | [![CircleCI (Linux)](https://circleci.com/gh/man-group/pytest-plugins/tree/master.svg?style=svg)](https://circleci.com/gh/man-group/pytest-plugins/tree/master) | [![Coverage Status](https://coveralls.io/repos/github/manahl/pytest-plugins/badge.svg?branch=master)](https://coveralls.io/github/manahl/pytest-plugins?branch=master) - ![Windows](img/windows.png) | [![Travic CI (Windows)](https://travis-ci.org/man-group/pytest-plugins.svg?branch=master)](https://travis-ci.org/man-group/pytest-plugins) | +OS | Build | Coverage | + ------ |------------------------------------------------------------------------------------------------------------------------------------------------------------------| -------- | + ![Linux](img/linux.png) | [![CircleCI (Linux)](https://circleci.com/gh/man-group/pytest-plugins/tree/master.svg?style=svg)](https://circleci.com/gh/man-group/pytest-plugins/tree/master) | [![Coverage Status](https://coveralls.io/repos/github/manahl/pytest-plugins/badge.svg?branch=master)](https://coveralls.io/github/manahl/pytest-plugins?branch=master) + ![Windows](img/windows.png) | [![CircleCI (Linux)](https://circleci.com/gh/man-group/pytest-plugins/tree/master.svg?style=svg)](https://circleci.com/gh/man-group/pytest-plugins/tree/master) | Plugin | Description | Supported OS | ------ | ----------- | ------------ | diff --git a/common_setup.py b/common_setup.py index 318fbc20..1c8cbe63 100644 --- a/common_setup.py +++ b/common_setup.py @@ -1,46 +1,5 @@ # Common setup.py code shared between all the projects in this repository -import sys import os -import logging - -from setuptools.command.test import test as TestCommand -from setuptools.command.egg_info import egg_info as EggInfoCommand - - -class PyTest(TestCommand): - pytest_args = [] - src_dir = None - - def initialize_options(self): - TestCommand.initialize_options(self) - - def finalize_options(self): - TestCommand.finalize_options(self) - self.test_args = [] - self.test_suite = True - - def run_tests(self): - global pytest_args - logging.basicConfig(format='%(asctime)s %(levelname)s %(name)s %(message)s', level='DEBUG') - - # import here, cause outside the eggs aren't loaded - import pytest - - self.pytest_args.extend(['--junitxml', 'junit.xml']) - errno = pytest.main(self.pytest_args) - sys.exit(errno) - - -class EggInfo(EggInfoCommand): - """ Customisation of the package metadata creation. Changes are: - - Save the test requirements into an extra called 'tests' - """ - def run(self): - if self.distribution.extras_require is None: - self.distribution.extras_require = {} - if 'tests' not in self.distribution.extras_require and hasattr(self.distribution, 'tests_require'): - self.distribution.extras_require['tests'] = self.distribution.tests_require - EggInfoCommand.run(self) def common_setup(src_dir): @@ -48,16 +7,10 @@ def common_setup(src_dir): readme_file = os.path.join(this_dir, 'README.md') changelog_file = os.path.join(this_dir, 'CHANGES.md') version_file = os.path.join(this_dir, 'VERSION') + long_description = open(readme_file).read() changelog = open(changelog_file).read() - # Gather trailing arguments for pytest, this can't be done using setuptools' api - if 'test' in sys.argv: - PyTest.pytest_args = sys.argv[sys.argv.index('test') + 1:] - if PyTest.pytest_args: - sys.argv = sys.argv[:-len(PyTest.pytest_args)] - PyTest.src_dir = src_dir - return dict( # Version is shared between all the projects in this repo version=open(version_file).read().strip(), @@ -66,7 +19,6 @@ def common_setup(src_dir): url='https://github.com/man-group/pytest-plugins', license='MIT license', platforms=['unix', 'linux'], - cmdclass={'test': PyTest, 'egg_info': EggInfo}, include_package_data=True, python_requires='>=3.6', ) diff --git a/install.sh b/install.sh index ef6cac8c..5e448f2a 100644 --- a/install.sh +++ b/install.sh @@ -36,23 +36,25 @@ function install_python_packaging { function install_python { local py=$1 sudo apt-get install -y $py $py-dev - if [ "$py" = "python3.6" ]; then - sudo apt-get install python3.6-distutils || { - curl --silent --show-error --retry 5 https://bootstrap.pypa.io/pip/3.6/get-pip.py | sudo $py + local version=$(echo $py | grep -oP '(?<=python)\d+\.\d+') + + if [ "$version" = "3.6" ] || [ "$version" = "3.7" ]; then + sudo apt-get install ${py}-distutils || { + curl --silent --show-error --retry 5 https://bootstrap.pypa.io/pip/$version/get-pip.py | sudo $py sudo $py -m pip install setuptools } + elif [ "$version" = "3.10" ] || [ "$version" = "3.11" ] || [ "$version" = "3.12" ]; then + sudo apt-get install ${py}-distutils + curl --silent --show-error --retry 5 https://bootstrap.pypa.io/get-pip.py | sudo $py else - sudo apt-get install python3.7-distutils || { - curl --silent --show-error --retry 5 https://bootstrap.pypa.io/pip/3.7/get-pip.py | sudo $py - sudo $py -m pip install setuptools - } + sudo apt-get install ${py} + curl --silent --show-error --retry 5 https://bootstrap.pypa.io/get-pip.py | sudo $py fi install_python_packaging $py } function choco_install { local args=$* - # choco fails randomly with network errors on travis, have a few goes for i in {1..5}; do choco install $args && return 0 echo 'choco install failed, log tail follows:' @@ -68,35 +70,26 @@ function install_windows_make { choco_install make --params "/InstallDir:C:\\tools\\make" } - -function install_windows_py27 { - choco_install python2 --params "/InstallDir:C:\\Python" - export PATH="/c/Python:/c/Python/Scripts:$PATH" - install_python_packaging python -} - - -function install_windows_py35 { - choco_install python --version 3.5.4 --params "/InstallDir:C:\\Python" - export PATH="/c/Python35:/c/Python35/Scripts:$PATH" - install_python_packaging python +function install_windows_python() { + if [ -z "$1" ]; then + echo "Please provide a Python version argument, e.g., 'python3.11'" + return 1 + fi + python_arg="$1" + python_version="${python_arg#python}" + major_version="${python_version%%.*}" + minor_version="${python_version#*.}" + choco_package="python${major_version}${minor_version}" + install_dir="/c/Python${major_version}${minor_version}" + choco_install "$choco_package" --params "/InstallDir:C:\\Python" -y + if [ $? -ne 0 ]; then + echo "Failed to install Python $python_version" + return 1 + fi + export PATH="$install_dir:$install_dir/Scripts:$PATH" + install_python_packaging python } - -function install_windows_py36 { - choco_install python --version 3.6.8 --params "/InstallDir:C:\\Python" - export PATH="/c/Python36:/c/Python36/Scripts:$PATH" - install_python_packaging python -} - - -function install_windows_py37 { - choco_install python --version 3.7.5 --params "/InstallDir:C:\\Python" - export PATH="/c/Python37:/c/Python37/Scripts:$PATH" - install_python_packaging python -} - - function init_venv { local py=$1 virtualenv venv --python=$py @@ -142,6 +135,10 @@ function install_redis { apt-get install -y redis-server } +function install_graphviz { + apt-get install -y graphviz +} + function install_jenkins { apt-get install -y jenkins service jenkins stop; update-rc.d jenkins disable; diff --git a/pytest-profiling/pytest_profiling.py b/pytest-profiling/pytest_profiling.py index e6316e62..c5c85d7b 100644 --- a/pytest-profiling/pytest_profiling.py +++ b/pytest-profiling/pytest_profiling.py @@ -29,7 +29,8 @@ class Profiling(object): profs = [] stripdirs = False combined = None - svg_err = None + err_msg = None + exit_code = None dot_cmd = None gprof2dot_cmd = None @@ -71,42 +72,45 @@ def pytest_sessionfinish(self, session, exitstatus): # @UnusedVariable # A handcrafted Popen pipe actually seems to work on both windows and unix: # do it in 2 subprocesses, with a pipe in between - pdot = subprocess.Popen(dot_args, stdin=subprocess.PIPE, shell=True) - pgprof = subprocess.Popen(gprof2dot_args, stdout=pdot.stdin, shell=True) - (stdoutdata1, stderrdata1) = pgprof.communicate() - (stdoutdata2, stderrdata2) = pdot.communicate() - if stderrdata1 is not None or pgprof.poll() > 0: - # error: gprof2dot - self.svg_err = 1 - elif stderrdata2 is not None or pdot.poll() > 0: - # error: dot - self.svg_err = 2 - else: - # success - self.svg_err = 0 + try: + with subprocess.Popen(gprof2dot_args, stdout=subprocess.PIPE) as pgprof: + with subprocess.Popen( + dot_args, stdin=pgprof.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) as pdot: + pgprof.stdout.close() # Allow pgprof to receive a SIGPIPE if pdot exits + stdout, stderr = pdot.communicate() + if pgprof.returncode != 0: + self.err_msg = f"gprof2dot failed with return code {pgprof.returncode}" + self.exit_code = pgprof.returncode + if pdot.returncode != 0: + self.err_msg = f"dot failed with return code {pdot.returncode}: {stderr.decode()}" + self.exit_code = pdot.returncode + else: + self.exit_code = 0 + + except subprocess.CalledProcessError as e: + self.err_msg = stderr.decode() + self.exit_code = 1 + except FileNotFoundError as e: + self.err_msg = str(e) + self.exit_code = 1 def pytest_terminal_summary(self, terminalreporter): if self.combined: terminalreporter.write("Profiling (from {prof}):\n".format(prof=self.combined)) stats = pstats.Stats(self.combined, stream=terminalreporter) if self.stripdirs: - stats.strip_dirs() + stats.strip_dirs() stats.sort_stats('cumulative').print_stats(self.element_number) if self.svg_name: - if not self.svg_err: + if not self.exit_code: # 0 - SUCCESS terminalreporter.write("SVG profile created in {svg}.\n".format(svg=self.svg_name)) else: - if self.svg_err == 1: - # 1 - GPROF2DOT ERROR - terminalreporter.write("Error creating SVG profile in {svg}.\n" - "Command failed: {cmd}".format(svg=self.svg_name, cmd=self.gprof2dot_cmd)) - elif self.svg_err == 2: - # 2 - DOT ERROR - terminalreporter.write("Error creating SVG profile in {svg}.\n" - "Command succeeded: {cmd} \n" - "Command failed: {cmd2}".format(svg=self.svg_name, cmd=self.gprof2dot_cmd, - cmd2=self.dot_cmd)) + terminalreporter.write( + f"Error when executing: {self.gprof2dot_cmd} | {self.dot_cmd} \n" + f"Error message={self.err_msg}" + ) @pytest.hookimpl(hookwrapper=True) def pytest_runtest_protocol(self, item, nextitem): @@ -143,7 +147,7 @@ def pytest_addoption(parser): help="generate profiling graph (using gprof2dot and dot -Tsvg)") group.addoption("--pstats-dir", nargs=1, help="configure the dump directory of profile data files") - group.addoption("--element-number", action="store", type="int", default=20, + group.addoption("--element-number", action="store", type=int, default=20, help="defines how many elements will display in a result") group.addoption("--strip-dirs", action="store_true", help="configure to show/hide the leading path information from file names") diff --git a/pytest-profiling/setup.py b/pytest-profiling/setup.py index 612899a7..9a0fcf5e 100644 --- a/pytest-profiling/setup.py +++ b/pytest-profiling/setup.py @@ -1,8 +1,10 @@ import sys import os + sys.path.append(os.path.dirname(os.path.dirname(__file__))) from setuptools import setup + from common_setup import common_setup classifiers = [ @@ -15,6 +17,11 @@ 'Operating System :: POSIX', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', ] install_requires = ['six', diff --git a/pytest-profiling/tests/integration/test_profile_integration.py b/pytest-profiling/tests/integration/test_profile_integration.py index f66d2e97..dddbafac 100644 --- a/pytest-profiling/tests/integration/test_profile_integration.py +++ b/pytest-profiling/tests/integration/test_profile_integration.py @@ -1,5 +1,5 @@ -from distutils.dir_util import copy_tree import shutil +import sys from pkg_resources import resource_filename, get_distribution import pytest @@ -7,19 +7,21 @@ from pytest_virtualenv import VirtualEnv -@pytest.yield_fixture(scope="session") +@pytest.fixture(scope="session") def virtualenv(): with VirtualEnv() as venv: test_dir = resource_filename("pytest_profiling", "tests/integration/profile") - venv.install_package("more-itertools") - - # Keep pytest version the same as what's running this test to ensure P27 keeps working venv.install_package("pytest=={}".format(get_distribution("pytest").version)) - venv.install_package("pytest-cov") - venv.install_package("pytest-profiling") - copy_tree(str(test_dir), str(venv.workspace)) + venv.install_package(resource_filename("pytest_profiling", ".")) + + pyversion = sys.version_info + if (pyversion.major, pyversion.minor) < (3, 8): + import distutils.dir_util + distutils.dir_util.copy_tree(str(test_dir), str(venv.workspace)) + else: + shutil.copytree(str(test_dir), str(venv.workspace), dirs_exist_ok=True) shutil.rmtree( venv.workspace / "tests" / "unit" / "__pycache__", ignore_errors=True ) diff --git a/pytest-profiling/tests/unit/test_profile.py b/pytest-profiling/tests/unit/test_profile.py index d93a301e..cf71bb19 100644 --- a/pytest-profiling/tests/unit/test_profile.py +++ b/pytest-profiling/tests/unit/test_profile.py @@ -42,24 +42,24 @@ def test_generates_svg(): plugin.gprof2dot = "/somewhere/gprof2dot" plugin.profs = [sentinel.prof] popen1 = Mock( - communicate=Mock(return_value=[None, None]), poll=Mock(return_value=0) + communicate=Mock(return_value=[None, None]), poll=Mock(return_value=0), returncode=0 ) popen2 = Mock( - communicate=Mock(return_value=[None, None]), poll=Mock(return_value=0) + communicate=Mock(return_value=[None, None]), poll=Mock(return_value=0), returncode=0 ) with patch("pstats.Stats"): - with patch("subprocess.Popen", side_effect=[popen1, popen2]) as popen: + with patch("subprocess.Popen") as popen: + popen.return_value.__enter__.side_effect = [popen1, popen2] plugin.pytest_sessionfinish(Mock(), Mock()) - calls = popen.mock_calls - assert calls[0] == call( + popen.assert_any_call( ["dot", "-Tsvg", "-o", f"{os.getcwd()}/prof/combined.svg"], - stdin=subprocess.PIPE, - shell=True, + stdin=popen1.stdout, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, ) - assert calls[1] == call( + popen.assert_any_call( ["/somewhere/gprof2dot", "-f", "pstats", f"{os.getcwd()}/prof/combined.prof"], - stdout=popen1.stdin, - shell=True, + stdout=subprocess.PIPE, ) @@ -81,13 +81,14 @@ def test_writes_summary_svg(): plugin.profs = [sentinel.prof] terminalreporter = Mock() popen1 = Mock( - communicate=Mock(return_value=[None, None]), poll=Mock(return_value=0) + communicate=Mock(return_value=[None, None]), poll=Mock(return_value=0), returncode=0 ) popen2 = Mock( - communicate=Mock(return_value=[None, None]), poll=Mock(return_value=0) + communicate=Mock(return_value=[None, None]), poll=Mock(return_value=0), returncode=0 ) with patch("pstats.Stats"): - with patch("subprocess.Popen", side_effect=[popen1, popen2]): + with patch("subprocess.Popen") as popen: + popen.return_value.__enter__.side_effect = [popen1, popen2] plugin.pytest_sessionfinish(Mock(), Mock()) plugin.pytest_terminal_summary(terminalreporter) assert "SVG" in terminalreporter.write.call_args[0][0] diff --git a/pytest-server-fixtures/pytest_server_fixtures/base.py b/pytest-server-fixtures/pytest_server_fixtures/base.py index 14487d34..10cfc11d 100644 --- a/pytest-server-fixtures/pytest_server_fixtures/base.py +++ b/pytest-server-fixtures/pytest_server_fixtures/base.py @@ -148,7 +148,7 @@ def __init__(self, hostname, port, run_cmd, run_stdin=None, env=None, cwd=None): ProcessReader(self.p, self.p.stderr, True).start() def run(self): - log.debug("Running server: %s" % ' '.join(self.run_cmd)) + log.debug("Running server: %s" % ' '.join(str(c) for c in self.run_cmd)) log.debug("CWD: %s" % self.cwd) try: if self.run_stdin: diff --git a/pytest-shutil/setup.py b/pytest-shutil/setup.py index 905b277f..dbde2193 100644 --- a/pytest-shutil/setup.py +++ b/pytest-shutil/setup.py @@ -17,11 +17,13 @@ 'Programming Language :: Python :: 3.7', ] -install_requires = ['six', - 'execnet', - 'pytest', - 'termcolor' - ] +install_requires = [ + 'six', + 'execnet', + 'pytest', + 'termcolor', + 'importlib_metadata;python_version<"3.8"', +] tests_require = ['pytest', ] diff --git a/pytest-verbose-parametrize/tests/integration/test_verbose_parametrize.py b/pytest-verbose-parametrize/tests/integration/test_verbose_parametrize.py index 5876250c..b16fa4f0 100644 --- a/pytest-verbose-parametrize/tests/integration/test_verbose_parametrize.py +++ b/pytest-verbose-parametrize/tests/integration/test_verbose_parametrize.py @@ -1,63 +1,50 @@ import sys import os -from distutils.dir_util import copy_tree -from pkg_resources import resource_filename # @UnresolvedImport +from pkg_resources import resource_filename, get_distribution # @UnresolvedImport from pytest_shutil.run import run_with_coverage TEST_DIR = resource_filename('pytest_verbose_parametrize', 'tests/integration/parametrize_ids') -PYTEST = os.path.join(os.path.dirname(sys.executable), 'py.test') - - -def _update_expected(expected, output): - """If pytest >= 4.1.0 is used, remove single quotes from expected output. - - This function allows to successfully assert output using version of pytest - with or without pytest-dev/pytest@e9b2475e2 (Display actual test ids in `--collect-only`) - introduced in version 4.1.0. - """ - pytest_410_and_above = ".py'>" not in output - return expected.replace("'", "") if pytest_410_and_above else expected +PYTEST = os.path.join(os.path.dirname(sys.executable), 'pytest') +PYTEST_VERSION = get_distribution("pytest").parsed_version +MODULE_PREFIX = "" if PYTEST_VERSION.major >= 8 else "tests/integration/parametrize_ids/tests/unit/" def test_parametrize_ids_generates_ids(pytestconfig): output = run_with_coverage([PYTEST, '--collectonly', 'tests/unit/test_parametrized.py'], pytestconfig, cd=TEST_DIR) - expected = ''' - - -''' - expected = _update_expected(expected, output) - assert expected in output + expected_lines = [f"", ""] + for line in expected_lines: + assert line in output def test_parametrize_ids_leaves_nonparametrized(pytestconfig): output = run_with_coverage([PYTEST, '--collectonly', 'tests/unit/test_non_parametrized.py'], pytestconfig, cd=TEST_DIR) - expected = ''' - -''' - expected = _update_expected(expected, output) - assert expected in output + expected_lines = [f"", ""] + for line in expected_lines: + assert line in output def test_handles_apparent_duplicates(pytestconfig): output = run_with_coverage([PYTEST, '--collectonly', 'tests/unit/test_duplicates.py'], pytestconfig, cd=TEST_DIR) - expected = ''' - - - + expected = f''' + + + ''' - expected = _update_expected(expected, output) - assert expected in output + expected_lines = expected.splitlines() + for line in expected_lines: + assert line in output def test_truncates_long_ids(pytestconfig): output = run_with_coverage([PYTEST, '--collectonly', 'tests/unit/test_long_ids.py'], pytestconfig, cd=TEST_DIR) - expected = ''' - + expected = f''' + ''' - expected = _update_expected(expected, output) - assert expected in output + expected_lines = expected.splitlines() + for line in expected_lines: + assert line in output diff --git a/pytest-virtualenv/pytest_virtualenv.py b/pytest-virtualenv/pytest_virtualenv.py index dabc15b2..92574543 100644 --- a/pytest-virtualenv/pytest_virtualenv.py +++ b/pytest-virtualenv/pytest_virtualenv.py @@ -6,9 +6,10 @@ import shutil import sys from enum import Enum +from typing import Optional, Tuple + +from importlib_metadata import distribution, distributions, PackageNotFoundError -import importlib_metadata as metadata -import pkg_resources from pytest import yield_fixture from pytest_shutil.workspace import Workspace @@ -128,7 +129,6 @@ def __init__(self, env=None, workspace=None, name='.env', python=None, args=None self.python = self.virtualenv / 'bin' / 'python' self.pip = self.virtualenv / "bin" / "pip" self.coverage = self.virtualenv / 'bin' / 'coverage' - if env is None: self.env = dict(os.environ) else: @@ -151,6 +151,12 @@ def __init__(self, env=None, workspace=None, name='.env', python=None, args=None cmd.append(str(self.virtualenv)) self.run(cmd) self._importlib_metadata_installed = False + self.pip_version = self._get_pip_version() + + def _get_pip_version(self) -> Tuple[int, ...]: + output = self.run([self.python, "-m", "pip", "--version"], capture=True) + version_number_strs = output.split(" ")[1].split(".") + return tuple(map(int, version_number_strs)) def run(self, args, **kwargs): """ @@ -197,7 +203,7 @@ def install_package(self, pkg_name, version=PackageVersion.LATEST, installer="pi """ if sys.platform == 'win32': # In virtualenv on windows "Scripts" folder is used instead of "bin". - installer = str(self.virtualenv / 'Scripts' / installer + '.exe') + installer = str(self.virtualenv / 'Scripts' / installer) + '.exe' else: installer = str(self.virtualenv / 'bin' / installer) if not self.debug: @@ -211,12 +217,19 @@ def install_package(self, pkg_name, version=PackageVersion.LATEST, installer="pi ) elif version == PackageVersion.CURRENT: dist = next( - iter([dist for dist in metadata.distributions() if _normalize(dist.name) == _normalize(pkg_name)]), None + iter([dist for dist in distributions() if _normalize(dist.name) == _normalize(pkg_name)]), None ) if dist: + pkg_location = ( + _get_editable_package_location_from_direct_url(dist.name) if self.pip_version >= (19, 3) else None + ) egg_link = _get_egg_link(dist.name) - if egg_link: - self._install_editable_package(egg_link, dist) + if pkg_location: + self.run( + f"{self.python} {installer} {installer_command} -e {pkg_location}" + ) + elif egg_link: + self._install_package_from_editable_egg_link(egg_link, dist) else: spec = "{pkg_name}=={version}".format(pkg_name=pkg_name, version=dist.version) self.run( @@ -255,7 +268,7 @@ def installed_packages(self, package_type=None): "for i in metadata.distributions(): print(i.name + ' ' + i.version + ' ' + str(i.locate_file('')))" lines = self.run([self.python, "-c", code], capture=True).split('\n') for line in [i.strip() for i in lines if i.strip()]: - name, version, location = line.split() + name, version, location = line.split(" ", 2) res[name] = PackageEntry(name, version, location) return res @@ -264,10 +277,16 @@ def _install_importlib_metadata(self): self.install_package("importlib_metadata", version=PackageVersion.CURRENT) self._importlib_metadata_installed = True - def _install_editable_package(self, egg_link, package): - python_dir = "python{}.{}".format(sys.version_info.major, sys.version_info.minor) - shutil.copy(egg_link, self.virtualenv / "lib" / python_dir / "site-packages" / egg_link.name) - easy_install_pth_path = self.virtualenv / "lib" / python_dir / "site-packages" / "easy-install.pth" + def _install_package_from_editable_egg_link(self, egg_link, package): + import pkg_resources + + if sys.platform == "win32": + shutil.copy(egg_link, self.virtualenv / "Lib" / "site-packages" / egg_link.name) + easy_install_pth_path = self.virtualenv / "Lib" / "site-packages" / "easy-install.pth" + else: + python_dir = "python{}.{}".format(sys.version_info.major, sys.version_info.minor) + shutil.copy(egg_link, self.virtualenv / "lib" / python_dir / "site-packages" / egg_link.name) + easy_install_pth_path = self.virtualenv / "lib" / python_dir / "site-packages" / "easy-install.pth" with open(easy_install_pth_path, "a") as pth, open(egg_link) as egg_link: pth.write(egg_link.read()) pth.write("\n") @@ -282,13 +301,37 @@ def _normalize(name): return re.sub(r"[-_.]+", "-", name).lower() -def _get_egg_link(pkg_name): +def _get_egg_link(package_name): for path in sys.path: - egg_link = pathlib.Path(path) / (pkg_name + ".egg-link") + egg_link = pathlib.Path(path) / (package_name + ".egg-link") if egg_link.is_file(): return egg_link return None +def _get_editable_package_location_from_direct_url(package_name: str) -> Optional[str]: + """ + Uses the PEP610 direct_url.json to get the installed location of a given + editable package. + Parameters + ---------- + package_name: The name of the package, for example "pytest_virtualenv". + + Returns + ------- + The URL of the installed package, e.g. "file:///users//workspace/pytest-plugins/pytest-virtualenv/". + """ + + try: + dist = distribution(package_name) + if dist.read_text('direct_url.json') and dist.origin.dir_info.editable: + return dist.origin.url + except PackageNotFoundError: + return None + except FileNotFoundError: + return None + return None + + def _is_extra_requirement(spec): return any(x.replace(" ", "").startswith("extra==") for x in spec.split(";")) diff --git a/pytest-virtualenv/tests/integration/test_tmpvirtualenv.py b/pytest-virtualenv/tests/integration/test_tmpvirtualenv.py index 6af1e5ff..b88b0f90 100644 --- a/pytest-virtualenv/tests/integration/test_tmpvirtualenv.py +++ b/pytest-virtualenv/tests/integration/test_tmpvirtualenv.py @@ -1,8 +1,4 @@ -import os import pathlib -import subprocess -import sys -import textwrap import pytest_virtualenv as venv