diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f7f80fb8a..2a7da8136 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -45,6 +45,8 @@ jobs: "macos-py38", "linting", + "docs", + "doctesting", ] include: @@ -114,7 +116,17 @@ jobs: - name: "linting" python: "3.7" os: ubuntu-latest - tox_env: "linting,docs,doctesting" + tox_env: "linting" + skip_coverage: true + - name: "docs" + python: "3.7" + os: ubuntu-latest + tox_env: "docs" + skip_coverage: true + - name: "doctesting" + python: "3.7" + os: ubuntu-latest + tox_env: "doctesting" steps: - uses: actions/checkout@v1 @@ -144,21 +156,11 @@ jobs: run: | python scripts/append_codecov_token.py - - name: Combine coverage + - name: Report coverage if: (!matrix.skip_coverage) - run: | - python -m coverage combine - python -m coverage xml - - - name: Codecov upload - if: (!matrix.skip_coverage) - uses: codecov/codecov-action@v1 - with: - token: ${{ secrets.codecov }} - file: ./coverage.xml - flags: ${{ runner.os }} - fail_ci_if_error: false - name: ${{ matrix.name }} + env: + CODECOV_NAME: ${{ matrix.name }} + run: bash scripts/report-coverage.sh -F GHA,${{ runner.os }} deploy: if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') && github.repository == 'pytest-dev/pytest' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d750b297f..7cc5c4948 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -64,7 +64,7 @@ repos: _code\.| builtin\.| code\.| - io\.(BytesIO|saferepr)| + io\.(BytesIO|saferepr|TerminalWriter)| path\.local\.sysfind| process\.| std\. diff --git a/.travis.yml b/.travis.yml index d813cf07a..59c7951e4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -47,11 +47,6 @@ jobs: python: '3.5.1' dist: trusty - - env: TOXENV=linting,docs,doctesting PYTEST_COVERAGE=1 - cache: - directories: - - $HOME/.cache/pre-commit - before_script: - | # Do not (re-)upload coverage with cron runs. @@ -71,7 +66,7 @@ script: tox after_success: - | if [[ "$PYTEST_COVERAGE" = 1 ]]; then - env CODECOV_NAME="$TOXENV-$TRAVIS_OS_NAME" scripts/report-coverage.sh + env CODECOV_NAME="$TOXENV-$TRAVIS_OS_NAME" scripts/report-coverage.sh -F Travis fi notifications: diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 455998b78..0474fa3a3 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -166,7 +166,7 @@ Short version #. Fork the repository. #. Enable and install `pre-commit `_ to ensure style-guides and code checks are followed. -#. Target ``master`` for bugfixes and doc changes. +#. Target ``master`` for bug fixes and doc changes. #. Target ``features`` for new features or functionality changes. #. Follow **PEP-8** for naming and `black `_ for formatting. #. Tests are run using ``tox``:: @@ -212,7 +212,7 @@ Here is a simple overview, with pytest-specific bits: $ git checkout -b your-feature-branch-name features - Given we have "major.minor.micro" version numbers, bugfixes will usually + Given we have "major.minor.micro" version numbers, bug fixes will usually be released in micro releases whereas features will be released in minor releases and incompatible changes in major releases. @@ -294,7 +294,7 @@ Here is a simple overview, with pytest-specific bits: compare: your-branch-name base-fork: pytest-dev/pytest - base: master # if it's a bugfix + base: master # if it's a bug fix base: features # if it's a feature diff --git a/HOWTORELEASE.rst b/HOWTORELEASE.rst index d0704b172..b6b596ba2 100644 --- a/HOWTORELEASE.rst +++ b/HOWTORELEASE.rst @@ -1,14 +1,14 @@ Release Procedure ----------------- -Our current policy for releasing is to aim for a bugfix every few weeks and a minor release every 2-3 months. The idea +Our current policy for releasing is to aim for a bug-fix release every few weeks and a minor release every 2-3 months. The idea is to get fixes and new features out instead of trying to cram a ton of features into a release and by consequence taking a lot of time to make a new one. .. important:: pytest releases must be prepared on **Linux** because the docs and examples expect - to be executed in that platform. + to be executed on that platform. #. Create a branch ``release-X.Y.Z`` with the version for the release. diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index a6d856d91..000000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,80 +0,0 @@ -trigger: -- master -- features - -variables: - PYTEST_ADDOPTS: "--junitxml=build/test-results/$(tox.env).xml -vv" - PYTEST_COVERAGE: '0' - -jobs: - -- job: 'Test' - pool: - vmImage: "vs2017-win2016" - strategy: - matrix: - # -- pypy3 disabled for now: #5279 -- - # pypy3: - # python.version: 'pypy3' - # tox.env: 'pypy3' - py35-xdist: - python.version: '3.5' - tox.env: 'py35-xdist' - # Coverage for: - # - test_supports_breakpoint_module_global - PYTEST_COVERAGE: '1' - py36-xdist: - python.version: '3.6' - tox.env: 'py36-xdist' - py37: - python.version: '3.7' - tox.env: 'py37-twisted-numpy' - # Coverage for: - # - _py36_windowsconsoleio_workaround (with py36+) - # - test_request_garbage (no xdist) - PYTEST_COVERAGE: '1' - py37-linting/docs/doctesting: - python.version: '3.7' - tox.env: 'linting,docs,doctesting' - py37-pluggymaster-xdist: - python.version: '3.7' - tox.env: 'py37-pluggymaster-xdist' - py38-xdist: - python.version: '3.8' - tox.env: 'py38-xdist' - maxParallel: 10 - - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: '$(python.version)' - architecture: 'x64' - - - script: python -m pip install --upgrade pip && python -m pip install tox - displayName: 'Install tox' - - - bash: | - if [[ "$PYTEST_COVERAGE" == "1" ]]; then - export _PYTEST_TOX_COVERAGE_RUN="coverage run -m" - export _PYTEST_TOX_EXTRA_DEP=coverage-enable-subprocess - export COVERAGE_FILE="$PWD/.coverage" - export COVERAGE_PROCESS_START="$PWD/.coveragerc" - fi - python -m tox -e $(tox.env) - displayName: 'Run tests' - - - task: PublishTestResults@2 - inputs: - testResultsFiles: 'build/test-results/$(tox.env).xml' - testRunTitle: '$(tox.env)' - condition: succeededOrFailed() - - - bash: | - if [[ "$PYTEST_COVERAGE" == 1 ]]; then - scripts/report-coverage.sh - fi - env: - CODECOV_NAME: $(tox.env) - CODECOV_TOKEN: $(CODECOV_TOKEN) - displayName: Report and upload coverage - condition: eq(variables['PYTEST_COVERAGE'], '1') diff --git a/changelog/README.rst b/changelog/README.rst index dd0e7dfea..d91eb81e1 100644 --- a/changelog/README.rst +++ b/changelog/README.rst @@ -15,7 +15,7 @@ Each file should be named like ``..rst``, where * ``feature``: new user facing features, like new command-line options and new behavior. * ``improvement``: improvement of existing functionality, usually without requiring user intervention (for example, new fields being written in ``--junitxml``, improved colors in terminal, etc). -* ``bugfix``: fixes a reported bug. +* ``bugfix``: fixes a bug. * ``doc``: documentation improvement, like rewording an entire session or adding missing docs. * ``deprecation``: feature deprecation. * ``breaking``: a change which may break existing suites, such as feature removal or behavior change. diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 0bda6bb54..e0a2495cc 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -2357,7 +2357,7 @@ Deprecations and Removals - `#4036 `_: The ``item`` parameter of ``pytest_warning_captured`` hook is now documented as deprecated. We realized only after the ``3.8`` release that this parameter is incompatible with ``pytest-xdist``. - Our policy is to not deprecate features during bugfix releases, but in this case we believe it makes sense as we are + Our policy is to not deprecate features during bug-fix releases, but in this case we believe it makes sense as we are only documenting it as deprecated, without issuing warnings which might potentially break test suites. This will get the word out that hook implementers should not use this parameter at all. @@ -5380,7 +5380,7 @@ time or change existing behaviors in order to make them less surprising/more use Thanks Daniel Grunwald for the report and Bruno Oliveira for the PR. - (experimental) adapt more SEMVER style versioning and change meaning of - master branch in git repo: "master" branch now keeps the bugfixes, changes + master branch in git repo: "master" branch now keeps the bug fixes, changes aimed for micro releases. "features" branch will only be released with minor or major pytest releases. diff --git a/doc/en/development_guide.rst b/doc/en/development_guide.rst index 649419316..31fc2c438 100644 --- a/doc/en/development_guide.rst +++ b/doc/en/development_guide.rst @@ -19,7 +19,7 @@ Branches We have two long term branches: -* ``master``: contains the code for the next bugfix release. +* ``master``: contains the code for the next bug-fix release. * ``features``: contains the code with new features for the next minor release. The official repository usually does not contain topic branches, developers and contributors should create topic diff --git a/doc/en/example/costlysetup/conftest.py b/doc/en/example/costlysetup/conftest.py deleted file mode 100644 index 803559834..000000000 --- a/doc/en/example/costlysetup/conftest.py +++ /dev/null @@ -1,20 +0,0 @@ -import pytest - - -@pytest.fixture(scope="session") -def setup(request): - setup = CostlySetup() - yield setup - setup.finalize() - - -class CostlySetup: - def __init__(self): - import time - - print("performing costly setup") - time.sleep(5) - self.timecostly = 1 - - def finalize(self): - del self.timecostly diff --git a/doc/en/example/costlysetup/sub_a/__init__.py b/doc/en/example/costlysetup/sub_a/__init__.py deleted file mode 100644 index 792d60054..000000000 --- a/doc/en/example/costlysetup/sub_a/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# diff --git a/doc/en/example/costlysetup/sub_a/test_quick.py b/doc/en/example/costlysetup/sub_a/test_quick.py deleted file mode 100644 index 38dda2660..000000000 --- a/doc/en/example/costlysetup/sub_a/test_quick.py +++ /dev/null @@ -1,2 +0,0 @@ -def test_quick(setup): - pass diff --git a/doc/en/example/costlysetup/sub_b/__init__.py b/doc/en/example/costlysetup/sub_b/__init__.py deleted file mode 100644 index 792d60054..000000000 --- a/doc/en/example/costlysetup/sub_b/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# diff --git a/doc/en/example/costlysetup/sub_b/test_two.py b/doc/en/example/costlysetup/sub_b/test_two.py deleted file mode 100644 index b1653aaab..000000000 --- a/doc/en/example/costlysetup/sub_b/test_two.py +++ /dev/null @@ -1,6 +0,0 @@ -def test_something(setup): - assert setup.timecostly == 1 - - -def test_something_more(setup): - assert setup.timecostly == 1 diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index db06a4015..2094027f3 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -1042,11 +1042,13 @@ file: import pytest - @pytest.fixture() + @pytest.fixture def cleandir(): + old_cwd = os.getcwd() newpath = tempfile.mkdtemp() os.chdir(newpath) yield + os.chdir(old_cwd) shutil.rmtree(newpath) and declare its use in a test module via a ``usefixtures`` marker: diff --git a/scripts/release.minor.rst b/scripts/release.minor.rst index 9a488edbc..f71f9b1b6 100644 --- a/scripts/release.minor.rst +++ b/scripts/release.minor.rst @@ -6,7 +6,7 @@ The pytest team is proud to announce the {version} release! pytest is a mature Python testing tool with more than a 2000 tests against itself, passing on many different interpreters and platforms. -This release contains a number of bugs fixes and improvements, so users are encouraged +This release contains a number of bug fixes and improvements, so users are encouraged to take a look at the CHANGELOG: https://docs.pytest.org/en/latest/changelog.html @@ -15,7 +15,7 @@ For complete documentation, please visit: https://docs.pytest.org/en/latest/ -As usual, you can upgrade from pypi via: +As usual, you can upgrade from PyPI via: pip install -U pytest @@ -24,4 +24,4 @@ Thanks to all who contributed to this release, among them: {contributors} Happy testing, -The Pytest Development Team +The pytest Development Team diff --git a/scripts/report-coverage.sh b/scripts/report-coverage.sh index 165426a11..fbcf20ca9 100755 --- a/scripts/report-coverage.sh +++ b/scripts/report-coverage.sh @@ -15,4 +15,4 @@ python -m coverage xml python -m coverage report -m # Set --connect-timeout to work around https://github.com/curl/curl/issues/4461 curl -S -L --connect-timeout 5 --retry 6 -s https://codecov.io/bash -o codecov-upload.sh -bash codecov-upload.sh -Z -X fix -f coverage.xml +bash codecov-upload.sh -Z -X fix -f coverage.xml "$@" diff --git a/scripts/retry.cmd b/scripts/retry.cmd deleted file mode 100644 index ac3836508..000000000 --- a/scripts/retry.cmd +++ /dev/null @@ -1,21 +0,0 @@ -@echo off -rem Source: https://github.com/appveyor/ci/blob/master/scripts/appveyor-retry.cmd -rem initiate the retry number -set retryNumber=0 -set maxRetries=3 - -:RUN -%* -set LastErrorLevel=%ERRORLEVEL% -IF %LastErrorLevel% == 0 GOTO :EOF -set /a retryNumber=%retryNumber%+1 -IF %reTryNumber% == %maxRetries% (GOTO :FAILED) - -:RETRY -set /a retryNumberDisp=%retryNumber%+1 -@echo Command "%*" failed with exit code %LastErrorLevel%. Retrying %retryNumberDisp% of %maxRetries% -GOTO :RUN - -: FAILED -@echo Sorry, we tried running command for %maxRetries% times and all attempts were unsuccessful! -EXIT /B %LastErrorLevel% diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index e18106f31..4cb373cc4 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -29,6 +29,7 @@ import pluggy import py import _pytest +from _pytest._io import TerminalWriter from _pytest._io.saferepr import safeformat from _pytest._io.saferepr import saferepr from _pytest.compat import overload @@ -913,14 +914,14 @@ class TerminalRepr: # FYI this is called from pytest-xdist's serialization of exception # information. io = StringIO() - tw = py.io.TerminalWriter(file=io) + tw = TerminalWriter(file=io) self.toterminal(tw) return io.getvalue().strip() def __repr__(self) -> str: return "<{} instance at {:0x}>".format(self.__class__, id(self)) - def toterminal(self, tw: py.io.TerminalWriter) -> None: + def toterminal(self, tw: TerminalWriter) -> None: raise NotImplementedError() @@ -931,7 +932,7 @@ class ExceptionRepr(TerminalRepr): def addsection(self, name: str, content: str, sep: str = "-") -> None: self.sections.append((name, content, sep)) - def toterminal(self, tw: py.io.TerminalWriter) -> None: + def toterminal(self, tw: TerminalWriter) -> None: for name, content, sep in self.sections: tw.sep(sep, name) tw.line(content) @@ -951,7 +952,7 @@ class ExceptionChainRepr(ExceptionRepr): self.reprtraceback = chain[-1][0] self.reprcrash = chain[-1][1] - def toterminal(self, tw: py.io.TerminalWriter) -> None: + def toterminal(self, tw: TerminalWriter) -> None: for element in self.chain: element[0].toterminal(tw) if element[2] is not None: @@ -968,7 +969,7 @@ class ReprExceptionInfo(ExceptionRepr): self.reprtraceback = reprtraceback self.reprcrash = reprcrash - def toterminal(self, tw: py.io.TerminalWriter) -> None: + def toterminal(self, tw: TerminalWriter) -> None: self.reprtraceback.toterminal(tw) super().toterminal(tw) @@ -986,7 +987,7 @@ class ReprTraceback(TerminalRepr): self.extraline = extraline self.style = style - def toterminal(self, tw: py.io.TerminalWriter) -> None: + def toterminal(self, tw: TerminalWriter) -> None: # the entries might have different styles for i, entry in enumerate(self.reprentries): if entry.style == "long": @@ -1018,7 +1019,7 @@ class ReprEntryNative(TerminalRepr): def __init__(self, tblines: Sequence[str]) -> None: self.lines = tblines - def toterminal(self, tw: py.io.TerminalWriter) -> None: + def toterminal(self, tw: TerminalWriter) -> None: tw.write("".join(self.lines)) @@ -1037,7 +1038,7 @@ class ReprEntry(TerminalRepr): self.reprfileloc = filelocrepr self.style = style - def toterminal(self, tw: py.io.TerminalWriter) -> None: + def toterminal(self, tw: TerminalWriter) -> None: if self.style == "short": assert self.reprfileloc is not None self.reprfileloc.toterminal(tw) @@ -1072,7 +1073,7 @@ class ReprFileLocation(TerminalRepr): self.lineno = lineno self.message = message - def toterminal(self, tw: py.io.TerminalWriter) -> None: + def toterminal(self, tw: TerminalWriter) -> None: # filename and lineno output for each entry, # using an output format that most editors understand msg = self.message @@ -1087,7 +1088,7 @@ class ReprLocals(TerminalRepr): def __init__(self, lines: Sequence[str]) -> None: self.lines = lines - def toterminal(self, tw: py.io.TerminalWriter, indent="") -> None: + def toterminal(self, tw: TerminalWriter, indent="") -> None: for line in self.lines: tw.line(indent + line) @@ -1096,7 +1097,7 @@ class ReprFuncArgs(TerminalRepr): def __init__(self, args: Sequence[Tuple[str, object]]) -> None: self.args = args - def toterminal(self, tw: py.io.TerminalWriter) -> None: + def toterminal(self, tw: TerminalWriter) -> None: if self.args: linesofar = "" for name, value in self.args: diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index 46ffde856..c7b103114 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -277,7 +277,7 @@ def compile_( # noqa: F811 return s.compile(filename, mode, flags, _genframe=_genframe) -def getfslineno(obj) -> Tuple[Union[str, py.path.local], int]: +def getfslineno(obj) -> Tuple[Optional[Union["Literal['']", py.path.local]], int]: """ Return source location (path, lineno) for the given object. If the source cannot be determined return ("", -1). diff --git a/src/_pytest/_io/__init__.py b/src/_pytest/_io/__init__.py index e69de29bb..047bb179a 100644 --- a/src/_pytest/_io/__init__.py +++ b/src/_pytest/_io/__init__.py @@ -0,0 +1,3 @@ +# Reexport TerminalWriter from here instead of py, to make it easier to +# extend or swap our own implementation in the future. +from py.io import TerminalWriter as TerminalWriter # noqa: F401 diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index b28b3a1b7..2f7f88454 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -17,6 +17,7 @@ from .pathlib import Path from .pathlib import resolve_from_str from .pathlib import rm_rf from _pytest import nodes +from _pytest._io import TerminalWriter from _pytest.config import Config from _pytest.main import Session @@ -418,7 +419,7 @@ def pytest_report_header(config): def cacheshow(config, session): from pprint import pformat - tw = py.io.TerminalWriter() + tw = TerminalWriter() tw.line("cachedir: " + str(config.cache._cachedir)) if not config.cache._cachedir.is_dir(): tw.line("cache is empty") diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index daa802ae1..9ddf49316 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -308,7 +308,7 @@ def get_real_method(obj, holder): return obj -def getfslineno(obj): +def getfslineno(obj) -> Tuple[Union[str, py.path.local], int]: # xxx let decorators etc specify a sane ordering obj = get_real_func(obj) if hasattr(obj, "place_as"): diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index d6a704b1d..bc62297c1 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -36,6 +36,7 @@ from .findpaths import determine_setup from .findpaths import exists from _pytest._code import ExceptionInfo from _pytest._code import filter_traceback +from _pytest._io import TerminalWriter from _pytest.compat import importlib_metadata from _pytest.compat import TYPE_CHECKING from _pytest.outcomes import fail @@ -75,7 +76,7 @@ def main(args=None, plugins=None) -> "Union[int, _pytest.main.ExitCode]": config = _prepareconfig(args, plugins) except ConftestImportFailure as e: exc_info = ExceptionInfo(e.excinfo) - tw = py.io.TerminalWriter(sys.stderr) + tw = TerminalWriter(sys.stderr) tw.line( "ImportError while loading conftest '{e.path}'.".format(e=e), red=True ) @@ -101,7 +102,7 @@ def main(args=None, plugins=None) -> "Union[int, _pytest.main.ExitCode]": finally: config._ensure_unconfigure() except UsageError as e: - tw = py.io.TerminalWriter(sys.stderr) + tw = TerminalWriter(sys.stderr) for msg in e.args: tw.line("ERROR: {}\n".format(msg), red=True) return ExitCode.USAGE_ERROR @@ -1177,12 +1178,12 @@ def setns(obj, dic): setattr(pytest, name, value) -def create_terminal_writer(config, *args, **kwargs): +def create_terminal_writer(config: Config, *args, **kwargs) -> TerminalWriter: """Create a TerminalWriter instance configured according to the options in the config object. Every code which requires a TerminalWriter object and has access to a config object should use this function. """ - tw = py.io.TerminalWriter(*args, **kwargs) + tw = TerminalWriter(*args, **kwargs) if config.option.color == "yes": tw.hasmarkup = True if config.option.color == "no": diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index fa9d8f5dc..140e04e97 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -102,8 +102,8 @@ class Parser: self.optparser = self._getparser() try_argcomplete(self.optparser) - args = [str(x) if isinstance(x, py.path.local) else x for x in args] - return self.optparser.parse_args(args, namespace=namespace) + strargs = [str(x) if isinstance(x, py.path.local) else x for x in args] + return self.optparser.parse_args(strargs, namespace=namespace) def _getparser(self) -> "MyOptionParser": from _pytest._argcomplete import filescompleter @@ -154,8 +154,8 @@ class Parser: the remaining arguments unknown at this point. """ optparser = self._getparser() - args = [str(x) if isinstance(x, py.path.local) else x for x in args] - return optparser.parse_known_args(args, namespace=namespace) + strargs = [str(x) if isinstance(x, py.path.local) else x for x in args] + return optparser.parse_known_args(strargs, namespace=namespace) def addini( self, diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 707ce969d..fb84160c1 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -1,6 +1,9 @@ import os +from typing import Any +from typing import Iterable from typing import List from typing import Optional +from typing import Tuple import py @@ -60,7 +63,7 @@ def getcfg(args, config=None): return None, None, None -def get_common_ancestor(paths): +def get_common_ancestor(paths: Iterable[py.path.local]) -> py.path.local: common_ancestor = None for path in paths: if not path.exists(): @@ -113,7 +116,7 @@ def determine_setup( args: List[str], rootdir_cmd_arg: Optional[str] = None, config: Optional["Config"] = None, -): +) -> Tuple[py.path.local, Optional[str], Any]: dirs = get_dirs_from_args(args) if inifile: iniconfig = py.iniconfig.IniConfig(inifile) diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index 4714b2c1d..9afd423db 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -13,13 +13,14 @@ from typing import Sequence from typing import Tuple from typing import Union -import py +import py.path import pytest from _pytest import outcomes from _pytest._code.code import ExceptionInfo from _pytest._code.code import ReprFileLocation from _pytest._code.code import TerminalRepr +from _pytest._io import TerminalWriter from _pytest.compat import safe_getattr from _pytest.compat import TYPE_CHECKING from _pytest.fixtures import FixtureRequest @@ -139,7 +140,7 @@ class ReprFailDoctest(TerminalRepr): ): self.reprlocation_lines = reprlocation_lines - def toterminal(self, tw: py.io.TerminalWriter) -> None: + def toterminal(self, tw: TerminalWriter) -> None: for reprlocation, lines in self.reprlocation_lines: for line in lines: tw.line(line) @@ -312,7 +313,7 @@ class DoctestItem(pytest.Item): else: return super().repr_failure(excinfo) - def reportinfo(self) -> Tuple[str, int, str]: + def reportinfo(self) -> Tuple[py.path.local, int, str]: return self.fspath, self.dtest.lineno, "[doctest] %s" % self.name diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index dabd09297..9e60d56ce 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -16,6 +16,7 @@ import py import _pytest from _pytest._code.code import FormattedExcinfo from _pytest._code.code import TerminalRepr +from _pytest._io import TerminalWriter from _pytest.compat import _format_args from _pytest.compat import _PytestWrapper from _pytest.compat import get_real_func @@ -352,7 +353,7 @@ class FixtureRequest: self.fixturename = None #: Scope string, one of "function", "class", "module", "session" self.scope = "function" - self._fixture_defs = {} # argname -> FixtureDef + self._fixture_defs = {} # type: Dict[str, FixtureDef] fixtureinfo = pyfuncitem._fixtureinfo self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy() self._arg2index = {} @@ -427,7 +428,8 @@ class FixtureRequest: @scopeproperty() def fspath(self) -> py.path.local: """ the file system path of the test module which collected this test. """ - return self._pyfuncitem.fspath + # TODO: Remove ignore once _pyfuncitem is properly typed. + return self._pyfuncitem.fspath # type: ignore @property def keywords(self): @@ -547,7 +549,9 @@ class FixtureRequest: source_path = py.path.local(frameinfo.filename) source_lineno = frameinfo.lineno if source_path.relto(funcitem.config.rootdir): - source_path = source_path.relto(funcitem.config.rootdir) + source_path_str = source_path.relto(funcitem.config.rootdir) + else: + source_path_str = str(source_path) msg = ( "The requested fixture has no parameter defined for test:\n" " {}\n\n" @@ -556,7 +560,7 @@ class FixtureRequest: funcitem.nodeid, fixturedef.argname, getlocation(fixturedef.func, funcitem.config.rootdir), - source_path, + source_path_str, source_lineno, ) ) @@ -749,7 +753,7 @@ class FixtureLookupErrorRepr(TerminalRepr): self.firstlineno = firstlineno self.argname = argname - def toterminal(self, tw: py.io.TerminalWriter) -> None: + def toterminal(self, tw: TerminalWriter) -> None: # tw.line("FixtureLookupError: %s" %(self.argname), red=True) for tbline in self.tblines: tw.line(tbline.rstrip()) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 057dae4f4..2e67066cd 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -15,6 +15,7 @@ import py import _pytest._code from _pytest import nodes from _pytest.compat import TYPE_CHECKING +from _pytest.config import Config from _pytest.config import directory_arg from _pytest.config import hookimpl from _pytest.config import UsageError @@ -375,9 +376,9 @@ class Failed(Exception): @attr.s class _bestrelpath_cache(dict): - path = attr.ib() + path = attr.ib(type=py.path.local) - def __missing__(self, path: str) -> str: + def __missing__(self, path: py.path.local) -> str: r = self.path.bestrelpath(path) # type: str self[path] = r return r @@ -391,7 +392,7 @@ class Session(nodes.FSCollector): # Set on the session by fixtures.pytest_sessionstart. _fixturemanager = None # type: FixtureManager - def __init__(self, config) -> None: + def __init__(self, config: Config) -> None: nodes.FSCollector.__init__( self, config.rootdir, parent=None, config=config, session=self, nodeid="" ) @@ -411,7 +412,7 @@ class Session(nodes.FSCollector): self._bestrelpathcache = _bestrelpath_cache( config.rootdir - ) # type: Dict[str, str] + ) # type: Dict[py.path.local, str] self.config.pluginmanager.register(self, name="session") @@ -428,7 +429,7 @@ class Session(nodes.FSCollector): self.testscollected, ) - def _node_location_to_relpath(self, node_path: str) -> str: + def _node_location_to_relpath(self, node_path: py.path.local) -> str: # bestrelpath is a quite slow function return self._bestrelpathcache[node_path] diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 6e3e79208..4f38fd889 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -482,6 +482,10 @@ class Item(Node): @cached_property def location(self) -> Tuple[str, Optional[int], str]: location = self.reportinfo() - fspath = self.session._node_location_to_relpath(location[0]) + if isinstance(location[0], py.path.local): + fspath = location[0] + else: + fspath = py.path.local(location[0]) + relfspath = self.session._node_location_to_relpath(fspath) assert type(location[2]) is str - return (fspath, location[1], location[2]) + return (relfspath, location[1], location[2]) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 281fa3cfe..de2a93344 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -13,6 +13,7 @@ from textwrap import dedent from typing import List from typing import Optional from typing import Tuple +from typing import Union import py @@ -282,15 +283,16 @@ class PyobjMixin(PyobjContext): parts.reverse() return ".".join(parts) - def reportinfo(self) -> Tuple[str, int, str]: + def reportinfo(self) -> Tuple[Union[py.path.local, str], int, str]: # XXX caching? obj = self.obj compat_co_firstlineno = getattr(obj, "compat_co_firstlineno", None) if isinstance(compat_co_firstlineno, int): # nose compatibility - fspath = sys.modules[obj.__module__].__file__ - if fspath.endswith(".pyc"): - fspath = fspath[:-1] + file_path = sys.modules[obj.__module__].__file__ + if file_path.endswith(".pyc"): + file_path = file_path[:-1] + fspath = file_path # type: Union[py.path.local, str] lineno = compat_co_firstlineno else: fspath, lineno = getfslineno(obj) @@ -369,7 +371,12 @@ class PyCollector(PyobjMixin, nodes.Collector): if not isinstance(res, list): res = [res] values.extend(res) - values.sort(key=lambda item: item.reportinfo()[:2]) + + def sort_key(item): + fspath, lineno, _ = item.reportinfo() + return (str(fspath), lineno) + + values.sort(key=sort_key) return values def _makeitem(self, name, obj): diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 79e106a65..3ad67c224 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -18,6 +18,7 @@ from _pytest._code.code import ReprFuncArgs from _pytest._code.code import ReprLocals from _pytest._code.code import ReprTraceback from _pytest._code.code import TerminalRepr +from _pytest._io import TerminalWriter from _pytest.compat import TYPE_CHECKING from _pytest.nodes import Node from _pytest.outcomes import skip @@ -80,7 +81,7 @@ class BaseReport: .. versionadded:: 3.0 """ - tw = py.io.TerminalWriter(stringio=True) + tw = TerminalWriter(stringio=True) tw.hasmarkup = False self.toterminal(tw) exc = tw.stringio.getvalue() diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index ae5d30b3a..c67201191 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -12,7 +12,8 @@ import pytest from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionInfo from _pytest._code.code import FormattedExcinfo - +from _pytest._io import TerminalWriter +from _pytest.pytester import LineMatcher try: import importlib @@ -775,14 +776,43 @@ raise ValueError() ) excinfo = pytest.raises(ValueError, mod.entry) - p = FormattedExcinfo() + p = FormattedExcinfo(abspath=False) + + raised = 0 + + orig_getcwd = os.getcwd def raiseos(): - raise OSError(2) + nonlocal raised + if sys._getframe().f_back.f_code.co_name == "checked_call": + # Only raise with expected calls, but not via e.g. inspect for + # py38-windows. + raised += 1 + raise OSError(2, "custom_oserror") + return orig_getcwd() monkeypatch.setattr(os, "getcwd", raiseos) assert p._makepath(__file__) == __file__ - p.repr_traceback(excinfo) + assert raised == 1 + repr_tb = p.repr_traceback(excinfo) + + matcher = LineMatcher(str(repr_tb).splitlines()) + matcher.fnmatch_lines( + [ + "def entry():", + "> f(0)", + "", + "{}:5: ".format(mod.__file__), + "_ _ *", + "", + " def f(x):", + "> raise ValueError(x)", + "E ValueError: 0", + "", + "{}:3: ValueError".format(mod.__file__), + ] + ) + assert raised == 3 def test_repr_excinfo_addouterr(self, importasmod, tw_mock): mod = importasmod( @@ -855,7 +885,7 @@ raise ValueError() from _pytest._code.code import TerminalRepr class MyRepr(TerminalRepr): - def toterminal(self, tw: py.io.TerminalWriter) -> None: + def toterminal(self, tw: TerminalWriter) -> None: tw.line("я") x = str(MyRepr()) @@ -1005,7 +1035,7 @@ raise ValueError() """ ) excinfo = pytest.raises(ValueError, mod.f) - tw = py.io.TerminalWriter(stringio=True) + tw = TerminalWriter(stringio=True) repr = excinfo.getrepr(**reproptions) repr.toterminal(tw) assert tw.stringio.getvalue() @@ -1200,8 +1230,6 @@ raise ValueError() real traceback, such as those raised in a subprocess submitted by the multiprocessing module (#1984). """ - from _pytest.pytester import LineMatcher - exc_handling_code = " from e" if reason == "cause" else "" mod = importasmod( """ @@ -1225,7 +1253,7 @@ raise ValueError() getattr(excinfo.value, attr).__traceback__ = None r = excinfo.getrepr() - tw = py.io.TerminalWriter(stringio=True) + tw = TerminalWriter(stringio=True) tw.hasmarkup = False r.toterminal(tw) @@ -1320,7 +1348,6 @@ def test_exception_repr_extraction_error_on_recursion(): Ensure we can properly detect a recursion error even if some locals raise error on comparison (#2459). """ - from _pytest.pytester import LineMatcher class numpy_like: def __eq__(self, other): diff --git a/testing/logging/test_formatter.py b/testing/logging/test_formatter.py index b363e8b03..85e949d7a 100644 --- a/testing/logging/test_formatter.py +++ b/testing/logging/test_formatter.py @@ -1,7 +1,6 @@ import logging -import py.io - +from _pytest._io import TerminalWriter from _pytest.logging import ColoredLevelFormatter @@ -22,7 +21,7 @@ def test_coloredlogformatter(): class option: pass - tw = py.io.TerminalWriter() + tw = TerminalWriter() tw.hasmarkup = True formatter = ColoredLevelFormatter(tw, logfmt) output = formatter.format(record) @@ -142,7 +141,7 @@ def test_colored_short_level(): class option: pass - tw = py.io.TerminalWriter() + tw = TerminalWriter() tw.hasmarkup = True formatter = ColoredLevelFormatter(tw, logfmt) output = formatter.format(record) diff --git a/testing/python/collect.py b/testing/python/collect.py index 8e85fb074..b30921fe3 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -68,7 +68,7 @@ class TestModule: def test_invalid_test_module_name(self, testdir): a = testdir.mkdir("a") a.ensure("test_one.part1.py") - result = testdir.runpytest("-rw") + result = testdir.runpytest() result.stdout.fnmatch_lines( [ "ImportError while importing test module*test_one.part1*", @@ -137,7 +137,7 @@ class TestClass: pass """ ) - result = testdir.runpytest("-rw") + result = testdir.runpytest() result.stdout.fnmatch_lines( [ "*cannot collect test class 'TestClass1' because it has " @@ -153,7 +153,7 @@ class TestClass: pass """ ) - result = testdir.runpytest("-rw") + result = testdir.runpytest() result.stdout.fnmatch_lines( [ "*cannot collect test class 'TestClass1' because it has " @@ -230,7 +230,7 @@ class TestClass: TestCase = collections.namedtuple('TestCase', ['a']) """ ) - result = testdir.runpytest("-rw") + result = testdir.runpytest() result.stdout.fnmatch_lines( "*cannot collect test class 'TestCase' " "because it has a __new__ constructor*" @@ -1162,7 +1162,7 @@ def test_dont_collect_non_function_callable(testdir): pass """ ) - result = testdir.runpytest("-rw") + result = testdir.runpytest() result.stdout.fnmatch_lines( [ "*collected 1 item*", diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 26374bc34..8cfaae50d 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -4207,3 +4207,38 @@ def test_fixture_parametrization_nparray(testdir): ) result = testdir.runpytest() result.assert_outcomes(passed=10) + + +def test_fixture_arg_ordering(testdir): + """ + This test describes how fixtures in the same scope but without explicit dependencies + between them are created. While users should make dependencies explicit, often + they rely on this order, so this test exists to catch regressions in this regard. + See #6540 and #6492. + """ + p1 = testdir.makepyfile( + """ + import pytest + + suffixes = [] + + @pytest.fixture + def fix_1(): suffixes.append("fix_1") + @pytest.fixture + def fix_2(): suffixes.append("fix_2") + @pytest.fixture + def fix_3(): suffixes.append("fix_3") + @pytest.fixture + def fix_4(): suffixes.append("fix_4") + @pytest.fixture + def fix_5(): suffixes.append("fix_5") + + @pytest.fixture + def fix_combined(fix_1, fix_2, fix_3, fix_4, fix_5): pass + + def test_suffix(fix_combined): + assert suffixes == ["fix_1", "fix_2", "fix_3", "fix_4", "fix_5"] + """ + ) + result = testdir.runpytest("-vv", str(p1)) + assert result.ret == 0 diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 5c0425829..e975a3fea 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1349,7 +1349,7 @@ def test_assert_indirect_tuple_no_warning(testdir): assert tpl """ ) - result = testdir.runpytest("-rw") + result = testdir.runpytest() output = "\n".join(result.stdout.lines) assert "WR1" not in output diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index 925a1861e..2690a7de8 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -66,7 +66,7 @@ class TestNewAPI: testdir.tmpdir.ensure_dir(".pytest_cache").chmod(0) try: testdir.makepyfile("def test_error(): raise Exception") - result = testdir.runpytest("-rw") + result = testdir.runpytest() assert result.ret == 1 # warnings from nodeids, lastfailed, and stepwise result.stdout.fnmatch_lines( diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 62bd5cbe2..5d3fdcbb5 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1134,13 +1134,14 @@ def test_record_property(testdir, run_and_parse): record_property("foo", "<1"); """ ) - result, dom = run_and_parse("-rwv") + result, dom = run_and_parse() node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") psnode = tnode.find_first_by_tag("properties") pnodes = psnode.find_by_tag("property") pnodes[0].assert_attr(name="bar", value="1") pnodes[1].assert_attr(name="foo", value="<1") + result.stdout.fnmatch_lines(["*= 1 passed in *"]) def test_record_property_same_name(testdir, run_and_parse): @@ -1151,7 +1152,7 @@ def test_record_property_same_name(testdir, run_and_parse): record_property("foo", "baz") """ ) - result, dom = run_and_parse("-rw") + result, dom = run_and_parse() node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") psnode = tnode.find_first_by_tag("properties") @@ -1193,7 +1194,7 @@ def test_record_attribute(testdir, run_and_parse): record_xml_attribute("foo", "<1"); """ ) - result, dom = run_and_parse("-rw") + result, dom = run_and_parse() node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") tnode.assert_attr(bar="1") @@ -1228,7 +1229,7 @@ def test_record_fixtures_xunit2(testdir, fixture_name, run_and_parse): ) ) - result, dom = run_and_parse("-rw", family=None) + result, dom = run_and_parse(family=None) expected_lines = [] if fixture_name == "record_xml_attribute": expected_lines.append( diff --git a/testing/test_nose.py b/testing/test_nose.py index 469c127af..b6200c6c9 100644 --- a/testing/test_nose.py +++ b/testing/test_nose.py @@ -377,15 +377,48 @@ def test_skip_test_with_unicode(testdir): result.stdout.fnmatch_lines(["* 1 skipped *"]) -def test_issue_6517(testdir): +def test_raises(testdir): testdir.makepyfile( """ from nose.tools import raises @raises(RuntimeError) - def test_fail_without_tcp(): + def test_raises_runtimeerror(): raise RuntimeError + + @raises(Exception) + def test_raises_baseexception_not_caught(): + raise BaseException + + @raises(BaseException) + def test_raises_baseexception_caught(): + raise BaseException """ ) - result = testdir.runpytest() - result.stdout.fnmatch_lines(["* 1 passed *"]) + result = testdir.runpytest("-vv") + result.stdout.fnmatch_lines( + [ + "test_raises.py::test_raises_runtimeerror PASSED*", + "test_raises.py::test_raises_baseexception_not_caught FAILED*", + "test_raises.py::test_raises_baseexception_caught PASSED*", + "*= FAILURES =*", + "*_ test_raises_baseexception_not_caught _*", + "", + "arg = (), kw = {}", + "", + " def newfunc(*arg, **kw):", + " try:", + "> func(*arg, **kw)", + "", + "*/nose/*: ", + "_ _ *", + "", + " @raises(Exception)", + " def test_raises_baseexception_not_caught():", + "> raise BaseException", + "E BaseException", + "", + "test_raises.py:9: BaseException", + "* 1 failed, 2 passed *", + ] + ) diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 3b8351209..43026f0a3 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -256,7 +256,7 @@ class TestPytestPluginManager: ) p.copy(p.dirpath("skipping2.py")) monkeypatch.setenv("PYTEST_PLUGINS", "skipping2") - result = testdir.runpytest("-rw", "-p", "skipping1", syspathinsert=True) + result = testdir.runpytest("-p", "skipping1", syspathinsert=True) assert result.ret == ExitCode.NO_TESTS_COLLECTED result.stdout.fnmatch_lines( ["*skipped plugin*skipping1*hello*", "*skipped plugin*skipping2*hello*"]