Merge pull request #8459 from bluetech/unnecessary-py-path-3

This commit is contained in:
Bruno Oliveira 2021-03-18 09:05:45 -03:00 committed by GitHub
commit 35df3e68d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 88 additions and 41 deletions

View File

@ -670,7 +670,7 @@ class FixtureRequest:
"\n\nRequested here:\n{}:{}".format( "\n\nRequested here:\n{}:{}".format(
funcitem.nodeid, funcitem.nodeid,
fixturedef.argname, fixturedef.argname,
getlocation(fixturedef.func, funcitem.config.rootdir), getlocation(fixturedef.func, funcitem.config.rootpath),
source_path_str, source_path_str,
source_lineno, source_lineno,
) )
@ -728,7 +728,7 @@ class FixtureRequest:
fs, lineno = getfslineno(factory) fs, lineno = getfslineno(factory)
if isinstance(fs, Path): if isinstance(fs, Path):
session: Session = self._pyfuncitem.session session: Session = self._pyfuncitem.session
p = bestrelpath(Path(session.fspath), fs) p = bestrelpath(session.path, fs)
else: else:
p = fs p = fs
args = _format_args(factory) args = _format_args(factory)

View File

@ -25,6 +25,7 @@ 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.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
@ -301,7 +302,7 @@ def wrap_session(
finally: finally:
# Explicitly break reference cycle. # Explicitly break reference cycle.
excinfo = None # type: ignore excinfo = None # type: ignore
session.startdir.chdir() os.chdir(session.startpath)
if initstate >= 2: if initstate >= 2:
try: try:
config.hook.pytest_sessionfinish( config.hook.pytest_sessionfinish(
@ -476,7 +477,6 @@ class Session(nodes.FSCollector):
self.shouldstop: Union[bool, str] = False self.shouldstop: Union[bool, str] = False
self.shouldfail: Union[bool, str] = False self.shouldfail: Union[bool, str] = False
self.trace = config.trace.root.get("collection") self.trace = config.trace.root.get("collection")
self.startdir = config.invocation_dir
self._initialpaths: FrozenSet[Path] = frozenset() self._initialpaths: FrozenSet[Path] = frozenset()
self._bestrelpathcache: Dict[Path, str] = _bestrelpath_cache(config.rootpath) self._bestrelpathcache: Dict[Path, str] = _bestrelpath_cache(config.rootpath)
@ -497,6 +497,24 @@ class Session(nodes.FSCollector):
self.testscollected, self.testscollected,
) )
@property
def startpath(self) -> Path:
"""The path from which pytest was invoked.
.. versionadded:: 6.3.0
"""
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

@ -32,6 +32,7 @@ from _pytest.mark.structures import MarkDecorator
from _pytest.mark.structures import NodeKeywords from _pytest.mark.structures import NodeKeywords
from _pytest.outcomes import fail from _pytest.outcomes import fail
from _pytest.pathlib import absolutepath from _pytest.pathlib import absolutepath
from _pytest.pathlib import commonpath
from _pytest.store import Store from _pytest.store import Store
if TYPE_CHECKING: if TYPE_CHECKING:
@ -517,13 +518,11 @@ class Collector(Node):
excinfo.traceback = ntraceback.filter() excinfo.traceback = ntraceback.filter()
def _check_initialpaths_for_relpath( def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[str]:
session: "Session", fspath: LEGACY_PATH
) -> Optional[str]:
for initial_path in session._initialpaths: for initial_path in session._initialpaths:
initial_path_ = legacy_path(initial_path) if commonpath(path, initial_path) == initial_path:
if fspath.common(initial_path_) == initial_path_: rel = str(path.relative_to(initial_path))
return fspath.relto(initial_path_) return "" if rel == "." else rel
return None return None
@ -538,7 +537,7 @@ class FSCollector(Collector):
nodeid: Optional[str] = None, nodeid: Optional[str] = None,
) -> None: ) -> None:
path, fspath = _imply_path(path, fspath=fspath) path, fspath = _imply_path(path, fspath=fspath)
name = fspath.basename name = path.name
if parent is not None and parent.path != path: if parent is not None and parent.path != path:
try: try:
rel = path.relative_to(parent.path) rel = path.relative_to(parent.path)
@ -547,7 +546,7 @@ class FSCollector(Collector):
else: else:
name = str(rel) name = str(rel)
name = name.replace(os.sep, SEP) name = name.replace(os.sep, SEP)
self.path = Path(fspath) self.path = path
session = session or parent.session session = session or parent.session
@ -555,7 +554,7 @@ class FSCollector(Collector):
try: try:
nodeid = str(self.path.relative_to(session.config.rootpath)) nodeid = str(self.path.relative_to(session.config.rootpath))
except ValueError: except ValueError:
nodeid = _check_initialpaths_for_relpath(session, fspath) nodeid = _check_initialpaths_for_relpath(session, path)
if nodeid and os.sep != SEP: if nodeid and os.sep != SEP:
nodeid = nodeid.replace(os.sep, SEP) nodeid = nodeid.replace(os.sep, SEP)

View File

@ -583,7 +583,7 @@ def resolve_package_path(path: Path) -> Optional[Path]:
def visit( def visit(
path: str, recurse: Callable[["os.DirEntry[str]"], bool] path: Union[str, "os.PathLike[str]"], recurse: Callable[["os.DirEntry[str]"], bool]
) -> Iterator["os.DirEntry[str]"]: ) -> Iterator["os.DirEntry[str]"]:
"""Walk a directory recursively, in breadth-first order. """Walk a directory recursively, in breadth-first order.
@ -657,3 +657,21 @@ def bestrelpath(directory: Path, dest: Path) -> str:
# Forward from base to dest. # Forward from base to dest.
*reldest.parts, *reldest.parts,
) )
# Originates from py. path.local.copy(), with siginficant trims and adjustments.
# TODO(py38): Replace with shutil.copytree(..., symlinks=True, dirs_exist_ok=True)
def copytree(source: Path, target: Path) -> None:
"""Recursively copy a source directory to target."""
assert source.is_dir()
for entry in visit(source, recurse=lambda entry: not entry.is_symlink()):
x = Path(entry)
relpath = x.relative_to(source)
newx = target / relpath
newx.parent.mkdir(exist_ok=True)
if x.is_symlink():
newx.symlink_to(os.readlink(x))
elif x.is_file():
shutil.copyfile(x, newx)
elif x.is_dir():
newx.mkdir(exist_ok=True)

View File

@ -63,6 +63,7 @@ from _pytest.outcomes import fail
from _pytest.outcomes import importorskip from _pytest.outcomes import importorskip
from _pytest.outcomes import skip from _pytest.outcomes import skip
from _pytest.pathlib import bestrelpath from _pytest.pathlib import bestrelpath
from _pytest.pathlib import copytree
from _pytest.pathlib import make_numbered_dir from _pytest.pathlib import make_numbered_dir
from _pytest.reports import CollectReport from _pytest.reports import CollectReport
from _pytest.reports import TestReport from _pytest.reports import TestReport
@ -935,10 +936,7 @@ class Pytester:
example_path = example_dir.joinpath(name) example_path = example_dir.joinpath(name)
if example_path.is_dir() and not example_path.joinpath("__init__.py").is_file(): if example_path.is_dir() and not example_path.joinpath("__init__.py").is_file():
# TODO: legacy_path.copy can copy files to existing directories, copytree(example_path, self.path)
# while with shutil.copytree the destination directory cannot exist,
# we will need to roll our own in order to drop legacy_path completely
legacy_path(example_path).copy(legacy_path(self.path))
return self.path return self.path
elif example_path.is_file(): elif example_path.is_file():
result = self.path.joinpath(example_path.name) result = self.path.joinpath(example_path.name)

View File

@ -645,7 +645,7 @@ class Package(Module):
session=session, session=session,
nodeid=nodeid, nodeid=nodeid,
) )
self.name = os.path.basename(str(fspath.dirname)) self.name = path.parent.name
def setup(self) -> None: def setup(self) -> None:
# Not using fixtures to call setup_module here because autouse fixtures # Not using fixtures to call setup_module here because autouse fixtures

View File

@ -1,5 +1,5 @@
import os
from io import StringIO from io import StringIO
from pathlib import Path
from pprint import pprint from pprint import pprint
from typing import Any from typing import Any
from typing import cast from typing import cast
@ -29,7 +29,6 @@ from _pytest._code.code import ReprTraceback
from _pytest._code.code import TerminalRepr from _pytest._code.code import TerminalRepr
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.config import Config from _pytest.config import Config
from _pytest.nodes import Collector from _pytest.nodes import Collector
from _pytest.nodes import Item from _pytest.nodes import Item
@ -500,8 +499,8 @@ def _report_to_json(report: BaseReport) -> Dict[str, Any]:
else: else:
d["longrepr"] = report.longrepr d["longrepr"] = report.longrepr
for name in d: for name in d:
if isinstance(d[name], (LEGACY_PATH, Path)): if isinstance(d[name], os.PathLike):
d[name] = str(d[name]) d[name] = os.fspath(d[name])
elif name == "result": elif name == "result":
d[name] = None # for now d[name] = None # for now
return d return d

View File

@ -37,6 +37,8 @@ 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
@ -318,7 +320,6 @@ class TerminalReporter:
self.stats: Dict[str, List[Any]] = {} self.stats: Dict[str, List[Any]] = {}
self._main_color: Optional[str] = None self._main_color: Optional[str] = None
self._known_types: Optional[List[str]] = None self._known_types: Optional[List[str]] = None
self.startdir = config.invocation_dir
self.startpath = config.invocation_params.dir self.startpath = config.invocation_params.dir
if file is None: if file is None:
file = sys.stdout file = sys.stdout
@ -381,6 +382,16 @@ 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

View File

@ -933,11 +933,11 @@ def test_setup_only_available_in_subdir(pytester: Pytester) -> None:
"""\ """\
import pytest import pytest
def pytest_runtest_setup(item): def pytest_runtest_setup(item):
assert item.fspath.purebasename == "test_in_sub1" assert item.path.stem == "test_in_sub1"
def pytest_runtest_call(item): def pytest_runtest_call(item):
assert item.fspath.purebasename == "test_in_sub1" assert item.path.stem == "test_in_sub1"
def pytest_runtest_teardown(item): def pytest_runtest_teardown(item):
assert item.fspath.purebasename == "test_in_sub1" assert item.path.stem == "test_in_sub1"
""" """
) )
) )
@ -946,11 +946,11 @@ def test_setup_only_available_in_subdir(pytester: Pytester) -> None:
"""\ """\
import pytest import pytest
def pytest_runtest_setup(item): def pytest_runtest_setup(item):
assert item.fspath.purebasename == "test_in_sub2" assert item.path.stem == "test_in_sub2"
def pytest_runtest_call(item): def pytest_runtest_call(item):
assert item.fspath.purebasename == "test_in_sub2" assert item.path.stem == "test_in_sub2"
def pytest_runtest_teardown(item): def pytest_runtest_teardown(item):
assert item.fspath.purebasename == "test_in_sub2" assert item.path.stem == "test_in_sub2"
""" """
) )
) )
@ -1125,8 +1125,7 @@ class TestReportInfo:
def test_func_reportinfo(self, pytester: Pytester) -> None: def test_func_reportinfo(self, pytester: Pytester) -> None:
item = pytester.getitem("def test_func(): pass") item = pytester.getitem("def test_func(): pass")
fspath, lineno, modpath = item.reportinfo() fspath, lineno, modpath = item.reportinfo()
with pytest.warns(DeprecationWarning): assert str(fspath) == str(item.path)
assert fspath == item.fspath
assert lineno == 0 assert lineno == 0
assert modpath == "test_func" assert modpath == "test_func"
@ -1141,8 +1140,7 @@ class TestReportInfo:
classcol = pytester.collect_by_name(modcol, "TestClass") classcol = pytester.collect_by_name(modcol, "TestClass")
assert isinstance(classcol, Class) assert isinstance(classcol, Class)
fspath, lineno, msg = classcol.reportinfo() fspath, lineno, msg = classcol.reportinfo()
with pytest.warns(DeprecationWarning): assert str(fspath) == str(modcol.path)
assert fspath == modcol.fspath
assert lineno == 1 assert lineno == 1
assert msg == "TestClass" assert msg == "TestClass"

View File

@ -5,7 +5,6 @@ from typing import Type
import pytest import pytest
from _pytest import nodes from _pytest import nodes
from _pytest.compat import legacy_path
from _pytest.pytester import Pytester from _pytest.pytester import Pytester
from _pytest.warning_types import PytestWarning from _pytest.warning_types import PytestWarning
@ -76,7 +75,7 @@ def test__check_initialpaths_for_relpath() -> None:
session = cast(pytest.Session, FakeSession1) session = cast(pytest.Session, FakeSession1)
assert nodes._check_initialpaths_for_relpath(session, legacy_path(cwd)) == "" assert nodes._check_initialpaths_for_relpath(session, cwd) == ""
sub = cwd / "file" sub = cwd / "file"
@ -85,9 +84,9 @@ def test__check_initialpaths_for_relpath() -> None:
session = cast(pytest.Session, FakeSession2) session = cast(pytest.Session, FakeSession2)
assert nodes._check_initialpaths_for_relpath(session, legacy_path(sub)) == "file" assert nodes._check_initialpaths_for_relpath(session, sub) == "file"
outside = legacy_path("/outside") outside = Path("/outside")
assert nodes._check_initialpaths_for_relpath(session, outside) is None assert nodes._check_initialpaths_for_relpath(session, outside) is None

View File

@ -4,7 +4,6 @@ from typing import Union
import pytest import pytest
from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionChainRepr
from _pytest._code.code import ExceptionRepr from _pytest._code.code import ExceptionRepr
from _pytest.compat import legacy_path
from _pytest.config import Config from _pytest.config import Config
from _pytest.pytester import Pytester from _pytest.pytester import Pytester
from _pytest.reports import CollectReport from _pytest.reports import CollectReport
@ -225,18 +224,26 @@ class TestReportSerialization:
assert newrep.longrepr == str(rep.longrepr) assert newrep.longrepr == str(rep.longrepr)
def test_paths_support(self, pytester: Pytester) -> None: def test_paths_support(self, pytester: Pytester) -> None:
"""Report attributes which are py.path or pathlib objects should become strings.""" """Report attributes which are path-like should become strings."""
pytester.makepyfile( pytester.makepyfile(
""" """
def test_a(): def test_a():
assert False assert False
""" """
) )
class MyPathLike:
def __init__(self, path: str) -> None:
self.path = path
def __fspath__(self) -> str:
return self.path
reprec = pytester.inline_run() reprec = pytester.inline_run()
reports = reprec.getreports("pytest_runtest_logreport") reports = reprec.getreports("pytest_runtest_logreport")
assert len(reports) == 3 assert len(reports) == 3
test_a_call = reports[1] test_a_call = reports[1]
test_a_call.path1 = legacy_path(pytester.path) # type: ignore[attr-defined] test_a_call.path1 = MyPathLike(str(pytester.path)) # type: ignore[attr-defined]
test_a_call.path2 = pytester.path # type: ignore[attr-defined] test_a_call.path2 = pytester.path # type: ignore[attr-defined]
data = test_a_call._to_json() data = test_a_call._to_json()
assert data["path1"] == str(pytester.path) assert data["path1"] == str(pytester.path)