diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f7f80fb8a2e..2a7da813669 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 d750b297f3b..7cc5c494890 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 d813cf07a80..59c7951e407 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 455998b785e..0474fa3a362 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 d0704b17279..b6b596ba2ea 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 a6d856d9187..00000000000 --- 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 dd0e7dfea83..d91eb81e132 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 0bda6bb54f5..e0a2495ccb7 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 649419316d1..31fc2c4386d 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 80355983466..00000000000 --- 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 792d6005489..00000000000 --- 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 38dda2660d2..00000000000 --- 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 792d6005489..00000000000 --- 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 b1653aaab88..00000000000 --- 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 db06a401564..2094027f304 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 9a488edbc52..f71f9b1b64c 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 165426a119e..fbcf20ca929 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 ac383650857..00000000000 --- 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 e18106f31d2..4cb373cc47a 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -29,6 +29,7 @@ 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 @@ def __str__(self) -> str: # 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 @@ def __init__(self) -> None: 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 @@ def __init__( 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 @@ def __init__( 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 @@ def __init__( 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 @@ def __init__( 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 @@ def __init__(self, path, lineno: int, message: str) -> None: 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 46ffde856ef..c7b10311427 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 e69de29bb2d..047bb179a89 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 b28b3a1b706..2f7f8845440 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -17,6 +17,7 @@ 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 daa802ae1a9..9ddf493169c 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 d6a704b1d8f..bc62297c16d 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -36,6 +36,7 @@ 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 fa9d8f5dc60..140e04e9723 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -102,8 +102,8 @@ def parse( 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 @@ def parse_known_and_unknown_args( 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 707ce969d93..fb84160c1ff 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 4714b2c1d61..9afd423db70 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -13,13 +13,14 @@ 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 @@ def __init__( ): 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 @@ def repr_failure(self, excinfo): 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 dabd0929763..9e60d56ce90 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -16,6 +16,7 @@ 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 @@ def __init__(self, pyfuncitem): 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 @@ def module(self): @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 @@ def _compute_fixture_value(self, fixturedef: "FixtureDef") -> None: 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 @@ def _compute_fixture_value(self, fixturedef: "FixtureDef") -> None: funcitem.nodeid, fixturedef.argname, getlocation(fixturedef.func, funcitem.config.rootdir), - source_path, + source_path_str, source_lineno, ) ) @@ -749,7 +753,7 @@ def __init__(self, filename, firstlineno, tblines, errorstring, argname): 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 057dae4f4f4..2e67066cd9e 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -15,6 +15,7 @@ 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 @@ def __init__(self, config) -> None: 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 @@ def __repr__(self): 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 6e3e792082c..4f38fd8896f 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -482,6 +482,10 @@ def reportinfo(self) -> Tuple[Union[py.path.local, str], Optional[int], str]: @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 281fa3cfe36..de2a9334409 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -13,6 +13,7 @@ from typing import List from typing import Optional from typing import Tuple +from typing import Union import py @@ -282,15 +283,16 @@ def getmodpath(self, stopatmodule=True, includemodule=False): 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 @@ def collect(self): 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 79e106a65ad..3ad67c224c4 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -18,6 +18,7 @@ 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 @@ def longreprtext(self): .. 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 ae5d30b3a15..c67201191e0 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -12,7 +12,8 @@ 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 @@ def entry(): ) 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 @@ def test_reprexcinfo_unicode(self): 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 @@ def f(): """ ) 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 @@ def test_exc_chain_repr_without_traceback(self, importasmod, reason, description 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 @@ def g(): 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 b363e8b03ff..85e949d7a78 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 @@ class ColorConfig: class option: pass - tw = py.io.TerminalWriter() + tw = TerminalWriter() tw.hasmarkup = True formatter = ColoredLevelFormatter(tw, logfmt) output = formatter.format(record) @@ -142,7 +141,7 @@ class ColorConfig: 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 8e85fb07499..b30921fe39f 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -68,7 +68,7 @@ def test_module_considers_pluginmanager_at_import(self, testdir): 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 @@ def __init__(self): pass """ ) - result = testdir.runpytest("-rw") + result = testdir.runpytest() result.stdout.fnmatch_lines( [ "*cannot collect test class 'TestClass1' because it has " @@ -153,7 +153,7 @@ def __new__(self): pass """ ) - result = testdir.runpytest("-rw") + result = testdir.runpytest() result.stdout.fnmatch_lines( [ "*cannot collect test class 'TestClass1' because it has " @@ -230,7 +230,7 @@ def test_issue1579_namedtuple(self, testdir): 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_real(): 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 26374bc34a8..8cfaae50d65 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -4207,3 +4207,38 @@ def test_bug(value): ) 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 5c0425829ab..e975a3fea2b 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1349,7 +1349,7 @@ def test_tuple(): 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 925a1861e8f..2690a7de8a5 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -66,7 +66,7 @@ def test_cache_failure_warns(self, testdir, monkeypatch): 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 62bd5cbe268..5d3fdcbb5cc 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1134,13 +1134,14 @@ def test_record(record_property, other): 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_with_same_name(record_property): 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(record_xml_attribute, other): 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({fixture_name}, other): ) ) - 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 469c127afc4..b6200c6c9ad 100644 --- a/testing/test_nose.py +++ b/testing/test_nose.py @@ -377,15 +377,48 @@ def test_io(self): 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 3b83512092e..43026f0a3d4 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -256,7 +256,7 @@ def test_plugin_skip(self, testdir, monkeypatch): ) 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*"]