Merge branch 'main' into minor-tidyup-of-terminal-py

This commit is contained in:
symonk 2021-11-02 21:07:36 +00:00
commit 8a209e16d3
39 changed files with 8344 additions and 1476 deletions

View File

@ -165,38 +165,17 @@ jobs:
if: "matrix.use_coverage" if: "matrix.use_coverage"
run: "tox -e ${{ matrix.tox_env }}-coverage" run: "tox -e ${{ matrix.tox_env }}-coverage"
- name: Upload coverage - name: Generate coverage report
if: matrix.use_coverage && github.repository == 'pytest-dev/pytest' if: "matrix.use_coverage"
env: run: python -m coverage xml
CODECOV_NAME: ${{ matrix.name }}
run: bash scripts/upload-coverage.sh -F GHA,${{ runner.os }}
linting: - name: Upload coverage to Codecov
runs-on: ubuntu-latest if: "matrix.use_coverage"
permissions: uses: codecov/codecov-action@v2
contents: read
steps:
- uses: actions/checkout@v2
with: with:
persist-credentials: false fail_ci_if_error: true
files: ./coverage.xml
- uses: actions/setup-python@v2 verbose: true
- name: set PY
run: echo "name=PY::$(python -c 'import hashlib, sys;print(hashlib.sha256(sys.version.encode()+sys.executable.encode()).hexdigest())')" >> $GITHUB_ENV
- uses: actions/cache@v2
with:
path: ~/.cache/pre-commit
key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox
- run: tox -e linting
deploy: deploy:
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') && github.repository == 'pytest-dev/pytest' if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') && github.repository == 'pytest-dev/pytest'

View File

@ -31,7 +31,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install packaging requests tabulate[widechars] pip install packaging requests tabulate[widechars] tqdm
- name: Update Plugin List - name: Update Plugin List
run: python scripts/update-plugin-list.py run: python scripts/update-plugin-list.py

View File

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 21.9b0 rev: 21.10b0
hooks: hooks:
- id: black - id: black
args: [--safe, --quiet] args: [--safe, --quiet]
@ -39,7 +39,7 @@ repos:
- id: pyupgrade - id: pyupgrade
args: [--py36-plus] args: [--py36-plus]
- repo: https://github.com/asottile/setup-cfg-fmt - repo: https://github.com/asottile/setup-cfg-fmt
rev: v1.18.0 rev: v1.19.0
hooks: hooks:
- id: setup-cfg-fmt - id: setup-cfg-fmt
args: [--max-py-version=3.10] args: [--max-py-version=3.10]

View File

@ -1,13 +1,15 @@
version: 2 version: 2
python: python:
version: 3.7
install: install:
- requirements: doc/en/requirements.txt - requirements: doc/en/requirements.txt
- method: pip - method: pip
path: . path: .
build: build:
os: ubuntu-20.04
tools:
python: "3.9"
apt_packages: apt_packages:
- inkscape - inkscape

View File

@ -96,6 +96,7 @@ David Vierra
Daw-Ran Liou Daw-Ran Liou
Debi Mishra Debi Mishra
Denis Kirisov Denis Kirisov
Denivy Braiam Rück
Dhiren Serai Dhiren Serai
Diego Russo Diego Russo
Dmitry Dygalo Dmitry Dygalo
@ -138,6 +139,7 @@ Grigorii Eremeev (budulianin)
Guido Wesdorp Guido Wesdorp
Guoqiang Zhang Guoqiang Zhang
Harald Armin Massa Harald Armin Massa
Harshna
Henk-Jaap Wagenaar Henk-Jaap Wagenaar
Holger Kohr Holger Kohr
Hugo van Kemenade Hugo van Kemenade

1
changelog/451.doc.rst Normal file
View File

@ -0,0 +1 @@
The PDF documentations list of plugins doesnt run off the page anymore.

View File

@ -0,0 +1,2 @@
The test selection options ``pytest -k`` and ``pytest -m`` now support matching
names containing forward slash (``/``) characters.

View File

@ -0,0 +1 @@
Fixed internal error when skipping doctests.

View File

@ -0,0 +1 @@
Fix `pytest -vv` crashing with an internal exception `AttributeError: 'str' object has no attribute 'relative_to'` in some cases.

View File

@ -0,0 +1 @@
Add github action to upload coverage report to codecov instead of bash uploader.

1
changelog/9242.doc.rst Normal file
View File

@ -0,0 +1 @@
Upgrade readthedocs configuration to use a [newer Ubuntu version](https://blog.readthedocs.com/new-build-specification/) with better unicode support for PDF docs.

View File

@ -19,6 +19,7 @@ import ast
import os import os
import shutil import shutil
import sys import sys
from textwrap import dedent
from typing import List from typing import List
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -39,9 +40,22 @@ autodoc_member_order = "bysource"
autodoc_typehints = "description" autodoc_typehints = "description"
todo_include_todos = 1 todo_include_todos = 1
# Use a different latex engine due to possible Unicode characters in the documentation: latex_engine = "lualatex"
# https://docs.readthedocs.io/en/stable/guides/pdf-non-ascii-languages.html
latex_engine = "xelatex" latex_elements = {
"preamble": dedent(
r"""
\directlua{
luaotfload.add_fallback("fallbacks", {
"Noto Serif CJK SC:style=Regular;",
"Symbola:Style=Regular;"
})
}
\setmainfont{FreeSerif}[RawFeature={fallback=fallbacks}]
"""
)
}
# -- General configuration ----------------------------------------------------- # -- General configuration -----------------------------------------------------
@ -446,3 +460,7 @@ def setup(app: "sphinx.application.Sphinx") -> None:
) )
sphinx.pycode.parser.VariableCommentPicker.is_final = patched_is_final sphinx.pycode.parser.VariableCommentPicker.is_final = patched_is_final
# legacypath.py monkey-patches pytest.Testdir in. Import the file so
# that autodoc can discover references to it.
import _pytest.legacypath # noqa: F401

File diff suppressed because it is too large Load Diff

View File

@ -638,7 +638,7 @@ tmpdir
:ref:`tmpdir and tmpdir_factory` :ref:`tmpdir and tmpdir_factory`
.. autofunction:: _pytest.tmpdir.tmpdir() .. autofunction:: _pytest.legacypath.LegacyTmpdirPlugin.tmpdir()
:no-auto-options: :no-auto-options:

View File

@ -1,10 +1,14 @@
import datetime import datetime
import pathlib import pathlib
import re import re
from textwrap import dedent
from textwrap import indent
import packaging.version import packaging.version
import requests import requests
import tabulate import tabulate
import wcwidth
from tqdm import tqdm
FILE_HEAD = r""" FILE_HEAD = r"""
.. _plugin-list: .. _plugin-list:
@ -14,6 +18,11 @@ Plugin List
PyPI projects that match "pytest-\*" are considered plugins and are listed PyPI projects that match "pytest-\*" are considered plugins and are listed
automatically. Packages classified as inactive are excluded. automatically. Packages classified as inactive are excluded.
.. The following conditional uses a different format for this list when
creating a PDF, because otherwise the table gets far too wide for the
page.
""" """
DEVELOPMENT_STATUS_CLASSIFIERS = ( DEVELOPMENT_STATUS_CLASSIFIERS = (
"Development Status :: 1 - Planning", "Development Status :: 1 - Planning",
@ -42,10 +51,15 @@ def escape_rst(text: str) -> str:
def iter_plugins(): def iter_plugins():
regex = r">([\d\w-]*)</a>" regex = r">([\d\w-]*)</a>"
response = requests.get("https://pypi.org/simple") response = requests.get("https://pypi.org/simple")
for match in re.finditer(regex, response.text):
matches = list(
match
for match in re.finditer(regex, response.text)
if match.groups()[0].startswith("pytest-")
)
for match in tqdm(matches, smoothing=0):
name = match.groups()[0] name = match.groups()[0]
if not name.startswith("pytest-"):
continue
response = requests.get(f"https://pypi.org/pypi/{name}/json") response = requests.get(f"https://pypi.org/pypi/{name}/json")
if response.status_code == 404: if response.status_code == 404:
# Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple but # Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple but
@ -79,22 +93,47 @@ def iter_plugins():
summary = escape_rst(info["summary"].replace("\n", "")) summary = escape_rst(info["summary"].replace("\n", ""))
yield { yield {
"name": name, "name": name,
"summary": summary, "summary": summary.strip(),
"last release": last_release, "last release": last_release,
"status": status, "status": status,
"requires": requires, "requires": requires,
} }
def plugin_definitions(plugins):
"""Return RST for the plugin list that fits better on a vertical page."""
for plugin in plugins:
yield dedent(
f"""
{plugin['name']}
*last release*: {plugin["last release"]},
*status*: {plugin["status"]},
*requires*: {plugin["requires"]}
{plugin["summary"]}
"""
)
def main(): def main():
plugins = list(iter_plugins()) plugins = list(iter_plugins())
plugin_table = tabulate.tabulate(plugins, headers="keys", tablefmt="rst")
plugin_list = pathlib.Path("doc", "en", "reference", "plugin_list.rst") reference_dir = pathlib.Path("doc", "en", "reference")
plugin_list = reference_dir / "plugin_list.rst"
with plugin_list.open("w") as f: with plugin_list.open("w") as f:
f.write(FILE_HEAD) f.write(FILE_HEAD)
f.write(f"This list contains {len(plugins)} plugins.\n\n") f.write(f"This list contains {len(plugins)} plugins.\n\n")
f.write(plugin_table) f.write(".. only:: not latex\n\n")
f.write("\n")
wcwidth # reference library that must exist for tabulate to work
plugin_table = tabulate.tabulate(plugins, headers="keys", tablefmt="rst")
f.write(indent(plugin_table, " "))
f.write("\n\n")
f.write(".. only:: latex\n\n")
f.write(indent("".join(plugin_definitions(plugins)), " "))
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -1,28 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
set -x
# Install coverage.
if [[ -z ${TOXENV+x} || -z $TOXENV ]]; then
python -m pip install coverage
else
# Add last TOXENV to $PATH.
PATH="$PWD/.tox/${TOXENV##*,}/bin:$PATH"
fi
# Run coverage.
python -m coverage xml
# Download and verify latest Codecov bash uploader.
# Set --connect-timeout to work around https://github.com/curl/curl/issues/4461
curl --silent --show-error --location --connect-timeout 5 --retry 6 -o codecov https://codecov.io/bash
VERSION=$(grep --only-matching 'VERSION=\"[0-9\.]*\"' codecov | cut -d'"' -f2)
if command -v sha256sum; then
sha256sum --check --strict --ignore-missing --quiet <(curl --silent "https://raw.githubusercontent.com/codecov/codecov-bash/${VERSION}/SHA256SUM")
else
shasum --algorithm 256 --check --strict --ignore-missing --quiet <(curl --silent "https://raw.githubusercontent.com/codecov/codecov-bash/${VERSION}/SHA256SUM")
fi
# Upload coverage.
bash codecov -Z -X fix -f coverage.xml "$@"

View File

@ -20,8 +20,6 @@ from .reports import CollectReport
from _pytest import nodes from _pytest import nodes
from _pytest._io import TerminalWriter from _pytest._io import TerminalWriter
from _pytest.compat import final from _pytest.compat import final
from _pytest.compat import LEGACY_PATH
from _pytest.compat import legacy_path
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import ExitCode from _pytest.config import ExitCode
from _pytest.config import hookimpl from _pytest.config import hookimpl
@ -142,13 +140,6 @@ class Cache:
res.mkdir(exist_ok=True, parents=True) res.mkdir(exist_ok=True, parents=True)
return res return res
def makedir(self, name: str) -> LEGACY_PATH:
"""Return a directory path object with the given name.
Same as :func:`mkdir`, but returns a legacy py path instance.
"""
return legacy_path(self.mkdir(name))
def _getvaluepath(self, key: str) -> Path: def _getvaluepath(self, key: str) -> Path:
return self._cachedir.joinpath(self._CACHE_PREFIX_VALUES, Path(key)) return self._cachedir.joinpath(self._CACHE_PREFIX_VALUES, Path(key))

View File

@ -49,8 +49,6 @@ from _pytest._code import filter_traceback
from _pytest._io import TerminalWriter from _pytest._io import TerminalWriter
from _pytest.compat import final from _pytest.compat import final
from _pytest.compat import importlib_metadata from _pytest.compat import importlib_metadata
from _pytest.compat import LEGACY_PATH
from _pytest.compat import legacy_path
from _pytest.outcomes import fail from _pytest.outcomes import fail
from _pytest.outcomes import Skipped from _pytest.outcomes import Skipped
from _pytest.pathlib import absolutepath from _pytest.pathlib import absolutepath
@ -240,6 +238,7 @@ default_plugins = essential_plugins + (
"unittest", "unittest",
"capture", "capture",
"skipping", "skipping",
"legacypath",
"tmpdir", "tmpdir",
"monkeypatch", "monkeypatch",
"recwarn", "recwarn",
@ -949,17 +948,6 @@ class Config:
self.cache: Optional[Cache] = None self.cache: Optional[Cache] = None
@property
def invocation_dir(self) -> LEGACY_PATH:
"""The directory from which pytest was invoked.
Prefer to use :attr:`invocation_params.dir <InvocationParams.dir>`,
which is a :class:`pathlib.Path`.
:type: LEGACY_PATH
"""
return legacy_path(str(self.invocation_params.dir))
@property @property
def rootpath(self) -> Path: def rootpath(self) -> Path:
"""The path to the :ref:`rootdir <rootdir>`. """The path to the :ref:`rootdir <rootdir>`.
@ -970,16 +958,6 @@ class Config:
""" """
return self._rootpath return self._rootpath
@property
def rootdir(self) -> LEGACY_PATH:
"""The path to the :ref:`rootdir <rootdir>`.
Prefer to use :attr:`rootpath`, which is a :class:`pathlib.Path`.
:type: LEGACY_PATH
"""
return legacy_path(str(self.rootpath))
@property @property
def inipath(self) -> Optional[Path]: def inipath(self) -> Optional[Path]:
"""The path to the :ref:`configfile <configfiles>`. """The path to the :ref:`configfile <configfiles>`.
@ -990,16 +968,6 @@ class Config:
""" """
return self._inipath return self._inipath
@property
def inifile(self) -> Optional[LEGACY_PATH]:
"""The path to the :ref:`configfile <configfiles>`.
Prefer to use :attr:`inipath`, which is a :class:`pathlib.Path`.
:type: Optional[LEGACY_PATH]
"""
return legacy_path(str(self.inipath)) if self.inipath else None
def add_cleanup(self, func: Callable[[], None]) -> None: def add_cleanup(self, func: Callable[[], None]) -> None:
"""Add a function to be called when the config object gets out of """Add a function to be called when the config object gets out of
use (usually coninciding with pytest_unconfigure).""" use (usually coninciding with pytest_unconfigure)."""
@ -1400,6 +1368,12 @@ class Config:
self._inicache[name] = val = self._getini(name) self._inicache[name] = val = self._getini(name)
return val return val
# Meant for easy monkeypatching by legacypath plugin.
# Can be inlined back (with no cover removed) once legacypath is gone.
def _getini_unknown_type(self, name: str, type: str, value: Union[str, List[str]]):
msg = f"unknown configuration type: {type}"
raise ValueError(msg, value) # pragma: no cover
def _getini(self, name: str): def _getini(self, name: str):
try: try:
description, type, default = self._parser._inidict[name] description, type, default = self._parser._inidict[name]
@ -1432,13 +1406,7 @@ class Config:
# a_line_list = ["tests", "acceptance"] # a_line_list = ["tests", "acceptance"]
# in this case, we already have a list ready to use. # in this case, we already have a list ready to use.
# #
if type == "pathlist": if type == "paths":
# TODO: This assert is probably not valid in all cases.
assert self.inipath is not None
dp = self.inipath.parent
input_values = shlex.split(value) if isinstance(value, str) else value
return [legacy_path(str(dp / x)) for x in input_values]
elif type == "paths":
# TODO: This assert is probably not valid in all cases. # TODO: This assert is probably not valid in all cases.
assert self.inipath is not None assert self.inipath is not None
dp = self.inipath.parent dp = self.inipath.parent
@ -1453,9 +1421,12 @@ class Config:
return value return value
elif type == "bool": elif type == "bool":
return _strtobool(str(value).strip()) return _strtobool(str(value).strip())
else: elif type == "string":
assert type in [None, "string"]
return value return value
elif type is None:
return value
else:
return self._getini_unknown_type(name, type, value)
def _getconftest_pathlist( def _getconftest_pathlist(
self, name: str, path: Path, rootpath: Path self, name: str, path: Path, rootpath: Path

View File

@ -504,12 +504,18 @@ class DoctestModule(pytest.Module):
def _find_lineno(self, obj, source_lines): def _find_lineno(self, obj, source_lines):
"""Doctest code does not take into account `@property`, this """Doctest code does not take into account `@property`, this
is a hackish way to fix it. is a hackish way to fix it. https://bugs.python.org/issue17446
https://bugs.python.org/issue17446 Wrapped Doctests will need to be unwrapped so the correct
line number is returned. This will be reported upstream. #8796
""" """
if isinstance(obj, property): if isinstance(obj, property):
obj = getattr(obj, "fget", obj) obj = getattr(obj, "fget", obj)
if hasattr(obj, "__wrapped__"):
# Get the main obj in case of it being wrapped
obj = inspect.unwrap(obj)
# Type ignored because this is a private function. # Type ignored because this is a private function.
return super()._find_lineno( # type:ignore[misc] return super()._find_lineno( # type:ignore[misc]
obj, obj,

View File

@ -46,8 +46,6 @@ from _pytest.compat import getfuncargnames
from _pytest.compat import getimfunc from _pytest.compat import getimfunc
from _pytest.compat import getlocation from _pytest.compat import getlocation
from _pytest.compat import is_generator from _pytest.compat import is_generator
from _pytest.compat import LEGACY_PATH
from _pytest.compat import legacy_path
from _pytest.compat import NOTSET from _pytest.compat import NOTSET
from _pytest.compat import safe_getattr from _pytest.compat import safe_getattr
from _pytest.config import _PluggyPlugin from _pytest.config import _PluggyPlugin
@ -528,11 +526,6 @@ class FixtureRequest:
raise AttributeError(f"module not available in {self.scope}-scoped context") raise AttributeError(f"module not available in {self.scope}-scoped context")
return self._pyfuncitem.getparent(_pytest.python.Module).obj return self._pyfuncitem.getparent(_pytest.python.Module).obj
@property
def fspath(self) -> LEGACY_PATH:
"""(deprecated) The file system path of the test module which collected this test."""
return legacy_path(self.path)
@property @property
def path(self) -> Path: def path(self) -> Path:
if self.scope not in ("function", "class", "module", "package"): if self.scope not in ("function", "class", "module", "package"):

454
src/_pytest/legacypath.py Normal file
View File

@ -0,0 +1,454 @@
"""Add backward compatibility support for the legacy py path type."""
import shlex
import subprocess
from pathlib import Path
from typing import List
from typing import Optional
from typing import TYPE_CHECKING
from typing import Union
import attr
from iniconfig import SectionWrapper
import pytest
from _pytest.compat import final
from _pytest.compat import LEGACY_PATH
from _pytest.compat import legacy_path
from _pytest.deprecated import check_ispytest
from _pytest.nodes import Node
from _pytest.terminal import TerminalReporter
if TYPE_CHECKING:
from typing_extensions import Final
import pexpect
@final
class Testdir:
"""
Similar to :class:`Pytester`, but this class works with legacy legacy_path objects instead.
All methods just forward to an internal :class:`Pytester` instance, converting results
to `legacy_path` objects as necessary.
"""
__test__ = False
CLOSE_STDIN: "Final" = pytest.Pytester.CLOSE_STDIN
TimeoutExpired: "Final" = pytest.Pytester.TimeoutExpired
def __init__(self, pytester: pytest.Pytester, *, _ispytest: bool = False) -> None:
check_ispytest(_ispytest)
self._pytester = pytester
@property
def tmpdir(self) -> LEGACY_PATH:
"""Temporary directory where tests are executed."""
return legacy_path(self._pytester.path)
@property
def test_tmproot(self) -> LEGACY_PATH:
return legacy_path(self._pytester._test_tmproot)
@property
def request(self):
return self._pytester._request
@property
def plugins(self):
return self._pytester.plugins
@plugins.setter
def plugins(self, plugins):
self._pytester.plugins = plugins
@property
def monkeypatch(self) -> pytest.MonkeyPatch:
return self._pytester._monkeypatch
def make_hook_recorder(self, pluginmanager) -> pytest.HookRecorder:
"""See :meth:`Pytester.make_hook_recorder`."""
return self._pytester.make_hook_recorder(pluginmanager)
def chdir(self) -> None:
"""See :meth:`Pytester.chdir`."""
return self._pytester.chdir()
def finalize(self) -> None:
"""See :meth:`Pytester._finalize`."""
return self._pytester._finalize()
def makefile(self, ext, *args, **kwargs) -> LEGACY_PATH:
"""See :meth:`Pytester.makefile`."""
if ext and not ext.startswith("."):
# pytester.makefile is going to throw a ValueError in a way that
# testdir.makefile did not, because
# pathlib.Path is stricter suffixes than py.path
# This ext arguments is likely user error, but since testdir has
# allowed this, we will prepend "." as a workaround to avoid breaking
# testdir usage that worked before
ext = "." + ext
return legacy_path(self._pytester.makefile(ext, *args, **kwargs))
def makeconftest(self, source) -> LEGACY_PATH:
"""See :meth:`Pytester.makeconftest`."""
return legacy_path(self._pytester.makeconftest(source))
def makeini(self, source) -> LEGACY_PATH:
"""See :meth:`Pytester.makeini`."""
return legacy_path(self._pytester.makeini(source))
def getinicfg(self, source: str) -> SectionWrapper:
"""See :meth:`Pytester.getinicfg`."""
return self._pytester.getinicfg(source)
def makepyprojecttoml(self, source) -> LEGACY_PATH:
"""See :meth:`Pytester.makepyprojecttoml`."""
return legacy_path(self._pytester.makepyprojecttoml(source))
def makepyfile(self, *args, **kwargs) -> LEGACY_PATH:
"""See :meth:`Pytester.makepyfile`."""
return legacy_path(self._pytester.makepyfile(*args, **kwargs))
def maketxtfile(self, *args, **kwargs) -> LEGACY_PATH:
"""See :meth:`Pytester.maketxtfile`."""
return legacy_path(self._pytester.maketxtfile(*args, **kwargs))
def syspathinsert(self, path=None) -> None:
"""See :meth:`Pytester.syspathinsert`."""
return self._pytester.syspathinsert(path)
def mkdir(self, name) -> LEGACY_PATH:
"""See :meth:`Pytester.mkdir`."""
return legacy_path(self._pytester.mkdir(name))
def mkpydir(self, name) -> LEGACY_PATH:
"""See :meth:`Pytester.mkpydir`."""
return legacy_path(self._pytester.mkpydir(name))
def copy_example(self, name=None) -> LEGACY_PATH:
"""See :meth:`Pytester.copy_example`."""
return legacy_path(self._pytester.copy_example(name))
def getnode(
self, config: pytest.Config, arg
) -> Optional[Union[pytest.Item, pytest.Collector]]:
"""See :meth:`Pytester.getnode`."""
return self._pytester.getnode(config, arg)
def getpathnode(self, path):
"""See :meth:`Pytester.getpathnode`."""
return self._pytester.getpathnode(path)
def genitems(
self, colitems: List[Union[pytest.Item, pytest.Collector]]
) -> List[pytest.Item]:
"""See :meth:`Pytester.genitems`."""
return self._pytester.genitems(colitems)
def runitem(self, source):
"""See :meth:`Pytester.runitem`."""
return self._pytester.runitem(source)
def inline_runsource(self, source, *cmdlineargs):
"""See :meth:`Pytester.inline_runsource`."""
return self._pytester.inline_runsource(source, *cmdlineargs)
def inline_genitems(self, *args):
"""See :meth:`Pytester.inline_genitems`."""
return self._pytester.inline_genitems(*args)
def inline_run(self, *args, plugins=(), no_reraise_ctrlc: bool = False):
"""See :meth:`Pytester.inline_run`."""
return self._pytester.inline_run(
*args, plugins=plugins, no_reraise_ctrlc=no_reraise_ctrlc
)
def runpytest_inprocess(self, *args, **kwargs) -> pytest.RunResult:
"""See :meth:`Pytester.runpytest_inprocess`."""
return self._pytester.runpytest_inprocess(*args, **kwargs)
def runpytest(self, *args, **kwargs) -> pytest.RunResult:
"""See :meth:`Pytester.runpytest`."""
return self._pytester.runpytest(*args, **kwargs)
def parseconfig(self, *args) -> pytest.Config:
"""See :meth:`Pytester.parseconfig`."""
return self._pytester.parseconfig(*args)
def parseconfigure(self, *args) -> pytest.Config:
"""See :meth:`Pytester.parseconfigure`."""
return self._pytester.parseconfigure(*args)
def getitem(self, source, funcname="test_func"):
"""See :meth:`Pytester.getitem`."""
return self._pytester.getitem(source, funcname)
def getitems(self, source):
"""See :meth:`Pytester.getitems`."""
return self._pytester.getitems(source)
def getmodulecol(self, source, configargs=(), withinit=False):
"""See :meth:`Pytester.getmodulecol`."""
return self._pytester.getmodulecol(
source, configargs=configargs, withinit=withinit
)
def collect_by_name(
self, modcol: pytest.Collector, name: str
) -> Optional[Union[pytest.Item, pytest.Collector]]:
"""See :meth:`Pytester.collect_by_name`."""
return self._pytester.collect_by_name(modcol, name)
def popen(
self,
cmdargs,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
stdin=CLOSE_STDIN,
**kw,
):
"""See :meth:`Pytester.popen`."""
return self._pytester.popen(cmdargs, stdout, stderr, stdin, **kw)
def run(self, *cmdargs, timeout=None, stdin=CLOSE_STDIN) -> pytest.RunResult:
"""See :meth:`Pytester.run`."""
return self._pytester.run(*cmdargs, timeout=timeout, stdin=stdin)
def runpython(self, script) -> pytest.RunResult:
"""See :meth:`Pytester.runpython`."""
return self._pytester.runpython(script)
def runpython_c(self, command):
"""See :meth:`Pytester.runpython_c`."""
return self._pytester.runpython_c(command)
def runpytest_subprocess(self, *args, timeout=None) -> pytest.RunResult:
"""See :meth:`Pytester.runpytest_subprocess`."""
return self._pytester.runpytest_subprocess(*args, timeout=timeout)
def spawn_pytest(
self, string: str, expect_timeout: float = 10.0
) -> "pexpect.spawn":
"""See :meth:`Pytester.spawn_pytest`."""
return self._pytester.spawn_pytest(string, expect_timeout=expect_timeout)
def spawn(self, cmd: str, expect_timeout: float = 10.0) -> "pexpect.spawn":
"""See :meth:`Pytester.spawn`."""
return self._pytester.spawn(cmd, expect_timeout=expect_timeout)
def __repr__(self) -> str:
return f"<Testdir {self.tmpdir!r}>"
def __str__(self) -> str:
return str(self.tmpdir)
pytest.Testdir = Testdir # type: ignore[attr-defined]
class LegacyTestdirPlugin:
@staticmethod
@pytest.fixture
def testdir(pytester: pytest.Pytester) -> Testdir:
"""
Identical to :fixture:`pytester`, and provides an instance whose methods return
legacy ``LEGACY_PATH`` objects instead when applicable.
New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`.
"""
return Testdir(pytester, _ispytest=True)
@final
@attr.s(init=False, auto_attribs=True)
class TempdirFactory:
"""Backward compatibility wrapper that implements :class:``_pytest.compat.LEGACY_PATH``
for :class:``TempPathFactory``."""
_tmppath_factory: pytest.TempPathFactory
def __init__(
self, tmppath_factory: pytest.TempPathFactory, *, _ispytest: bool = False
) -> None:
check_ispytest(_ispytest)
self._tmppath_factory = tmppath_factory
def mktemp(self, basename: str, numbered: bool = True) -> LEGACY_PATH:
"""Same as :meth:`TempPathFactory.mktemp`, but returns a ``_pytest.compat.LEGACY_PATH`` object."""
return legacy_path(self._tmppath_factory.mktemp(basename, numbered).resolve())
def getbasetemp(self) -> LEGACY_PATH:
"""Backward compat wrapper for ``_tmppath_factory.getbasetemp``."""
return legacy_path(self._tmppath_factory.getbasetemp().resolve())
pytest.TempdirFactory = TempdirFactory # type: ignore[attr-defined]
class LegacyTmpdirPlugin:
@staticmethod
@pytest.fixture(scope="session")
def tmpdir_factory(request: pytest.FixtureRequest) -> TempdirFactory:
"""Return a :class:`pytest.TempdirFactory` instance for the test session."""
# Set dynamically by pytest_configure().
return request.config._tmpdirhandler # type: ignore
@staticmethod
@pytest.fixture
def tmpdir(tmp_path: Path) -> LEGACY_PATH:
"""Return a temporary directory path object which is unique to each test
function invocation, created as a sub directory of the base temporary
directory.
By default, a new base temporary directory is created each test session,
and old bases are removed after 3 sessions, to aid in debugging. If
``--basetemp`` is used then it is cleared each session. See :ref:`base
temporary directory`.
The returned object is a `legacy_path`_ object.
.. _legacy_path: https://py.readthedocs.io/en/latest/path.html
"""
return legacy_path(tmp_path)
def Cache_makedir(self: pytest.Cache, name: str) -> LEGACY_PATH:
"""Return a directory path object with the given name.
Same as :func:`mkdir`, but returns a legacy py path instance.
"""
return legacy_path(self.mkdir(name))
def FixtureRequest_fspath(self: pytest.FixtureRequest) -> LEGACY_PATH:
"""(deprecated) The file system path of the test module which collected this test."""
return legacy_path(self.path)
def TerminalReporter_startdir(self: TerminalReporter) -> LEGACY_PATH:
"""The directory from which pytest was invoked.
Prefer to use ``startpath`` which is a :class:`pathlib.Path`.
:type: LEGACY_PATH
"""
return legacy_path(self.startpath)
def Config_invocation_dir(self: pytest.Config) -> LEGACY_PATH:
"""The directory from which pytest was invoked.
Prefer to use :attr:`invocation_params.dir <InvocationParams.dir>`,
which is a :class:`pathlib.Path`.
:type: LEGACY_PATH
"""
return legacy_path(str(self.invocation_params.dir))
def Config_rootdir(self: pytest.Config) -> LEGACY_PATH:
"""The path to the :ref:`rootdir <rootdir>`.
Prefer to use :attr:`rootpath`, which is a :class:`pathlib.Path`.
:type: LEGACY_PATH
"""
return legacy_path(str(self.rootpath))
def Config_inifile(self: pytest.Config) -> Optional[LEGACY_PATH]:
"""The path to the :ref:`configfile <configfiles>`.
Prefer to use :attr:`inipath`, which is a :class:`pathlib.Path`.
:type: Optional[LEGACY_PATH]
"""
return legacy_path(str(self.inipath)) if self.inipath else None
def Session_stardir(self: pytest.Session) -> LEGACY_PATH:
"""The path from which pytest was invoked.
Prefer to use ``startpath`` which is a :class:`pathlib.Path`.
:type: LEGACY_PATH
"""
return legacy_path(self.startpath)
def Config__getini_unknown_type(
self, name: str, type: str, value: Union[str, List[str]]
):
if type == "pathlist":
# TODO: This assert is probably not valid in all cases.
assert self.inipath is not None
dp = self.inipath.parent
input_values = shlex.split(value) if isinstance(value, str) else value
return [legacy_path(str(dp / x)) for x in input_values]
else:
raise ValueError(f"unknown configuration type: {type}", value)
def Node_fspath(self: Node) -> LEGACY_PATH:
"""(deprecated) returns a legacy_path copy of self.path"""
return legacy_path(self.path)
def Node_fspath_set(self: Node, value: LEGACY_PATH) -> None:
self.path = Path(value)
def pytest_configure(config: pytest.Config) -> None:
mp = pytest.MonkeyPatch()
config.add_cleanup(mp.undo)
if config.pluginmanager.has_plugin("pytester"):
config.pluginmanager.register(LegacyTestdirPlugin, "legacypath-pytester")
if config.pluginmanager.has_plugin("tmpdir"):
# Create TmpdirFactory and attach it to the config object.
#
# This is to comply with existing plugins which expect the handler to be
# available at pytest_configure time, but ideally should be moved entirely
# to the tmpdir_factory session fixture.
try:
tmp_path_factory = config._tmp_path_factory # type: ignore[attr-defined]
except AttributeError:
# tmpdir plugin is blocked.
pass
else:
_tmpdirhandler = TempdirFactory(tmp_path_factory, _ispytest=True)
mp.setattr(config, "_tmpdirhandler", _tmpdirhandler, raising=False)
config.pluginmanager.register(LegacyTmpdirPlugin, "legacypath-tmpdir")
# Add Cache.makedir().
mp.setattr(pytest.Cache, "makedir", Cache_makedir, raising=False)
# Add FixtureRequest.fspath property.
mp.setattr(
pytest.FixtureRequest, "fspath", property(FixtureRequest_fspath), raising=False
)
# Add TerminalReporter.startdir property.
mp.setattr(
TerminalReporter, "startdir", property(TerminalReporter_startdir), raising=False
)
# Add Config.{invocation_dir,rootdir,inifile} properties.
mp.setattr(
pytest.Config, "invocation_dir", property(Config_invocation_dir), raising=False
)
mp.setattr(pytest.Config, "rootdir", property(Config_rootdir), raising=False)
mp.setattr(pytest.Config, "inifile", property(Config_inifile), raising=False)
# Add Session.startdir property.
mp.setattr(pytest.Session, "startdir", property(Session_stardir), raising=False)
# Add pathlist configuration type.
mp.setattr(pytest.Config, "_getini_unknown_type", Config__getini_unknown_type)
# Add Node.fspath property.
mp.setattr(Node, "fspath", property(Node_fspath, Node_fspath_set), raising=False)

View File

@ -25,8 +25,6 @@ import attr
import _pytest._code import _pytest._code
from _pytest import nodes from _pytest import nodes
from _pytest.compat import final from _pytest.compat import final
from _pytest.compat import LEGACY_PATH
from _pytest.compat import legacy_path
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import directory_arg from _pytest.config import directory_arg
from _pytest.config import ExitCode from _pytest.config import ExitCode
@ -504,16 +502,6 @@ class Session(nodes.FSCollector):
""" """
return self.config.invocation_params.dir return self.config.invocation_params.dir
@property
def stardir(self) -> LEGACY_PATH:
"""The path from which pytest was invoked.
Prefer to use ``startpath`` which is a :class:`pathlib.Path`.
:type: LEGACY_PATH
"""
return legacy_path(self.startpath)
def _node_location_to_relpath(self, node_path: Path) -> str: def _node_location_to_relpath(self, node_path: Path) -> str:
# bestrelpath is a quite slow function. # bestrelpath is a quite slow function.
return self._bestrelpathcache[node_path] return self._bestrelpathcache[node_path]

View File

@ -6,7 +6,7 @@ expression: expr? EOF
expr: and_expr ('or' and_expr)* expr: and_expr ('or' and_expr)*
and_expr: not_expr ('and' not_expr)* and_expr: not_expr ('and' not_expr)*
not_expr: 'not' not_expr | '(' expr ')' | ident not_expr: 'not' not_expr | '(' expr ')' | ident
ident: (\w|:|\+|-|\.|\[|\]|\\)+ ident: (\w|:|\+|-|\.|\[|\]|\\|/)+
The semantics are: The semantics are:
@ -88,7 +88,7 @@ class Scanner:
yield Token(TokenType.RPAREN, ")", pos) yield Token(TokenType.RPAREN, ")", pos)
pos += 1 pos += 1
else: else:
match = re.match(r"(:?\w|:|\+|-|\.|\[|\]|\\)+", input[pos:]) match = re.match(r"(:?\w|:|\+|-|\.|\[|\]|\\|/)+", input[pos:])
if match: if match:
value = match.group(0) value = match.group(0)
if value == "or": if value == "or":

View File

@ -24,7 +24,6 @@ from _pytest._code.code import ExceptionInfo
from _pytest._code.code import TerminalRepr from _pytest._code.code import TerminalRepr
from _pytest.compat import cached_property from _pytest.compat import cached_property
from _pytest.compat import LEGACY_PATH from _pytest.compat import LEGACY_PATH
from _pytest.compat import legacy_path
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import ConftestImportFailure from _pytest.config import ConftestImportFailure
from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
@ -238,15 +237,6 @@ class Node(metaclass=NodeMeta):
# Deprecated alias. Was never public. Can be removed in a few releases. # Deprecated alias. Was never public. Can be removed in a few releases.
self._store = self.stash self._store = self.stash
@property
def fspath(self) -> LEGACY_PATH:
"""(deprecated) returns a legacy_path copy of self.path"""
return legacy_path(self.path)
@fspath.setter
def fspath(self, value: LEGACY_PATH) -> None:
self.path = Path(value)
@classmethod @classmethod
def from_parent(cls, parent: "Node", **kw): def from_parent(cls, parent: "Node", **kw):
"""Public constructor for Nodes. """Public constructor for Nodes.

View File

@ -685,6 +685,8 @@ def bestrelpath(directory: Path, dest: Path) -> str:
If no such path can be determined, returns dest. If no such path can be determined, returns dest.
""" """
assert isinstance(directory, Path)
assert isinstance(dest, Path)
if dest == directory: if dest == directory:
return os.curdir return os.curdir
# Find the longest common directory. # Find the longest common directory.

View File

@ -40,8 +40,6 @@ from _pytest import timing
from _pytest._code import Source from _pytest._code import Source
from _pytest.capture import _get_multicapture from _pytest.capture import _get_multicapture
from _pytest.compat import final from _pytest.compat import final
from _pytest.compat import LEGACY_PATH
from _pytest.compat import legacy_path
from _pytest.compat import NOTSET from _pytest.compat import NOTSET
from _pytest.compat import NotSetType from _pytest.compat import NotSetType
from _pytest.config import _PluggyPlugin from _pytest.config import _PluggyPlugin
@ -493,17 +491,6 @@ def pytester(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> "Pyt
return Pytester(request, tmp_path_factory, _ispytest=True) return Pytester(request, tmp_path_factory, _ispytest=True)
@fixture
def testdir(pytester: "Pytester") -> "Testdir":
"""
Identical to :fixture:`pytester`, and provides an instance whose methods return
legacy ``LEGACY_PATH`` objects instead when applicable.
New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`.
"""
return Testdir(pytester, _ispytest=True)
@fixture @fixture
def _sys_snapshot() -> Generator[None, None, None]: def _sys_snapshot() -> Generator[None, None, None]:
snappaths = SysPathsSnapshot() snappaths = SysPathsSnapshot()
@ -1531,224 +1518,6 @@ class LineComp:
LineMatcher(lines1).fnmatch_lines(lines2) LineMatcher(lines1).fnmatch_lines(lines2)
@final
class Testdir:
"""
Similar to :class:`Pytester`, but this class works with legacy legacy_path objects instead.
All methods just forward to an internal :class:`Pytester` instance, converting results
to `legacy_path` objects as necessary.
"""
__test__ = False
CLOSE_STDIN: "Final" = Pytester.CLOSE_STDIN
TimeoutExpired: "Final" = Pytester.TimeoutExpired
def __init__(self, pytester: Pytester, *, _ispytest: bool = False) -> None:
check_ispytest(_ispytest)
self._pytester = pytester
@property
def tmpdir(self) -> LEGACY_PATH:
"""Temporary directory where tests are executed."""
return legacy_path(self._pytester.path)
@property
def test_tmproot(self) -> LEGACY_PATH:
return legacy_path(self._pytester._test_tmproot)
@property
def request(self):
return self._pytester._request
@property
def plugins(self):
return self._pytester.plugins
@plugins.setter
def plugins(self, plugins):
self._pytester.plugins = plugins
@property
def monkeypatch(self) -> MonkeyPatch:
return self._pytester._monkeypatch
def make_hook_recorder(self, pluginmanager) -> HookRecorder:
"""See :meth:`Pytester.make_hook_recorder`."""
return self._pytester.make_hook_recorder(pluginmanager)
def chdir(self) -> None:
"""See :meth:`Pytester.chdir`."""
return self._pytester.chdir()
def finalize(self) -> None:
"""See :meth:`Pytester._finalize`."""
return self._pytester._finalize()
def makefile(self, ext, *args, **kwargs) -> LEGACY_PATH:
"""See :meth:`Pytester.makefile`."""
if ext and not ext.startswith("."):
# pytester.makefile is going to throw a ValueError in a way that
# testdir.makefile did not, because
# pathlib.Path is stricter suffixes than py.path
# This ext arguments is likely user error, but since testdir has
# allowed this, we will prepend "." as a workaround to avoid breaking
# testdir usage that worked before
ext = "." + ext
return legacy_path(self._pytester.makefile(ext, *args, **kwargs))
def makeconftest(self, source) -> LEGACY_PATH:
"""See :meth:`Pytester.makeconftest`."""
return legacy_path(self._pytester.makeconftest(source))
def makeini(self, source) -> LEGACY_PATH:
"""See :meth:`Pytester.makeini`."""
return legacy_path(self._pytester.makeini(source))
def getinicfg(self, source: str) -> SectionWrapper:
"""See :meth:`Pytester.getinicfg`."""
return self._pytester.getinicfg(source)
def makepyprojecttoml(self, source) -> LEGACY_PATH:
"""See :meth:`Pytester.makepyprojecttoml`."""
return legacy_path(self._pytester.makepyprojecttoml(source))
def makepyfile(self, *args, **kwargs) -> LEGACY_PATH:
"""See :meth:`Pytester.makepyfile`."""
return legacy_path(self._pytester.makepyfile(*args, **kwargs))
def maketxtfile(self, *args, **kwargs) -> LEGACY_PATH:
"""See :meth:`Pytester.maketxtfile`."""
return legacy_path(self._pytester.maketxtfile(*args, **kwargs))
def syspathinsert(self, path=None) -> None:
"""See :meth:`Pytester.syspathinsert`."""
return self._pytester.syspathinsert(path)
def mkdir(self, name) -> LEGACY_PATH:
"""See :meth:`Pytester.mkdir`."""
return legacy_path(self._pytester.mkdir(name))
def mkpydir(self, name) -> LEGACY_PATH:
"""See :meth:`Pytester.mkpydir`."""
return legacy_path(self._pytester.mkpydir(name))
def copy_example(self, name=None) -> LEGACY_PATH:
"""See :meth:`Pytester.copy_example`."""
return legacy_path(self._pytester.copy_example(name))
def getnode(self, config: Config, arg) -> Optional[Union[Item, Collector]]:
"""See :meth:`Pytester.getnode`."""
return self._pytester.getnode(config, arg)
def getpathnode(self, path):
"""See :meth:`Pytester.getpathnode`."""
return self._pytester.getpathnode(path)
def genitems(self, colitems: List[Union[Item, Collector]]) -> List[Item]:
"""See :meth:`Pytester.genitems`."""
return self._pytester.genitems(colitems)
def runitem(self, source):
"""See :meth:`Pytester.runitem`."""
return self._pytester.runitem(source)
def inline_runsource(self, source, *cmdlineargs):
"""See :meth:`Pytester.inline_runsource`."""
return self._pytester.inline_runsource(source, *cmdlineargs)
def inline_genitems(self, *args):
"""See :meth:`Pytester.inline_genitems`."""
return self._pytester.inline_genitems(*args)
def inline_run(self, *args, plugins=(), no_reraise_ctrlc: bool = False):
"""See :meth:`Pytester.inline_run`."""
return self._pytester.inline_run(
*args, plugins=plugins, no_reraise_ctrlc=no_reraise_ctrlc
)
def runpytest_inprocess(self, *args, **kwargs) -> RunResult:
"""See :meth:`Pytester.runpytest_inprocess`."""
return self._pytester.runpytest_inprocess(*args, **kwargs)
def runpytest(self, *args, **kwargs) -> RunResult:
"""See :meth:`Pytester.runpytest`."""
return self._pytester.runpytest(*args, **kwargs)
def parseconfig(self, *args) -> Config:
"""See :meth:`Pytester.parseconfig`."""
return self._pytester.parseconfig(*args)
def parseconfigure(self, *args) -> Config:
"""See :meth:`Pytester.parseconfigure`."""
return self._pytester.parseconfigure(*args)
def getitem(self, source, funcname="test_func"):
"""See :meth:`Pytester.getitem`."""
return self._pytester.getitem(source, funcname)
def getitems(self, source):
"""See :meth:`Pytester.getitems`."""
return self._pytester.getitems(source)
def getmodulecol(self, source, configargs=(), withinit=False):
"""See :meth:`Pytester.getmodulecol`."""
return self._pytester.getmodulecol(
source, configargs=configargs, withinit=withinit
)
def collect_by_name(
self, modcol: Collector, name: str
) -> Optional[Union[Item, Collector]]:
"""See :meth:`Pytester.collect_by_name`."""
return self._pytester.collect_by_name(modcol, name)
def popen(
self,
cmdargs,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
stdin=CLOSE_STDIN,
**kw,
):
"""See :meth:`Pytester.popen`."""
return self._pytester.popen(cmdargs, stdout, stderr, stdin, **kw)
def run(self, *cmdargs, timeout=None, stdin=CLOSE_STDIN) -> RunResult:
"""See :meth:`Pytester.run`."""
return self._pytester.run(*cmdargs, timeout=timeout, stdin=stdin)
def runpython(self, script) -> RunResult:
"""See :meth:`Pytester.runpython`."""
return self._pytester.runpython(script)
def runpython_c(self, command):
"""See :meth:`Pytester.runpython_c`."""
return self._pytester.runpython_c(command)
def runpytest_subprocess(self, *args, timeout=None) -> RunResult:
"""See :meth:`Pytester.runpytest_subprocess`."""
return self._pytester.runpytest_subprocess(*args, timeout=timeout)
def spawn_pytest(
self, string: str, expect_timeout: float = 10.0
) -> "pexpect.spawn":
"""See :meth:`Pytester.spawn_pytest`."""
return self._pytester.spawn_pytest(string, expect_timeout=expect_timeout)
def spawn(self, cmd: str, expect_timeout: float = 10.0) -> "pexpect.spawn":
"""See :meth:`Pytester.spawn`."""
return self._pytester.spawn(cmd, expect_timeout=expect_timeout)
def __repr__(self) -> str:
return f"<Testdir {self.tmpdir!r}>"
def __str__(self) -> str:
return str(self.tmpdir)
@final
class LineMatcher: class LineMatcher:
"""Flexible matching of text. """Flexible matching of text.

View File

@ -37,8 +37,6 @@ from _pytest._code import ExceptionInfo
from _pytest._code.code import ExceptionRepr from _pytest._code.code import ExceptionRepr
from _pytest._io.wcwidth import wcswidth from _pytest._io.wcwidth import wcswidth
from _pytest.compat import final from _pytest.compat import final
from _pytest.compat import LEGACY_PATH
from _pytest.compat import legacy_path
from _pytest.config import _PluggyPlugin from _pytest.config import _PluggyPlugin
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import ExitCode from _pytest.config import ExitCode
@ -383,16 +381,6 @@ class TerminalReporter:
def showlongtestinfo(self) -> bool: def showlongtestinfo(self) -> bool:
return self.verbosity > 0 return self.verbosity > 0
@property
def startdir(self) -> LEGACY_PATH:
"""The directory from which pytest was invoked.
Prefer to use ``startpath`` which is a :class:`pathlib.Path`.
:type: LEGACY_PATH
"""
return legacy_path(self.startpath)
def hasopt(self, char: str) -> bool: def hasopt(self, char: str) -> bool:
char = {"xfailed": "x", "skipped": "s"}.get(char, char) char = {"xfailed": "x", "skipped": "s"}.get(char, char)
return char in self.reportchars return char in self.reportchars
@ -875,7 +863,6 @@ class TerminalReporter:
return line return line
# collect_fspath comes from testid which has a "/"-normalized path. # collect_fspath comes from testid which has a "/"-normalized path.
if fspath: if fspath:
res = mkrel(nodeid) res = mkrel(nodeid)
if self.verbosity >= 2 and nodeid.split("::")[0] != fspath.replace( if self.verbosity >= 2 and nodeid.split("::")[0] != fspath.replace(

View File

@ -13,8 +13,6 @@ from .pathlib import make_numbered_dir
from .pathlib import make_numbered_dir_with_cleanup from .pathlib import make_numbered_dir_with_cleanup
from .pathlib import rm_rf from .pathlib import rm_rf
from _pytest.compat import final from _pytest.compat import final
from _pytest.compat import LEGACY_PATH
from _pytest.compat import legacy_path
from _pytest.config import Config from _pytest.config import Config
from _pytest.deprecated import check_ispytest from _pytest.deprecated import check_ispytest
from _pytest.fixtures import fixture from _pytest.fixtures import fixture
@ -157,29 +155,6 @@ class TempPathFactory:
return basetemp return basetemp
@final
@attr.s(init=False, auto_attribs=True)
class TempdirFactory:
"""Backward compatibility wrapper that implements :class:``_pytest.compat.LEGACY_PATH``
for :class:``TempPathFactory``."""
_tmppath_factory: TempPathFactory
def __init__(
self, tmppath_factory: TempPathFactory, *, _ispytest: bool = False
) -> None:
check_ispytest(_ispytest)
self._tmppath_factory = tmppath_factory
def mktemp(self, basename: str, numbered: bool = True) -> LEGACY_PATH:
"""Same as :meth:`TempPathFactory.mktemp`, but returns a ``_pytest.compat.LEGACY_PATH`` object."""
return legacy_path(self._tmppath_factory.mktemp(basename, numbered).resolve())
def getbasetemp(self) -> LEGACY_PATH:
"""Backward compat wrapper for ``_tmppath_factory.getbasetemp``."""
return legacy_path(self._tmppath_factory.getbasetemp().resolve())
def get_user() -> Optional[str]: def get_user() -> Optional[str]:
"""Return the current user name, or None if getuser() does not work """Return the current user name, or None if getuser() does not work
in the current environment (see #1010).""" in the current environment (see #1010)."""
@ -201,16 +176,7 @@ def pytest_configure(config: Config) -> None:
mp = MonkeyPatch() mp = MonkeyPatch()
config.add_cleanup(mp.undo) config.add_cleanup(mp.undo)
_tmp_path_factory = TempPathFactory.from_config(config, _ispytest=True) _tmp_path_factory = TempPathFactory.from_config(config, _ispytest=True)
_tmpdirhandler = TempdirFactory(_tmp_path_factory, _ispytest=True)
mp.setattr(config, "_tmp_path_factory", _tmp_path_factory, raising=False) mp.setattr(config, "_tmp_path_factory", _tmp_path_factory, raising=False)
mp.setattr(config, "_tmpdirhandler", _tmpdirhandler, raising=False)
@fixture(scope="session")
def tmpdir_factory(request: FixtureRequest) -> TempdirFactory:
"""Return a :class:`pytest.TempdirFactory` instance for the test session."""
# Set dynamically by pytest_configure() above.
return request.config._tmpdirhandler # type: ignore
@fixture(scope="session") @fixture(scope="session")
@ -228,24 +194,6 @@ def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path:
return factory.mktemp(name, numbered=True) return factory.mktemp(name, numbered=True)
@fixture
def tmpdir(tmp_path: Path) -> LEGACY_PATH:
"""Return a temporary directory path object which is unique to each test
function invocation, created as a sub directory of the base temporary
directory.
By default, a new base temporary directory is created each test session,
and old bases are removed after 3 sessions, to aid in debugging. If
``--basetemp`` is used then it is cleared each session. See :ref:`base
temporary directory`.
The returned object is a `legacy_path`_ object.
.. _legacy_path: https://py.readthedocs.io/en/latest/path.html
"""
return legacy_path(tmp_path)
@fixture @fixture
def tmp_path(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> Path: def tmp_path(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> Path:
"""Return a temporary directory path object which is unique to each test """Return a temporary directory path object which is unique to each test

View File

@ -46,7 +46,6 @@ from _pytest.pytester import LineMatcher
from _pytest.pytester import Pytester from _pytest.pytester import Pytester
from _pytest.pytester import RecordedHookCall from _pytest.pytester import RecordedHookCall
from _pytest.pytester import RunResult from _pytest.pytester import RunResult
from _pytest.pytester import Testdir
from _pytest.python import Class from _pytest.python import Class
from _pytest.python import Function from _pytest.python import Function
from _pytest.python import Instance from _pytest.python import Instance
@ -61,7 +60,6 @@ from _pytest.recwarn import warns
from _pytest.runner import CallInfo from _pytest.runner import CallInfo
from _pytest.stash import Stash from _pytest.stash import Stash
from _pytest.stash import StashKey from _pytest.stash import StashKey
from _pytest.tmpdir import TempdirFactory
from _pytest.tmpdir import TempPathFactory from _pytest.tmpdir import TempPathFactory
from _pytest.warning_types import PytestAssertRewriteWarning from _pytest.warning_types import PytestAssertRewriteWarning
from _pytest.warning_types import PytestCacheWarning from _pytest.warning_types import PytestCacheWarning
@ -145,8 +143,6 @@ __all__ = [
"StashKey", "StashKey",
"version_tuple", "version_tuple",
"TempPathFactory", "TempPathFactory",
"Testdir",
"TempdirFactory",
"UsageError", "UsageError",
"WarningsRecorder", "WarningsRecorder",
"warns", "warns",

View File

@ -1,7 +1,7 @@
anyio[curio,trio]==3.3.4 anyio[curio,trio]==3.3.4
django==3.2.8 django==3.2.8
pytest-asyncio==0.16.0 pytest-asyncio==0.16.0
pytest-bdd==4.1.0 pytest-bdd==5.0.0
pytest-cov==3.0.0 pytest-cov==3.0.0
pytest-django==4.4.0 pytest-django==4.4.0
pytest-flakes==4.0.4 pytest-flakes==4.0.4

View File

@ -967,7 +967,6 @@ class TestRequestBasic:
(item,) = pytester.genitems([modcol]) (item,) = pytester.genitems([modcol])
req = fixtures.FixtureRequest(item, _ispytest=True) req = fixtures.FixtureRequest(item, _ispytest=True)
assert req.path == modcol.path assert req.path == modcol.path
assert req.fspath == modcol.fspath
def test_request_fixturenames(self, pytester: Pytester) -> None: def test_request_fixturenames(self, pytester: Pytester) -> None:
pytester.makepyfile( pytester.makepyfile(
@ -1098,12 +1097,11 @@ class TestRequestSessionScoped:
def session_request(self, request): def session_request(self, request):
return request return request
@pytest.mark.parametrize("name", ["path", "fspath", "module"]) @pytest.mark.parametrize("name", ["path", "module"])
def test_session_scoped_unavailable_attributes(self, session_request, name): def test_session_scoped_unavailable_attributes(self, session_request, name):
expected = "path" if name == "fspath" else name
with pytest.raises( with pytest.raises(
AttributeError, AttributeError,
match=f"{expected} not available in session-scoped context", match=f"{name} not available in session-scoped context",
): ):
getattr(session_request, name) getattr(session_request, name)

View File

@ -614,7 +614,6 @@ class TestSession:
items2, hookrec = pytester.inline_genitems(item.nodeid) items2, hookrec = pytester.inline_genitems(item.nodeid)
(item2,) = items2 (item2,) = items2
assert item2.name == item.name assert item2.name == item.name
assert item2.fspath == item.fspath
assert item2.path == item.path assert item2.path == item.path
def test_find_byid_without_instance_parents(self, pytester: Pytester) -> None: def test_find_byid_without_instance_parents(self, pytester: Pytester) -> None:

View File

@ -635,14 +635,11 @@ class TestConfigAPI:
pytest.raises(ValueError, config.getini, "other") pytest.raises(ValueError, config.getini, "other")
@pytest.mark.parametrize("config_type", ["ini", "pyproject"]) @pytest.mark.parametrize("config_type", ["ini", "pyproject"])
@pytest.mark.parametrize("ini_type", ["paths", "pathlist"]) def test_addini_paths(self, pytester: Pytester, config_type: str) -> None:
def test_addini_paths(
self, pytester: Pytester, config_type: str, ini_type: str
) -> None:
pytester.makeconftest( pytester.makeconftest(
f""" """
def pytest_addoption(parser): def pytest_addoption(parser):
parser.addini("paths", "my new ini value", type="{ini_type}") parser.addini("paths", "my new ini value", type="paths")
parser.addini("abc", "abc value") parser.addini("abc", "abc value")
""" """
) )
@ -1521,12 +1518,11 @@ class TestOverrideIniArgs:
assert result.ret == 0 assert result.ret == 0
result.stdout.fnmatch_lines(["custom_option:3.0"]) result.stdout.fnmatch_lines(["custom_option:3.0"])
@pytest.mark.parametrize("ini_type", ["paths", "pathlist"]) def test_override_ini_paths(self, pytester: Pytester) -> None:
def test_override_ini_paths(self, pytester: Pytester, ini_type: str) -> None:
pytester.makeconftest( pytester.makeconftest(
f""" """
def pytest_addoption(parser): def pytest_addoption(parser):
parser.addini("paths", "my new ini value", type="{ini_type}")""" parser.addini("paths", "my new ini value", type="paths")"""
) )
pytester.makeini( pytester.makeini(
""" """
@ -1534,15 +1530,12 @@ class TestOverrideIniArgs:
paths=blah.py""" paths=blah.py"""
) )
pytester.makepyfile( pytester.makepyfile(
rf""" r"""
def test_overriden(pytestconfig): def test_overriden(pytestconfig):
config_paths = pytestconfig.getini("paths") config_paths = pytestconfig.getini("paths")
print(config_paths) print(config_paths)
for cpf in config_paths: for cpf in config_paths:
if "{ini_type}" == "pathlist": print('\nuser_path:%s' % cpf.name)
print('\nuser_path:%s' % cpf.basename)
else:
print('\nuser_path:%s' % cpf.name)
""" """
) )
result = pytester.runpytest( result = pytester.runpytest(

View File

@ -1170,6 +1170,41 @@ class TestDoctestSkips:
["*4: UnexpectedException*", "*5: DocTestFailure*", "*8: DocTestFailure*"] ["*4: UnexpectedException*", "*5: DocTestFailure*", "*8: DocTestFailure*"]
) )
def test_skipping_wrapped_test(self, pytester):
"""
Issue 8796: INTERNALERROR raised when skipping a decorated DocTest
through pytest_collection_modifyitems.
"""
pytester.makeconftest(
"""
import pytest
from _pytest.doctest import DoctestItem
def pytest_collection_modifyitems(config, items):
skip_marker = pytest.mark.skip()
for item in items:
if isinstance(item, DoctestItem):
item.add_marker(skip_marker)
"""
)
pytester.makepyfile(
"""
from contextlib import contextmanager
@contextmanager
def my_config_context():
'''
>>> import os
'''
"""
)
result = pytester.runpytest("--doctest-modules")
assert "INTERNALERROR" not in result.stdout.str()
result.assert_outcomes(skipped=1)
class TestDoctestAutoUseFixtures: class TestDoctestAutoUseFixtures:

163
testing/test_legacypath.py Normal file
View File

@ -0,0 +1,163 @@
from pathlib import Path
import pytest
from _pytest.compat import LEGACY_PATH
from _pytest.legacypath import TempdirFactory
from _pytest.legacypath import Testdir
def test_item_fspath(pytester: pytest.Pytester) -> None:
pytester.makepyfile("def test_func(): pass")
items, hookrec = pytester.inline_genitems()
assert len(items) == 1
(item,) = items
items2, hookrec = pytester.inline_genitems(item.nodeid)
(item2,) = items2
assert item2.name == item.name
assert item2.fspath == item.fspath # type: ignore[attr-defined]
assert item2.path == item.path
def test_testdir_testtmproot(testdir: Testdir) -> None:
"""Check test_tmproot is a py.path attribute for backward compatibility."""
assert testdir.test_tmproot.check(dir=1)
def test_testdir_makefile_dot_prefixes_extension_silently(
testdir: Testdir,
) -> None:
"""For backwards compat #8192"""
p1 = testdir.makefile("foo.bar", "")
assert ".foo.bar" in str(p1)
def test_testdir_makefile_ext_none_raises_type_error(testdir: Testdir) -> None:
"""For backwards compat #8192"""
with pytest.raises(TypeError):
testdir.makefile(None, "")
def test_testdir_makefile_ext_empty_string_makes_file(testdir: Testdir) -> None:
"""For backwards compat #8192"""
p1 = testdir.makefile("", "")
assert "test_testdir_makefile" in str(p1)
def attempt_symlink_to(path: str, to_path: str) -> None:
"""Try to make a symlink from "path" to "to_path", skipping in case this platform
does not support it or we don't have sufficient privileges (common on Windows)."""
try:
Path(path).symlink_to(Path(to_path))
except OSError:
pytest.skip("could not create symbolic link")
def test_tmpdir_factory(
tmpdir_factory: TempdirFactory,
tmp_path_factory: pytest.TempPathFactory,
) -> None:
assert str(tmpdir_factory.getbasetemp()) == str(tmp_path_factory.getbasetemp())
dir = tmpdir_factory.mktemp("foo")
assert dir.exists()
def test_tmpdir_equals_tmp_path(tmpdir: LEGACY_PATH, tmp_path: Path) -> None:
assert Path(tmpdir) == tmp_path
def test_tmpdir_always_is_realpath(pytester: pytest.Pytester) -> None:
# See test_tmp_path_always_is_realpath.
realtemp = pytester.mkdir("myrealtemp")
linktemp = pytester.path.joinpath("symlinktemp")
attempt_symlink_to(str(linktemp), str(realtemp))
p = pytester.makepyfile(
"""
def test_1(tmpdir):
import os
assert os.path.realpath(str(tmpdir)) == str(tmpdir)
"""
)
result = pytester.runpytest("-s", p, "--basetemp=%s/bt" % linktemp)
assert not result.ret
def test_cache_makedir(cache: pytest.Cache) -> None:
dir = cache.makedir("foo") # type: ignore[attr-defined]
assert dir.exists()
dir.remove()
def test_fixturerequest_getmodulepath(pytester: pytest.Pytester) -> None:
modcol = pytester.getmodulecol("def test_somefunc(): pass")
(item,) = pytester.genitems([modcol])
req = pytest.FixtureRequest(item, _ispytest=True)
assert req.path == modcol.path
assert req.fspath == modcol.fspath # type: ignore[attr-defined]
class TestFixtureRequestSessionScoped:
@pytest.fixture(scope="session")
def session_request(self, request):
return request
def test_session_scoped_unavailable_attributes(self, session_request):
with pytest.raises(
AttributeError,
match="path not available in session-scoped context",
):
session_request.fspath
@pytest.mark.parametrize("config_type", ["ini", "pyproject"])
def test_addini_paths(pytester: pytest.Pytester, config_type: str) -> None:
pytester.makeconftest(
"""
def pytest_addoption(parser):
parser.addini("paths", "my new ini value", type="pathlist")
parser.addini("abc", "abc value")
"""
)
if config_type == "ini":
inipath = pytester.makeini(
"""
[pytest]
paths=hello world/sub.py
"""
)
elif config_type == "pyproject":
inipath = pytester.makepyprojecttoml(
"""
[tool.pytest.ini_options]
paths=["hello", "world/sub.py"]
"""
)
config = pytester.parseconfig()
values = config.getini("paths")
assert len(values) == 2
assert values[0] == inipath.parent.joinpath("hello")
assert values[1] == inipath.parent.joinpath("world/sub.py")
pytest.raises(ValueError, config.getini, "other")
def test_override_ini_paths(pytester: pytest.Pytester) -> None:
pytester.makeconftest(
"""
def pytest_addoption(parser):
parser.addini("paths", "my new ini value", type="pathlist")"""
)
pytester.makeini(
"""
[pytest]
paths=blah.py"""
)
pytester.makepyfile(
r"""
def test_overriden(pytestconfig):
config_paths = pytestconfig.getini("paths")
print(config_paths)
for cpf in config_paths:
print('\nuser_path:%s' % cpf.basename)
"""
)
result = pytester.runpytest("--override-ini", "paths=foo/bar1.py foo/bar2.py", "-s")
result.stdout.fnmatch_lines(["user_path:bar1.py", "user_path:bar2.py"])

View File

@ -1111,7 +1111,7 @@ def test_pytest_param_id_allows_none_or_string(s) -> None:
assert pytest.param(id=s) assert pytest.param(id=s)
@pytest.mark.parametrize("expr", ("NOT internal_err", "NOT (internal_err)", "bogus/")) @pytest.mark.parametrize("expr", ("NOT internal_err", "NOT (internal_err)", "bogus="))
def test_marker_expr_eval_failure_handling(pytester: Pytester, expr) -> None: def test_marker_expr_eval_failure_handling(pytester: Pytester, expr) -> None:
foo = pytester.makepyfile( foo = pytester.makepyfile(
""" """

View File

@ -144,6 +144,7 @@ def test_syntax_errors(expr: str, column: int, message: str) -> None:
"a:::c", "a:::c",
"a+-b", "a+-b",
r"\nhe\\l\lo\n\t\rbye", r"\nhe\\l\lo\n\t\rbye",
"a/b",
"אבגד", "אבגד",
"aaאבגדcc", "aaאבגדcc",
"a[bcd]", "a[bcd]",
@ -170,7 +171,6 @@ def test_valid_idents(ident: str) -> None:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"ident", "ident",
( (
"/",
"^", "^",
"*", "*",
"=", "=",

View File

@ -17,7 +17,6 @@ from _pytest.pytester import LineMatcher
from _pytest.pytester import Pytester from _pytest.pytester import Pytester
from _pytest.pytester import SysModulesSnapshot from _pytest.pytester import SysModulesSnapshot
from _pytest.pytester import SysPathsSnapshot from _pytest.pytester import SysPathsSnapshot
from _pytest.pytester import Testdir
def test_make_hook_recorder(pytester: Pytester) -> None: def test_make_hook_recorder(pytester: Pytester) -> None:
@ -814,19 +813,6 @@ def test_makefile_joins_absolute_path(pytester: Pytester) -> None:
assert str(p1) == str(pytester.path / "absfile.py") assert str(p1) == str(pytester.path / "absfile.py")
def test_testtmproot(testdir) -> None:
"""Check test_tmproot is a py.path attribute for backward compatibility."""
assert testdir.test_tmproot.check(dir=1)
def test_testdir_makefile_dot_prefixes_extension_silently(
testdir: Testdir,
) -> None:
"""For backwards compat #8192"""
p1 = testdir.makefile("foo.bar", "")
assert ".foo.bar" in str(p1)
def test_pytester_makefile_dot_prefixes_extension_with_warning( def test_pytester_makefile_dot_prefixes_extension_with_warning(
pytester: Pytester, pytester: Pytester,
) -> None: ) -> None:
@ -837,18 +823,6 @@ def test_pytester_makefile_dot_prefixes_extension_with_warning(
pytester.makefile("foo.bar", "") pytester.makefile("foo.bar", "")
def test_testdir_makefile_ext_none_raises_type_error(testdir) -> None:
"""For backwards compat #8192"""
with pytest.raises(TypeError):
testdir.makefile(None, "")
def test_testdir_makefile_ext_empty_string_makes_file(testdir) -> None:
"""For backwards compat #8192"""
p1 = testdir.makefile("", "")
assert "test_testdir_makefile" in str(p1)
@pytest.mark.filterwarnings("default") @pytest.mark.filterwarnings("default")
def test_pytester_assert_outcomes_warnings(pytester: Pytester) -> None: def test_pytester_assert_outcomes_warnings(pytester: Pytester) -> None:
pytester.makepyfile( pytester.makepyfile(

View File

@ -118,8 +118,8 @@ def test_mktemp(pytester: Pytester, basename: str, is_ok: bool) -> None:
result.stdout.fnmatch_lines("*ValueError*") result.stdout.fnmatch_lines("*ValueError*")
def test_tmpdir_always_is_realpath(pytester: Pytester) -> None: def test_tmp_path_always_is_realpath(pytester: Pytester, monkeypatch) -> None:
# the reason why tmpdir should be a realpath is that # the reason why tmp_path should be a realpath is that
# when you cd to it and do "os.getcwd()" you will anyway # when you cd to it and do "os.getcwd()" you will anyway
# get the realpath. Using the symlinked path can thus # get the realpath. Using the symlinked path can thus
# easily result in path-inequality # easily result in path-inequality
@ -128,22 +128,6 @@ def test_tmpdir_always_is_realpath(pytester: Pytester) -> None:
realtemp = pytester.mkdir("myrealtemp") realtemp = pytester.mkdir("myrealtemp")
linktemp = pytester.path.joinpath("symlinktemp") linktemp = pytester.path.joinpath("symlinktemp")
attempt_symlink_to(linktemp, str(realtemp)) attempt_symlink_to(linktemp, str(realtemp))
p = pytester.makepyfile(
"""
def test_1(tmpdir):
import os
assert os.path.realpath(str(tmpdir)) == str(tmpdir)
"""
)
result = pytester.runpytest("-s", p, "--basetemp=%s/bt" % linktemp)
assert not result.ret
def test_tmp_path_always_is_realpath(pytester: Pytester, monkeypatch) -> None:
# for reasoning see: test_tmpdir_always_is_realpath test-case
realtemp = pytester.mkdir("myrealtemp")
linktemp = pytester.path.joinpath("symlinktemp")
attempt_symlink_to(linktemp, str(realtemp))
monkeypatch.setenv("PYTEST_DEBUG_TEMPROOT", str(linktemp)) monkeypatch.setenv("PYTEST_DEBUG_TEMPROOT", str(linktemp))
pytester.makepyfile( pytester.makepyfile(
""" """
@ -423,10 +407,6 @@ def attempt_symlink_to(path, to_path):
pytest.skip("could not create symbolic link") pytest.skip("could not create symbolic link")
def test_tmpdir_equals_tmp_path(tmpdir, tmp_path):
assert Path(tmpdir) == tmp_path
def test_basetemp_with_read_only_files(pytester: Pytester) -> None: def test_basetemp_with_read_only_files(pytester: Pytester) -> None:
"""Integration test for #5524""" """Integration test for #5524"""
pytester.makepyfile( pytester.makepyfile(