Merge master into features (#5874)

Merge master into features
This commit is contained in:
Bruno Oliveira 2019-09-23 12:44:20 -03:00 committed by GitHub
commit c28b63135f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 532 additions and 121 deletions

View File

@ -176,6 +176,7 @@ mbyt
Michael Aquilina Michael Aquilina
Michael Birtwell Michael Birtwell
Michael Droettboom Michael Droettboom
Michael Goerz
Michael Seifert Michael Seifert
Michal Wajszczuk Michal Wajszczuk
Mihai Capotă Mihai Capotă

View File

@ -0,0 +1,2 @@
Passing arguments to pytest.fixture() as positional arguments is deprecated - pass them
as a keyword argument instead.

View File

@ -0,0 +1,3 @@
The ``scope`` parameter of ``@pytest.fixture`` can now be a callable that receives
the fixture name and the ``config`` object as keyword-only parameters.
See `the docs <https://docs.pytest.org/en/fixture.html#dynamic-scope>`__ for more information.

View File

@ -0,0 +1 @@
The HelpFormatter uses ``py.io.get_terminal_width`` for better width detection.

View File

@ -0,0 +1 @@
New behavior of the ``--pastebin`` option: failures to connect to the pastebin server are reported, without failing the pytest run

View File

@ -0,0 +1 @@
Fix "lexer" being used when uploading to bpaste.net from ``--pastebin`` to "text".

View File

@ -1,7 +1,7 @@
import pytest import pytest
@pytest.fixture("session") @pytest.fixture(scope="session")
def setup(request): def setup(request):
setup = CostlySetup() setup = CostlySetup()
yield setup yield setup

View File

@ -301,6 +301,32 @@ are finalized when the last test of a *package* finishes.
Use this new feature sparingly and please make sure to report any issues you find. Use this new feature sparingly and please make sure to report any issues you find.
Dynamic scope
^^^^^^^^^^^^^
In some cases, you might want to change the scope of the fixture without changing the code.
To do that, pass a callable to ``scope``. The callable must return a string with a valid scope
and will be executed only once - during the fixture definition. It will be called with two
keyword arguments - ``fixture_name`` as a string and ``config`` with a configuration object.
This can be especially useful when dealing with fixtures that need time for setup, like spawning
a docker container. You can use the command-line argument to control the scope of the spawned
containers for different environments. See the example below.
.. code-block:: python
def determine_scope(fixture_name, config):
if config.getoption("--keep-containers"):
return "session"
return "function"
@pytest.fixture(scope=determine_scope)
def docker_container():
yield spawn_container()
Order: Higher-scoped fixtures are instantiated first Order: Higher-scoped fixtures are instantiated first
---------------------------------------------------- ----------------------------------------------------

View File

@ -5,10 +5,15 @@ import traceback
from inspect import CO_VARARGS from inspect import CO_VARARGS
from inspect import CO_VARKEYWORDS from inspect import CO_VARKEYWORDS
from traceback import format_exception_only from traceback import format_exception_only
from types import CodeType
from types import TracebackType from types import TracebackType
from typing import Any
from typing import Dict
from typing import Generic from typing import Generic
from typing import List
from typing import Optional from typing import Optional
from typing import Pattern from typing import Pattern
from typing import Set
from typing import Tuple from typing import Tuple
from typing import TypeVar from typing import TypeVar
from typing import Union from typing import Union
@ -29,7 +34,7 @@ if False: # TYPE_CHECKING
class Code: class Code:
""" wrapper around Python code objects """ """ wrapper around Python code objects """
def __init__(self, rawcode): def __init__(self, rawcode) -> None:
if not hasattr(rawcode, "co_filename"): if not hasattr(rawcode, "co_filename"):
rawcode = getrawcode(rawcode) rawcode = getrawcode(rawcode)
try: try:
@ -38,7 +43,7 @@ class Code:
self.name = rawcode.co_name self.name = rawcode.co_name
except AttributeError: except AttributeError:
raise TypeError("not a code object: {!r}".format(rawcode)) raise TypeError("not a code object: {!r}".format(rawcode))
self.raw = rawcode self.raw = rawcode # type: CodeType
def __eq__(self, other): def __eq__(self, other):
return self.raw == other.raw return self.raw == other.raw
@ -351,7 +356,7 @@ class Traceback(list):
""" return the index of the frame/TracebackEntry where recursion """ return the index of the frame/TracebackEntry where recursion
originates if appropriate, None if no recursion occurred originates if appropriate, None if no recursion occurred
""" """
cache = {} cache = {} # type: Dict[Tuple[Any, int, int], List[Dict[str, Any]]]
for i, entry in enumerate(self): for i, entry in enumerate(self):
# id for the code.raw is needed to work around # id for the code.raw is needed to work around
# the strange metaprogramming in the decorator lib from pypi # the strange metaprogramming in the decorator lib from pypi
@ -650,7 +655,7 @@ class FormattedExcinfo:
args.append((argname, saferepr(argvalue))) args.append((argname, saferepr(argvalue)))
return ReprFuncArgs(args) return ReprFuncArgs(args)
def get_source(self, source, line_index=-1, excinfo=None, short=False): def get_source(self, source, line_index=-1, excinfo=None, short=False) -> List[str]:
""" return formatted and marked up source lines. """ """ return formatted and marked up source lines. """
import _pytest._code import _pytest._code
@ -722,7 +727,7 @@ class FormattedExcinfo:
else: else:
line_index = entry.lineno - entry.getfirstlinesource() line_index = entry.lineno - entry.getfirstlinesource()
lines = [] lines = [] # type: List[str]
style = entry._repr_style style = entry._repr_style
if style is None: if style is None:
style = self.style style = self.style
@ -799,7 +804,7 @@ class FormattedExcinfo:
exc_msg=str(e), exc_msg=str(e),
max_frames=max_frames, max_frames=max_frames,
total=len(traceback), total=len(traceback),
) ) # type: Optional[str]
traceback = traceback[:max_frames] + traceback[-max_frames:] traceback = traceback[:max_frames] + traceback[-max_frames:]
else: else:
if recursionindex is not None: if recursionindex is not None:
@ -812,10 +817,12 @@ class FormattedExcinfo:
def repr_excinfo(self, excinfo): def repr_excinfo(self, excinfo):
repr_chain = [] repr_chain = (
[]
) # type: List[Tuple[ReprTraceback, Optional[ReprFileLocation], Optional[str]]]
e = excinfo.value e = excinfo.value
descr = None descr = None
seen = set() seen = set() # type: Set[int]
while e is not None and id(e) not in seen: while e is not None and id(e) not in seen:
seen.add(id(e)) seen.add(id(e))
if excinfo: if excinfo:
@ -868,8 +875,8 @@ class TerminalRepr:
class ExceptionRepr(TerminalRepr): class ExceptionRepr(TerminalRepr):
def __init__(self): def __init__(self) -> None:
self.sections = [] self.sections = [] # type: List[Tuple[str, str, str]]
def addsection(self, name, content, sep="-"): def addsection(self, name, content, sep="-"):
self.sections.append((name, content, sep)) self.sections.append((name, content, sep))

View File

@ -7,6 +7,7 @@ import tokenize
import warnings import warnings
from ast import PyCF_ONLY_AST as _AST_FLAG from ast import PyCF_ONLY_AST as _AST_FLAG
from bisect import bisect_right from bisect import bisect_right
from typing import List
import py import py
@ -19,11 +20,11 @@ class Source:
_compilecounter = 0 _compilecounter = 0
def __init__(self, *parts, **kwargs): def __init__(self, *parts, **kwargs):
self.lines = lines = [] self.lines = lines = [] # type: List[str]
de = kwargs.get("deindent", True) de = kwargs.get("deindent", True)
for part in parts: for part in parts:
if not part: if not part:
partlines = [] partlines = [] # type: List[str]
elif isinstance(part, Source): elif isinstance(part, Source):
partlines = part.lines partlines = part.lines
elif isinstance(part, (tuple, list)): elif isinstance(part, (tuple, list)):
@ -157,8 +158,7 @@ class Source:
source = "\n".join(self.lines) + "\n" source = "\n".join(self.lines) + "\n"
try: try:
co = compile(source, filename, mode, flag) co = compile(source, filename, mode, flag)
except SyntaxError: except SyntaxError as ex:
ex = sys.exc_info()[1]
# re-represent syntax errors from parsing python strings # re-represent syntax errors from parsing python strings
msglines = self.lines[: ex.lineno] msglines = self.lines[: ex.lineno]
if ex.offset: if ex.offset:
@ -173,7 +173,8 @@ class Source:
if flag & _AST_FLAG: if flag & _AST_FLAG:
return co return co
lines = [(x + "\n") for x in self.lines] lines = [(x + "\n") for x in self.lines]
linecache.cache[filename] = (1, None, lines, filename) # Type ignored because linecache.cache is private.
linecache.cache[filename] = (1, None, lines, filename) # type: ignore
return co return co
@ -282,7 +283,7 @@ def get_statement_startend2(lineno, node):
return start, end return start, end
def getstatementrange_ast(lineno, source, assertion=False, astnode=None): def getstatementrange_ast(lineno, source: Source, assertion=False, astnode=None):
if astnode is None: if astnode is None:
content = str(source) content = str(source)
# See #4260: # See #4260:

View File

@ -2,6 +2,7 @@
support for presenting detailed information in failing assertions. support for presenting detailed information in failing assertions.
""" """
import sys import sys
from typing import Optional
from _pytest.assertion import rewrite from _pytest.assertion import rewrite
from _pytest.assertion import truncate from _pytest.assertion import truncate
@ -52,7 +53,9 @@ def register_assert_rewrite(*names):
importhook = hook importhook = hook
break break
else: else:
importhook = DummyRewriteHook() # TODO(typing): Add a protocol for mark_rewrite() and use it
# for importhook and for PytestPluginManager.rewrite_hook.
importhook = DummyRewriteHook() # type: ignore
importhook.mark_rewrite(*names) importhook.mark_rewrite(*names)
@ -69,7 +72,7 @@ class AssertionState:
def __init__(self, config, mode): def __init__(self, config, mode):
self.mode = mode self.mode = mode
self.trace = config.trace.root.get("assertion") self.trace = config.trace.root.get("assertion")
self.hook = None self.hook = None # type: Optional[rewrite.AssertionRewritingHook]
def install_importhook(config): def install_importhook(config):
@ -108,6 +111,7 @@ def pytest_runtest_setup(item):
""" """
def callbinrepr(op, left, right): def callbinrepr(op, left, right):
# type: (str, object, object) -> Optional[str]
"""Call the pytest_assertrepr_compare hook and prepare the result """Call the pytest_assertrepr_compare hook and prepare the result
This uses the first result from the hook and then ensures the This uses the first result from the hook and then ensures the
@ -133,12 +137,13 @@ def pytest_runtest_setup(item):
if item.config.getvalue("assertmode") == "rewrite": if item.config.getvalue("assertmode") == "rewrite":
res = res.replace("%", "%%") res = res.replace("%", "%%")
return res return res
return None
util._reprcompare = callbinrepr util._reprcompare = callbinrepr
if item.ihook.pytest_assertion_pass.get_hookimpls(): if item.ihook.pytest_assertion_pass.get_hookimpls():
def call_assertion_pass_hook(lineno, expl, orig): def call_assertion_pass_hook(lineno, orig, expl):
item.ihook.pytest_assertion_pass( item.ihook.pytest_assertion_pass(
item=item, lineno=lineno, orig=orig, expl=expl item=item, lineno=lineno, orig=orig, expl=expl
) )

View File

@ -2,6 +2,7 @@
import ast import ast
import errno import errno
import functools import functools
import importlib.abc
import importlib.machinery import importlib.machinery
import importlib.util import importlib.util
import io import io
@ -16,6 +17,7 @@ from typing import Dict
from typing import List from typing import List
from typing import Optional from typing import Optional
from typing import Set from typing import Set
from typing import Tuple
import atomicwrites import atomicwrites
@ -34,7 +36,7 @@ PYC_EXT = ".py" + (__debug__ and "c" or "o")
PYC_TAIL = "." + PYTEST_TAG + PYC_EXT PYC_TAIL = "." + PYTEST_TAG + PYC_EXT
class AssertionRewritingHook: class AssertionRewritingHook(importlib.abc.MetaPathFinder):
"""PEP302/PEP451 import hook which rewrites asserts.""" """PEP302/PEP451 import hook which rewrites asserts."""
def __init__(self, config): def __init__(self, config):
@ -44,13 +46,13 @@ class AssertionRewritingHook:
except ValueError: except ValueError:
self.fnpats = ["test_*.py", "*_test.py"] self.fnpats = ["test_*.py", "*_test.py"]
self.session = None self.session = None
self._rewritten_names = set() self._rewritten_names = set() # type: Set[str]
self._must_rewrite = set() self._must_rewrite = set() # type: Set[str]
# flag to guard against trying to rewrite a pyc file while we are already writing another pyc file, # flag to guard against trying to rewrite a pyc file while we are already writing another pyc file,
# which might result in infinite recursion (#3506) # which might result in infinite recursion (#3506)
self._writing_pyc = False self._writing_pyc = False
self._basenames_to_check_rewrite = {"conftest"} self._basenames_to_check_rewrite = {"conftest"}
self._marked_for_rewrite_cache = {} self._marked_for_rewrite_cache = {} # type: Dict[str, bool]
self._session_paths_checked = False self._session_paths_checked = False
def set_session(self, session): def set_session(self, session):
@ -199,7 +201,7 @@ class AssertionRewritingHook:
return self._is_marked_for_rewrite(name, state) return self._is_marked_for_rewrite(name, state)
def _is_marked_for_rewrite(self, name, state): def _is_marked_for_rewrite(self, name: str, state):
try: try:
return self._marked_for_rewrite_cache[name] return self._marked_for_rewrite_cache[name]
except KeyError: except KeyError:
@ -214,7 +216,7 @@ class AssertionRewritingHook:
self._marked_for_rewrite_cache[name] = False self._marked_for_rewrite_cache[name] = False
return False return False
def mark_rewrite(self, *names): def mark_rewrite(self, *names: str) -> None:
"""Mark import names as needing to be rewritten. """Mark import names as needing to be rewritten.
The named module or package as well as any nested modules will The named module or package as well as any nested modules will
@ -381,6 +383,7 @@ def _format_boolop(explanations, is_or):
def _call_reprcompare(ops, results, expls, each_obj): def _call_reprcompare(ops, results, expls, each_obj):
# type: (Tuple[str, ...], Tuple[bool, ...], Tuple[str, ...], Tuple[object, ...]) -> str
for i, res, expl in zip(range(len(ops)), results, expls): for i, res, expl in zip(range(len(ops)), results, expls):
try: try:
done = not res done = not res
@ -396,11 +399,13 @@ def _call_reprcompare(ops, results, expls, each_obj):
def _call_assertion_pass(lineno, orig, expl): def _call_assertion_pass(lineno, orig, expl):
# type: (int, str, str) -> None
if util._assertion_pass is not None: if util._assertion_pass is not None:
util._assertion_pass(lineno=lineno, orig=orig, expl=expl) util._assertion_pass(lineno, orig, expl)
def _check_if_assertion_pass_impl(): def _check_if_assertion_pass_impl():
# type: () -> bool
"""Checks if any plugins implement the pytest_assertion_pass hook """Checks if any plugins implement the pytest_assertion_pass hook
in order not to generate explanation unecessarily (might be expensive)""" in order not to generate explanation unecessarily (might be expensive)"""
return True if util._assertion_pass else False return True if util._assertion_pass else False
@ -574,7 +579,7 @@ class AssertionRewriter(ast.NodeVisitor):
def _assert_expr_to_lineno(self): def _assert_expr_to_lineno(self):
return _get_assertion_exprs(self.source) return _get_assertion_exprs(self.source)
def run(self, mod): def run(self, mod: ast.Module) -> None:
"""Find all assert statements in *mod* and rewrite them.""" """Find all assert statements in *mod* and rewrite them."""
if not mod.body: if not mod.body:
# Nothing to do. # Nothing to do.
@ -616,12 +621,12 @@ class AssertionRewriter(ast.NodeVisitor):
] ]
mod.body[pos:pos] = imports mod.body[pos:pos] = imports
# Collect asserts. # Collect asserts.
nodes = [mod] nodes = [mod] # type: List[ast.AST]
while nodes: while nodes:
node = nodes.pop() node = nodes.pop()
for name, field in ast.iter_fields(node): for name, field in ast.iter_fields(node):
if isinstance(field, list): if isinstance(field, list):
new = [] new = [] # type: List
for i, child in enumerate(field): for i, child in enumerate(field):
if isinstance(child, ast.Assert): if isinstance(child, ast.Assert):
# Transform assert. # Transform assert.
@ -695,7 +700,7 @@ class AssertionRewriter(ast.NodeVisitor):
.explanation_param(). .explanation_param().
""" """
self.explanation_specifiers = {} self.explanation_specifiers = {} # type: Dict[str, ast.expr]
self.stack.append(self.explanation_specifiers) self.stack.append(self.explanation_specifiers)
def pop_format_context(self, expl_expr): def pop_format_context(self, expl_expr):
@ -738,7 +743,8 @@ class AssertionRewriter(ast.NodeVisitor):
from _pytest.warning_types import PytestAssertRewriteWarning from _pytest.warning_types import PytestAssertRewriteWarning
import warnings import warnings
warnings.warn_explicit( # Ignore type: typeshed bug https://github.com/python/typeshed/pull/3121
warnings.warn_explicit( # type: ignore
PytestAssertRewriteWarning( PytestAssertRewriteWarning(
"assertion is always true, perhaps remove parentheses?" "assertion is always true, perhaps remove parentheses?"
), ),
@ -747,15 +753,15 @@ class AssertionRewriter(ast.NodeVisitor):
lineno=assert_.lineno, lineno=assert_.lineno,
) )
self.statements = [] self.statements = [] # type: List[ast.stmt]
self.variables = [] self.variables = [] # type: List[str]
self.variable_counter = itertools.count() self.variable_counter = itertools.count()
if self.enable_assertion_pass_hook: if self.enable_assertion_pass_hook:
self.format_variables = [] self.format_variables = [] # type: List[str]
self.stack = [] self.stack = [] # type: List[Dict[str, ast.expr]]
self.expl_stmts = [] self.expl_stmts = [] # type: List[ast.stmt]
self.push_format_context() self.push_format_context()
# Rewrite assert into a bunch of statements. # Rewrite assert into a bunch of statements.
top_condition, explanation = self.visit(assert_.test) top_condition, explanation = self.visit(assert_.test)
@ -893,7 +899,7 @@ warn_explicit(
# Process each operand, short-circuiting if needed. # Process each operand, short-circuiting if needed.
for i, v in enumerate(boolop.values): for i, v in enumerate(boolop.values):
if i: if i:
fail_inner = [] fail_inner = [] # type: List[ast.stmt]
# cond is set in a prior loop iteration below # cond is set in a prior loop iteration below
self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa
self.expl_stmts = fail_inner self.expl_stmts = fail_inner
@ -904,10 +910,10 @@ warn_explicit(
call = ast.Call(app, [expl_format], []) call = ast.Call(app, [expl_format], [])
self.expl_stmts.append(ast.Expr(call)) self.expl_stmts.append(ast.Expr(call))
if i < levels: if i < levels:
cond = res cond = res # type: ast.expr
if is_or: if is_or:
cond = ast.UnaryOp(ast.Not(), cond) cond = ast.UnaryOp(ast.Not(), cond)
inner = [] inner = [] # type: List[ast.stmt]
self.statements.append(ast.If(cond, inner, [])) self.statements.append(ast.If(cond, inner, []))
self.statements = body = inner self.statements = body = inner
self.statements = save self.statements = save
@ -973,7 +979,7 @@ warn_explicit(
expl = pat % (res_expl, res_expl, value_expl, attr.attr) expl = pat % (res_expl, res_expl, value_expl, attr.attr)
return res, expl return res, expl
def visit_Compare(self, comp): def visit_Compare(self, comp: ast.Compare):
self.push_format_context() self.push_format_context()
left_res, left_expl = self.visit(comp.left) left_res, left_expl = self.visit(comp.left)
if isinstance(comp.left, (ast.Compare, ast.BoolOp)): if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
@ -1006,7 +1012,7 @@ warn_explicit(
ast.Tuple(results, ast.Load()), ast.Tuple(results, ast.Load()),
) )
if len(comp.ops) > 1: if len(comp.ops) > 1:
res = ast.BoolOp(ast.And(), load_names) res = ast.BoolOp(ast.And(), load_names) # type: ast.expr
else: else:
res = load_names[0] res = load_names[0]
return res, self.explanation_param(self.pop_format_context(expl_call)) return res, self.explanation_param(self.pop_format_context(expl_call))

View File

@ -1,6 +1,9 @@
"""Utilities for assertion debugging""" """Utilities for assertion debugging"""
import pprint import pprint
from collections.abc import Sequence from collections.abc import Sequence
from typing import Callable
from typing import List
from typing import Optional
import _pytest._code import _pytest._code
from _pytest import outcomes from _pytest import outcomes
@ -10,11 +13,11 @@ from _pytest._io.saferepr import saferepr
# interpretation code and assertion rewriter to detect this plugin was # interpretation code and assertion rewriter to detect this plugin was
# loaded and in turn call the hooks defined here as part of the # loaded and in turn call the hooks defined here as part of the
# DebugInterpreter. # DebugInterpreter.
_reprcompare = None _reprcompare = None # type: Optional[Callable[[str, object, object], Optional[str]]]
# Works similarly as _reprcompare attribute. Is populated with the hook call # Works similarly as _reprcompare attribute. Is populated with the hook call
# when pytest_runtest_setup is called. # when pytest_runtest_setup is called.
_assertion_pass = None _assertion_pass = None # type: Optional[Callable[[int, str, str], None]]
def format_explanation(explanation): def format_explanation(explanation):
@ -177,7 +180,7 @@ def _diff_text(left, right, verbose=0):
""" """
from difflib import ndiff from difflib import ndiff
explanation = [] explanation = [] # type: List[str]
def escape_for_readable_diff(binary_text): def escape_for_readable_diff(binary_text):
""" """
@ -235,7 +238,7 @@ def _compare_eq_verbose(left, right):
left_lines = repr(left).splitlines(keepends) left_lines = repr(left).splitlines(keepends)
right_lines = repr(right).splitlines(keepends) right_lines = repr(right).splitlines(keepends)
explanation = [] explanation = [] # type: List[str]
explanation += ["-" + line for line in left_lines] explanation += ["-" + line for line in left_lines]
explanation += ["+" + line for line in right_lines] explanation += ["+" + line for line in right_lines]
@ -259,7 +262,7 @@ def _compare_eq_iterable(left, right, verbose=0):
def _compare_eq_sequence(left, right, verbose=0): def _compare_eq_sequence(left, right, verbose=0):
comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes) comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes)
explanation = [] explanation = [] # type: List[str]
len_left = len(left) len_left = len(left)
len_right = len(right) len_right = len(right)
for i in range(min(len_left, len_right)): for i in range(min(len_left, len_right)):
@ -327,7 +330,7 @@ def _compare_eq_set(left, right, verbose=0):
def _compare_eq_dict(left, right, verbose=0): def _compare_eq_dict(left, right, verbose=0):
explanation = [] explanation = [] # type: List[str]
set_left = set(left) set_left = set(left)
set_right = set(right) set_right = set(right)
common = set_left.intersection(set_right) common = set_left.intersection(set_right)

View File

@ -9,6 +9,15 @@ import types
import warnings import warnings
from functools import lru_cache from functools import lru_cache
from pathlib import Path from pathlib import Path
from types import TracebackType
from typing import Any
from typing import Callable
from typing import Dict
from typing import List
from typing import Optional
from typing import Sequence
from typing import Set
from typing import Tuple
import attr import attr
import py import py
@ -32,6 +41,10 @@ from _pytest.outcomes import fail
from _pytest.outcomes import Skipped from _pytest.outcomes import Skipped
from _pytest.warning_types import PytestConfigWarning from _pytest.warning_types import PytestConfigWarning
if False: # TYPE_CHECKING
from typing import Type
hookimpl = HookimplMarker("pytest") hookimpl = HookimplMarker("pytest")
hookspec = HookspecMarker("pytest") hookspec = HookspecMarker("pytest")
@ -40,7 +53,7 @@ class ConftestImportFailure(Exception):
def __init__(self, path, excinfo): def __init__(self, path, excinfo):
Exception.__init__(self, path, excinfo) Exception.__init__(self, path, excinfo)
self.path = path self.path = path
self.excinfo = excinfo self.excinfo = excinfo # type: Tuple[Type[Exception], Exception, TracebackType]
def main(args=None, plugins=None): def main(args=None, plugins=None):
@ -237,14 +250,18 @@ class PytestPluginManager(PluginManager):
def __init__(self): def __init__(self):
super().__init__("pytest") super().__init__("pytest")
self._conftest_plugins = set() # The objects are module objects, only used generically.
self._conftest_plugins = set() # type: Set[object]
# state related to local conftest plugins # state related to local conftest plugins
self._dirpath2confmods = {} # Maps a py.path.local to a list of module objects.
self._conftestpath2mod = {} self._dirpath2confmods = {} # type: Dict[Any, List[object]]
# Maps a py.path.local to a module object.
self._conftestpath2mod = {} # type: Dict[Any, object]
self._confcutdir = None self._confcutdir = None
self._noconftest = False self._noconftest = False
self._duplicatepaths = set() # Set of py.path.local's.
self._duplicatepaths = set() # type: Set[Any]
self.add_hookspecs(_pytest.hookspec) self.add_hookspecs(_pytest.hookspec)
self.register(self) self.register(self)
@ -656,7 +673,7 @@ class Config:
args = attr.ib() args = attr.ib()
plugins = attr.ib() plugins = attr.ib()
dir = attr.ib() dir = attr.ib(type=Path)
def __init__(self, pluginmanager, *, invocation_params=None): def __init__(self, pluginmanager, *, invocation_params=None):
from .argparsing import Parser, FILE_OR_DIR from .argparsing import Parser, FILE_OR_DIR
@ -677,10 +694,10 @@ class Config:
self.pluginmanager = pluginmanager self.pluginmanager = pluginmanager
self.trace = self.pluginmanager.trace.root.get("config") self.trace = self.pluginmanager.trace.root.get("config")
self.hook = self.pluginmanager.hook self.hook = self.pluginmanager.hook
self._inicache = {} self._inicache = {} # type: Dict[str, Any]
self._override_ini = () self._override_ini = () # type: Sequence[str]
self._opt2dest = {} self._opt2dest = {} # type: Dict[str, str]
self._cleanup = [] self._cleanup = [] # type: List[Callable[[], None]]
self.pluginmanager.register(self, "pytestconfig") self.pluginmanager.register(self, "pytestconfig")
self._configured = False self._configured = False
self.hook.pytest_addoption.call_historic(kwargs=dict(parser=self._parser)) self.hook.pytest_addoption.call_historic(kwargs=dict(parser=self._parser))
@ -781,7 +798,7 @@ class Config:
def pytest_load_initial_conftests(self, early_config): def pytest_load_initial_conftests(self, early_config):
self.pluginmanager._set_initial_conftests(early_config.known_args_namespace) self.pluginmanager._set_initial_conftests(early_config.known_args_namespace)
def _initini(self, args): def _initini(self, args) -> None:
ns, unknown_args = self._parser.parse_known_and_unknown_args( ns, unknown_args = self._parser.parse_known_and_unknown_args(
args, namespace=copy.copy(self.option) args, namespace=copy.copy(self.option)
) )
@ -882,8 +899,7 @@ class Config:
self.hook.pytest_load_initial_conftests( self.hook.pytest_load_initial_conftests(
early_config=self, args=args, parser=self._parser early_config=self, args=args, parser=self._parser
) )
except ConftestImportFailure: except ConftestImportFailure as e:
e = sys.exc_info()[1]
if ns.help or ns.version: if ns.help or ns.version:
# we don't want to prevent --help/--version to work # we don't want to prevent --help/--version to work
# so just let is pass and print a warning at the end # so just let is pass and print a warning at the end
@ -949,7 +965,7 @@ class Config:
assert isinstance(x, list) assert isinstance(x, list)
x.append(line) # modifies the cached list inline x.append(line) # modifies the cached list inline
def getini(self, name): def getini(self, name: str):
""" return configuration value from an :ref:`ini file <inifiles>`. If the """ return configuration value from an :ref:`ini file <inifiles>`. If the
specified name hasn't been registered through a prior specified name hasn't been registered through a prior
:py:func:`parser.addini <_pytest.config.Parser.addini>` :py:func:`parser.addini <_pytest.config.Parser.addini>`
@ -960,7 +976,7 @@ class Config:
self._inicache[name] = val = self._getini(name) self._inicache[name] = val = self._getini(name)
return val return val
def _getini(self, name): def _getini(self, name: str) -> Any:
try: try:
description, type, default = self._parser._inidict[name] description, type, default = self._parser._inidict[name]
except KeyError: except KeyError:
@ -1005,7 +1021,7 @@ class Config:
values.append(relroot) values.append(relroot)
return values return values
def _get_override_ini_value(self, name): def _get_override_ini_value(self, name: str) -> Optional[str]:
value = None value = None
# override_ini is a list of "ini=value" options # override_ini is a list of "ini=value" options
# always use the last item if multiple values are set for same ini-name, # always use the last item if multiple values are set for same ini-name,
@ -1020,7 +1036,7 @@ class Config:
value = user_ini_value value = user_ini_value
return value return value
def getoption(self, name, default=notset, skip=False): def getoption(self, name: str, default=notset, skip: bool = False):
""" return command line option value. """ return command line option value.
:arg name: name of the option. You may also specify :arg name: name of the option. You may also specify

View File

@ -2,6 +2,11 @@ import argparse
import sys import sys
import warnings import warnings
from gettext import gettext from gettext import gettext
from typing import Any
from typing import Dict
from typing import List
from typing import Optional
from typing import Tuple
import py import py
@ -21,12 +26,12 @@ class Parser:
def __init__(self, usage=None, processopt=None): def __init__(self, usage=None, processopt=None):
self._anonymous = OptionGroup("custom options", parser=self) self._anonymous = OptionGroup("custom options", parser=self)
self._groups = [] self._groups = [] # type: List[OptionGroup]
self._processopt = processopt self._processopt = processopt
self._usage = usage self._usage = usage
self._inidict = {} self._inidict = {} # type: Dict[str, Tuple[str, Optional[str], Any]]
self._ininames = [] self._ininames = [] # type: List[str]
self.extra_info = {} self.extra_info = {} # type: Dict[str, Any]
def processoption(self, option): def processoption(self, option):
if self._processopt: if self._processopt:
@ -80,7 +85,7 @@ class Parser:
args = [str(x) if isinstance(x, py.path.local) else x for x in args] args = [str(x) if isinstance(x, py.path.local) else x for x in args]
return self.optparser.parse_args(args, namespace=namespace) return self.optparser.parse_args(args, namespace=namespace)
def _getparser(self): def _getparser(self) -> "MyOptionParser":
from _pytest._argcomplete import filescompleter from _pytest._argcomplete import filescompleter
optparser = MyOptionParser(self, self.extra_info, prog=self.prog) optparser = MyOptionParser(self, self.extra_info, prog=self.prog)
@ -94,7 +99,10 @@ class Parser:
a = option.attrs() a = option.attrs()
arggroup.add_argument(*n, **a) arggroup.add_argument(*n, **a)
# bash like autocompletion for dirs (appending '/') # bash like autocompletion for dirs (appending '/')
optparser.add_argument(FILE_OR_DIR, nargs="*").completer = filescompleter # Type ignored because typeshed doesn't know about argcomplete.
optparser.add_argument( # type: ignore
FILE_OR_DIR, nargs="*"
).completer = filescompleter
return optparser return optparser
def parse_setoption(self, args, option, namespace=None): def parse_setoption(self, args, option, namespace=None):
@ -103,13 +111,15 @@ class Parser:
setattr(option, name, value) setattr(option, name, value)
return getattr(parsedoption, FILE_OR_DIR) return getattr(parsedoption, FILE_OR_DIR)
def parse_known_args(self, args, namespace=None): def parse_known_args(self, args, namespace=None) -> argparse.Namespace:
"""parses and returns a namespace object with known arguments at this """parses and returns a namespace object with known arguments at this
point. point.
""" """
return self.parse_known_and_unknown_args(args, namespace=namespace)[0] return self.parse_known_and_unknown_args(args, namespace=namespace)[0]
def parse_known_and_unknown_args(self, args, namespace=None): def parse_known_and_unknown_args(
self, args, namespace=None
) -> Tuple[argparse.Namespace, List[str]]:
"""parses and returns a namespace object with known arguments, and """parses and returns a namespace object with known arguments, and
the remaining arguments unknown at this point. the remaining arguments unknown at this point.
""" """
@ -163,8 +173,8 @@ class Argument:
def __init__(self, *names, **attrs): def __init__(self, *names, **attrs):
"""store parms in private vars for use in add_argument""" """store parms in private vars for use in add_argument"""
self._attrs = attrs self._attrs = attrs
self._short_opts = [] self._short_opts = [] # type: List[str]
self._long_opts = [] self._long_opts = [] # type: List[str]
self.dest = attrs.get("dest") self.dest = attrs.get("dest")
if "%default" in (attrs.get("help") or ""): if "%default" in (attrs.get("help") or ""):
warnings.warn( warnings.warn(
@ -268,8 +278,8 @@ class Argument:
) )
self._long_opts.append(opt) self._long_opts.append(opt)
def __repr__(self): def __repr__(self) -> str:
args = [] args = [] # type: List[str]
if self._short_opts: if self._short_opts:
args += ["_short_opts: " + repr(self._short_opts)] args += ["_short_opts: " + repr(self._short_opts)]
if self._long_opts: if self._long_opts:
@ -286,7 +296,7 @@ class OptionGroup:
def __init__(self, name, description="", parser=None): def __init__(self, name, description="", parser=None):
self.name = name self.name = name
self.description = description self.description = description
self.options = [] self.options = [] # type: List[Argument]
self.parser = parser self.parser = parser
def addoption(self, *optnames, **attrs): def addoption(self, *optnames, **attrs):
@ -405,6 +415,12 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter):
- cache result on action object as this is called at least 2 times - cache result on action object as this is called at least 2 times
""" """
def __init__(self, *args, **kwargs):
"""Use more accurate terminal width via pylib."""
if "width" not in kwargs:
kwargs["width"] = py.io.get_terminal_width()
super().__init__(*args, **kwargs)
def _format_action_invocation(self, action): def _format_action_invocation(self, action):
orgstr = argparse.HelpFormatter._format_action_invocation(self, action) orgstr = argparse.HelpFormatter._format_action_invocation(self, action)
if orgstr and orgstr[0] != "-": # only optional arguments if orgstr and orgstr[0] != "-": # only optional arguments
@ -421,7 +437,7 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter):
option_map = getattr(action, "map_long_option", {}) option_map = getattr(action, "map_long_option", {})
if option_map is None: if option_map is None:
option_map = {} option_map = {}
short_long = {} short_long = {} # type: Dict[str, str]
for option in options: for option in options:
if len(option) == 2 or option[2] == " ": if len(option) == 2 or option[2] == " ":
continue continue

View File

@ -1,10 +1,15 @@
import os import os
from typing import List
from typing import Optional
import py import py
from .exceptions import UsageError from .exceptions import UsageError
from _pytest.outcomes import fail from _pytest.outcomes import fail
if False:
from . import Config # noqa: F401
def exists(path, ignore=EnvironmentError): def exists(path, ignore=EnvironmentError):
try: try:
@ -102,7 +107,12 @@ def get_dirs_from_args(args):
CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead." CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead."
def determine_setup(inifile, args, rootdir_cmd_arg=None, config=None): def determine_setup(
inifile: str,
args: List[str],
rootdir_cmd_arg: Optional[str] = None,
config: Optional["Config"] = None,
):
dirs = get_dirs_from_args(args) dirs = get_dirs_from_args(args)
if inifile: if inifile:
iniconfig = py.iniconfig.IniConfig(inifile) iniconfig = py.iniconfig.IniConfig(inifile)

View File

@ -29,3 +29,8 @@ RESULT_LOG = PytestDeprecationWarning(
"--result-log is deprecated and scheduled for removal in pytest 6.0.\n" "--result-log is deprecated and scheduled for removal in pytest 6.0.\n"
"See https://docs.pytest.org/en/latest/deprecations.html#result-log-result-log for more information." "See https://docs.pytest.org/en/latest/deprecations.html#result-log-result-log for more information."
) )
FIXTURE_POSITIONAL_ARGUMENTS = PytestDeprecationWarning(
"Passing arguments to pytest.fixture() as positional arguments is deprecated - pass them "
"as a keyword argument instead."
)

View File

@ -2,6 +2,7 @@ import functools
import inspect import inspect
import itertools import itertools
import sys import sys
import warnings
from collections import defaultdict from collections import defaultdict
from collections import deque from collections import deque
from collections import OrderedDict from collections import OrderedDict
@ -27,6 +28,7 @@ from _pytest.compat import getlocation
from _pytest.compat import is_generator from _pytest.compat import is_generator
from _pytest.compat import NOTSET from _pytest.compat import NOTSET
from _pytest.compat import safe_getattr from _pytest.compat import safe_getattr
from _pytest.deprecated import FIXTURE_POSITIONAL_ARGUMENTS
from _pytest.outcomes import fail from _pytest.outcomes import fail
from _pytest.outcomes import TEST_OUTCOME from _pytest.outcomes import TEST_OUTCOME
@ -58,7 +60,6 @@ def pytest_sessionstart(session):
scopename2class = {} # type: Dict[str, Type[nodes.Node]] scopename2class = {} # type: Dict[str, Type[nodes.Node]]
scope2props = dict(session=()) # type: Dict[str, Tuple[str, ...]] scope2props = dict(session=()) # type: Dict[str, Tuple[str, ...]]
scope2props["package"] = ("fspath",) scope2props["package"] = ("fspath",)
scope2props["module"] = ("fspath", "module") scope2props["module"] = ("fspath", "module")
@ -792,6 +793,25 @@ def _teardown_yield_fixture(fixturefunc, it):
) )
def _eval_scope_callable(scope_callable, fixture_name, config):
try:
result = scope_callable(fixture_name=fixture_name, config=config)
except Exception:
raise TypeError(
"Error evaluating {} while defining fixture '{}'.\n"
"Expected a function with the signature (*, fixture_name, config)".format(
scope_callable, fixture_name
)
)
if not isinstance(result, str):
fail(
"Expected {} to return a 'str' while defining fixture '{}', but it returned:\n"
"{!r}".format(scope_callable, fixture_name, result),
pytrace=False,
)
return result
class FixtureDef: class FixtureDef:
""" A container for a factory definition. """ """ A container for a factory definition. """
@ -811,6 +831,8 @@ class FixtureDef:
self.has_location = baseid is not None self.has_location = baseid is not None
self.func = func self.func = func
self.argname = argname self.argname = argname
if callable(scope):
scope = _eval_scope_callable(scope, argname, fixturemanager.config)
self.scope = scope self.scope = scope
self.scopenum = scope2index( self.scopenum = scope2index(
scope or "function", scope or "function",
@ -995,7 +1017,57 @@ class FixtureFunctionMarker:
return function return function
def fixture(scope="function", params=None, autouse=False, ids=None, name=None): FIXTURE_ARGS_ORDER = ("scope", "params", "autouse", "ids", "name")
def _parse_fixture_args(callable_or_scope, *args, **kwargs):
arguments = {
"scope": "function",
"params": None,
"autouse": False,
"ids": None,
"name": None,
}
kwargs = {
key: value for key, value in kwargs.items() if arguments.get(key) != value
}
fixture_function = None
if isinstance(callable_or_scope, str):
args = list(args)
args.insert(0, callable_or_scope)
else:
fixture_function = callable_or_scope
positionals = set()
for positional, argument_name in zip(args, FIXTURE_ARGS_ORDER):
arguments[argument_name] = positional
positionals.add(argument_name)
duplicated_kwargs = {kwarg for kwarg in kwargs.keys() if kwarg in positionals}
if duplicated_kwargs:
raise TypeError(
"The fixture arguments are defined as positional and keyword: {}. "
"Use only keyword arguments.".format(", ".join(duplicated_kwargs))
)
if positionals:
warnings.warn(FIXTURE_POSITIONAL_ARGUMENTS, stacklevel=2)
arguments.update(kwargs)
return fixture_function, arguments
def fixture(
callable_or_scope=None,
*args,
scope="function",
params=None,
autouse=False,
ids=None,
name=None
):
"""Decorator to mark a fixture factory function. """Decorator to mark a fixture factory function.
This decorator can be used, with or without parameters, to define a This decorator can be used, with or without parameters, to define a
@ -1041,21 +1113,55 @@ def fixture(scope="function", params=None, autouse=False, ids=None, name=None):
``fixture_<fixturename>`` and then use ``fixture_<fixturename>`` and then use
``@pytest.fixture(name='<fixturename>')``. ``@pytest.fixture(name='<fixturename>')``.
""" """
if callable(scope) and params is None and autouse is False: fixture_function, arguments = _parse_fixture_args(
callable_or_scope,
*args,
scope=scope,
params=params,
autouse=autouse,
ids=ids,
name=name
)
scope = arguments.get("scope")
params = arguments.get("params")
autouse = arguments.get("autouse")
ids = arguments.get("ids")
name = arguments.get("name")
if fixture_function and params is None and autouse is False:
# direct decoration # direct decoration
return FixtureFunctionMarker("function", params, autouse, name=name)(scope) return FixtureFunctionMarker(scope, params, autouse, name=name)(
fixture_function
)
if params is not None and not isinstance(params, (list, tuple)): if params is not None and not isinstance(params, (list, tuple)):
params = list(params) params = list(params)
return FixtureFunctionMarker(scope, params, autouse, ids=ids, name=name) return FixtureFunctionMarker(scope, params, autouse, ids=ids, name=name)
def yield_fixture(scope="function", params=None, autouse=False, ids=None, name=None): def yield_fixture(
callable_or_scope=None,
*args,
scope="function",
params=None,
autouse=False,
ids=None,
name=None
):
""" (return a) decorator to mark a yield-fixture factory function. """ (return a) decorator to mark a yield-fixture factory function.
.. deprecated:: 3.0 .. deprecated:: 3.0
Use :py:func:`pytest.fixture` directly instead. Use :py:func:`pytest.fixture` directly instead.
""" """
return fixture(scope=scope, params=params, autouse=autouse, ids=ids, name=name) return fixture(
callable_or_scope,
*args,
scope=scope,
params=params,
autouse=autouse,
ids=ids,
name=name
)
defaultfuncargprefixmarker = fixture() defaultfuncargprefixmarker = fixture()

View File

@ -51,6 +51,8 @@ class MarkEvaluator:
except TEST_OUTCOME: except TEST_OUTCOME:
self.exc = sys.exc_info() self.exc = sys.exc_info()
if isinstance(self.exc[1], SyntaxError): if isinstance(self.exc[1], SyntaxError):
# TODO: Investigate why SyntaxError.offset is Optional, and if it can be None here.
assert self.exc[1].offset is not None
msg = [" " * (self.exc[1].offset + 4) + "^"] msg = [" " * (self.exc[1].offset + 4) + "^"]
msg.append("SyntaxError: invalid syntax") msg.append("SyntaxError: invalid syntax")
else: else:

View File

@ -292,7 +292,7 @@ class MarkGenerator:
_config = None _config = None
_markers = set() # type: Set[str] _markers = set() # type: Set[str]
def __getattr__(self, name): def __getattr__(self, name: str) -> MarkDecorator:
if name[0] == "_": if name[0] == "_":
raise AttributeError("Marker name must NOT start with underscore") raise AttributeError("Marker name must NOT start with underscore")

View File

@ -1,14 +1,26 @@
import os import os
import warnings import warnings
from functools import lru_cache from functools import lru_cache
from typing import Any
from typing import Dict
from typing import List
from typing import Set
from typing import Tuple
from typing import Union
import py import py
import _pytest._code import _pytest._code
from _pytest.compat import getfslineno from _pytest.compat import getfslineno
from _pytest.mark.structures import Mark
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
if False: # TYPE_CHECKING
# Imported here due to circular import.
from _pytest.fixtures import FixtureDef
SEP = "/" SEP = "/"
tracebackcutdir = py.path.local(_pytest.__file__).dirpath() tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
@ -78,13 +90,13 @@ class Node:
self.keywords = NodeKeywords(self) self.keywords = NodeKeywords(self)
#: the marker objects belonging to this node #: the marker objects belonging to this node
self.own_markers = [] self.own_markers = [] # type: List[Mark]
#: allow adding of extra keywords to use for matching #: allow adding of extra keywords to use for matching
self.extra_keyword_matches = set() self.extra_keyword_matches = set() # type: Set[str]
# used for storing artificial fixturedefs for direct parametrization # used for storing artificial fixturedefs for direct parametrization
self._name2pseudofixturedef = {} self._name2pseudofixturedef = {} # type: Dict[str, FixtureDef]
if nodeid is not None: if nodeid is not None:
assert "::()" not in nodeid assert "::()" not in nodeid
@ -127,7 +139,8 @@ class Node:
) )
) )
path, lineno = get_fslocation_from_item(self) path, lineno = get_fslocation_from_item(self)
warnings.warn_explicit( # Type ignored: https://github.com/python/typeshed/pull/3121
warnings.warn_explicit( # type: ignore
warning, warning,
category=None, category=None,
filename=str(path), filename=str(path),
@ -160,7 +173,9 @@ class Node:
chain.reverse() chain.reverse()
return chain return chain
def add_marker(self, marker, append=True): def add_marker(
self, marker: Union[str, MarkDecorator], append: bool = True
) -> None:
"""dynamically add a marker object to the node. """dynamically add a marker object to the node.
:type marker: ``str`` or ``pytest.mark.*`` object :type marker: ``str`` or ``pytest.mark.*`` object
@ -168,17 +183,19 @@ class Node:
``append=True`` whether to append the marker, ``append=True`` whether to append the marker,
if ``False`` insert at position ``0``. if ``False`` insert at position ``0``.
""" """
from _pytest.mark import MarkDecorator, MARK_GEN from _pytest.mark import MARK_GEN
if isinstance(marker, str): if isinstance(marker, MarkDecorator):
marker = getattr(MARK_GEN, marker) marker_ = marker
elif not isinstance(marker, MarkDecorator): elif isinstance(marker, str):
raise ValueError("is not a string or pytest.mark.* Marker") marker_ = getattr(MARK_GEN, marker)
self.keywords[marker.name] = marker
if append:
self.own_markers.append(marker.mark)
else: else:
self.own_markers.insert(0, marker.mark) raise ValueError("is not a string or pytest.mark.* Marker")
self.keywords[marker_.name] = marker
if append:
self.own_markers.append(marker_.mark)
else:
self.own_markers.insert(0, marker_.mark)
def iter_markers(self, name=None): def iter_markers(self, name=None):
""" """
@ -211,7 +228,7 @@ class Node:
def listextrakeywords(self): def listextrakeywords(self):
""" Return a set of all extra keywords in self and any parents.""" """ Return a set of all extra keywords in self and any parents."""
extra_keywords = set() extra_keywords = set() # type: Set[str]
for item in self.listchain(): for item in self.listchain():
extra_keywords.update(item.extra_keyword_matches) extra_keywords.update(item.extra_keyword_matches)
return extra_keywords return extra_keywords
@ -239,7 +256,8 @@ class Node:
pass pass
def _repr_failure_py(self, excinfo, style=None): def _repr_failure_py(self, excinfo, style=None):
if excinfo.errisinstance(fail.Exception): # Type ignored: see comment where fail.Exception is defined.
if excinfo.errisinstance(fail.Exception): # type: ignore
if not excinfo.value.pytrace: if not excinfo.value.pytrace:
return str(excinfo.value) return str(excinfo.value)
fm = self.session._fixturemanager fm = self.session._fixturemanager
@ -383,13 +401,13 @@ class Item(Node):
def __init__(self, name, parent=None, config=None, session=None, nodeid=None): def __init__(self, name, parent=None, config=None, session=None, nodeid=None):
super().__init__(name, parent, config, session, nodeid=nodeid) super().__init__(name, parent, config, session, nodeid=nodeid)
self._report_sections = [] self._report_sections = [] # type: List[Tuple[str, str, str]]
#: user properties is a list of tuples (name, value) that holds user #: user properties is a list of tuples (name, value) that holds user
#: defined properties for this test. #: defined properties for this test.
self.user_properties = [] self.user_properties = [] # type: List[Tuple[str, Any]]
def add_report_section(self, when, key, content): def add_report_section(self, when: str, key: str, content: str) -> None:
""" """
Adds a new report section, similar to what's done internally to add stdout and Adds a new report section, similar to what's done internally to add stdout and
stderr captured output:: stderr captured output::

View File

@ -59,20 +59,25 @@ def create_new_paste(contents):
Creates a new paste using bpaste.net service. Creates a new paste using bpaste.net service.
:contents: paste contents as utf-8 encoded bytes :contents: paste contents as utf-8 encoded bytes
:returns: url to the pasted contents :returns: url to the pasted contents or error message
""" """
import re import re
from urllib.request import urlopen from urllib.request import urlopen
from urllib.parse import urlencode from urllib.parse import urlencode
params = {"code": contents, "lexer": "python3", "expiry": "1week"} params = {"code": contents, "lexer": "text", "expiry": "1week"}
url = "https://bpaste.net" url = "https://bpaste.net"
response = urlopen(url, data=urlencode(params).encode("ascii")).read() try:
m = re.search(r'href="/raw/(\w+)"', response.decode("utf-8")) response = (
urlopen(url, data=urlencode(params).encode("ascii")).read().decode("utf-8")
)
except OSError as exc_info: # urllib errors
return "bad response: %s" % exc_info
m = re.search(r'href="/raw/(\w+)"', response)
if m: if m:
return "{}/show/{}".format(url, m.group(1)) return "{}/show/{}".format(url, m.group(1))
else: else:
return "bad response: " + response.decode("utf-8") return "bad response: invalid format ('" + response + "')"
def pytest_terminal_summary(terminalreporter): def pytest_terminal_summary(terminalreporter):

View File

@ -1,5 +1,6 @@
from pprint import pprint from pprint import pprint
from typing import Optional from typing import Optional
from typing import Union
import py import py
@ -267,7 +268,8 @@ class TestReport(BaseReport):
if not isinstance(excinfo, ExceptionInfo): if not isinstance(excinfo, ExceptionInfo):
outcome = "failed" outcome = "failed"
longrepr = excinfo longrepr = excinfo
elif excinfo.errisinstance(skip.Exception): # Type ignored -- see comment where skip.Exception is defined.
elif excinfo.errisinstance(skip.Exception): # type: ignore
outcome = "skipped" outcome = "skipped"
r = excinfo._getreprcrash() r = excinfo._getreprcrash()
longrepr = (str(r.path), r.lineno, r.message) longrepr = (str(r.path), r.lineno, r.message)
@ -431,7 +433,7 @@ def _report_kwargs_from_json(reportdict):
reprlocals=reprlocals, reprlocals=reprlocals,
filelocrepr=reprfileloc, filelocrepr=reprfileloc,
style=data["style"], style=data["style"],
) ) # type: Union[ReprEntry, ReprEntryNative]
elif entry_type == "ReprEntryNative": elif entry_type == "ReprEntryNative":
reprentry = ReprEntryNative(data["lines"]) reprentry = ReprEntryNative(data["lines"])
else: else:

View File

@ -3,6 +3,10 @@ import bdb
import os import os
import sys import sys
from time import time from time import time
from typing import Callable
from typing import Dict
from typing import List
from typing import Tuple
import attr import attr
@ -10,10 +14,14 @@ from .reports import CollectErrorRepr
from .reports import CollectReport from .reports import CollectReport
from .reports import TestReport from .reports import TestReport
from _pytest._code.code import ExceptionInfo from _pytest._code.code import ExceptionInfo
from _pytest.nodes import Node
from _pytest.outcomes import Exit from _pytest.outcomes import Exit
from _pytest.outcomes import Skipped from _pytest.outcomes import Skipped
from _pytest.outcomes import TEST_OUTCOME from _pytest.outcomes import TEST_OUTCOME
if False: # TYPE_CHECKING
from typing import Type
# #
# pytest plugin hooks # pytest plugin hooks
@ -118,6 +126,7 @@ def pytest_runtest_call(item):
except Exception: except Exception:
# Store trace info to allow postmortem debugging # Store trace info to allow postmortem debugging
type, value, tb = sys.exc_info() type, value, tb = sys.exc_info()
assert tb is not None
tb = tb.tb_next # Skip *this* frame tb = tb.tb_next # Skip *this* frame
sys.last_type = type sys.last_type = type
sys.last_value = value sys.last_value = value
@ -185,7 +194,7 @@ def check_interactive_exception(call, report):
def call_runtest_hook(item, when, **kwds): def call_runtest_hook(item, when, **kwds):
hookname = "pytest_runtest_" + when hookname = "pytest_runtest_" + when
ihook = getattr(item.ihook, hookname) ihook = getattr(item.ihook, hookname)
reraise = (Exit,) reraise = (Exit,) # type: Tuple[Type[BaseException], ...]
if not item.config.getoption("usepdb", False): if not item.config.getoption("usepdb", False):
reraise += (KeyboardInterrupt,) reraise += (KeyboardInterrupt,)
return CallInfo.from_call( return CallInfo.from_call(
@ -252,7 +261,8 @@ def pytest_make_collect_report(collector):
skip_exceptions = [Skipped] skip_exceptions = [Skipped]
unittest = sys.modules.get("unittest") unittest = sys.modules.get("unittest")
if unittest is not None: if unittest is not None:
skip_exceptions.append(unittest.SkipTest) # Type ignored because unittest is loaded dynamically.
skip_exceptions.append(unittest.SkipTest) # type: ignore
if call.excinfo.errisinstance(tuple(skip_exceptions)): if call.excinfo.errisinstance(tuple(skip_exceptions)):
outcome = "skipped" outcome = "skipped"
r = collector._repr_failure_py(call.excinfo, "line").reprcrash r = collector._repr_failure_py(call.excinfo, "line").reprcrash
@ -266,7 +276,7 @@ def pytest_make_collect_report(collector):
rep = CollectReport( rep = CollectReport(
collector.nodeid, outcome, longrepr, getattr(call, "result", None) collector.nodeid, outcome, longrepr, getattr(call, "result", None)
) )
rep.call = call # see collect_one_node rep.call = call # type: ignore # see collect_one_node
return rep return rep
@ -274,8 +284,8 @@ class SetupState:
""" shared state for setting up/tearing down test items or collectors. """ """ shared state for setting up/tearing down test items or collectors. """
def __init__(self): def __init__(self):
self.stack = [] self.stack = [] # type: List[Node]
self._finalizers = {} self._finalizers = {} # type: Dict[Node, List[Callable[[], None]]]
def addfinalizer(self, finalizer, colitem): def addfinalizer(self, finalizer, colitem):
""" attach a finalizer to the given colitem. """ """ attach a finalizer to the given colitem. """
@ -302,6 +312,7 @@ class SetupState:
exc = sys.exc_info() exc = sys.exc_info()
if exc: if exc:
_, val, tb = exc _, val, tb = exc
assert val is not None
raise val.with_traceback(tb) raise val.with_traceback(tb)
def _teardown_with_finalization(self, colitem): def _teardown_with_finalization(self, colitem):
@ -335,6 +346,7 @@ class SetupState:
exc = sys.exc_info() exc = sys.exc_info()
if exc: if exc:
_, val, tb = exc _, val, tb = exc
assert val is not None
raise val.with_traceback(tb) raise val.with_traceback(tb)
def prepare(self, colitem): def prepare(self, colitem):

View File

@ -2217,6 +2217,68 @@ class TestFixtureMarker:
["*ScopeMismatch*You tried*function*session*request*"] ["*ScopeMismatch*You tried*function*session*request*"]
) )
def test_dynamic_scope(self, testdir):
testdir.makeconftest(
"""
import pytest
def pytest_addoption(parser):
parser.addoption("--extend-scope", action="store_true", default=False)
def dynamic_scope(fixture_name, config):
if config.getoption("--extend-scope"):
return "session"
return "function"
@pytest.fixture(scope=dynamic_scope)
def dynamic_fixture(calls=[]):
calls.append("call")
return len(calls)
"""
)
testdir.makepyfile(
"""
def test_first(dynamic_fixture):
assert dynamic_fixture == 1
def test_second(dynamic_fixture):
assert dynamic_fixture == 2
"""
)
reprec = testdir.inline_run()
reprec.assertoutcome(passed=2)
reprec = testdir.inline_run("--extend-scope")
reprec.assertoutcome(passed=1, failed=1)
def test_dynamic_scope_bad_return(self, testdir):
testdir.makepyfile(
"""
import pytest
def dynamic_scope(**_):
return "wrong-scope"
@pytest.fixture(scope=dynamic_scope)
def fixture():
pass
"""
)
result = testdir.runpytest()
result.stdout.fnmatch_lines(
"Fixture 'fixture' from test_dynamic_scope_bad_return.py "
"got an unexpected scope value 'wrong-scope'"
)
def test_register_only_with_mark(self, testdir): def test_register_only_with_mark(self, testdir):
testdir.makeconftest( testdir.makeconftest(
""" """
@ -4044,12 +4106,43 @@ def test_fixture_named_request(testdir):
) )
def test_fixture_duplicated_arguments(testdir):
"""Raise error if there are positional and keyword arguments for the same parameter (#1682)."""
with pytest.raises(TypeError) as excinfo:
@pytest.fixture("session", scope="session")
def arg(arg):
pass
assert (
str(excinfo.value)
== "The fixture arguments are defined as positional and keyword: scope. "
"Use only keyword arguments."
)
def test_fixture_with_positionals(testdir):
"""Raise warning, but the positionals should still works (#1682)."""
from _pytest.deprecated import FIXTURE_POSITIONAL_ARGUMENTS
with pytest.warns(pytest.PytestDeprecationWarning) as warnings:
@pytest.fixture("function", [0], True)
def fixture_with_positionals():
pass
assert str(warnings[0].message) == str(FIXTURE_POSITIONAL_ARGUMENTS)
assert fixture_with_positionals._pytestfixturefunction.scope == "function"
assert fixture_with_positionals._pytestfixturefunction.params == (0,)
assert fixture_with_positionals._pytestfixturefunction.autouse
def test_indirect_fixture_does_not_break_scope(testdir): def test_indirect_fixture_does_not_break_scope(testdir):
"""Ensure that fixture scope is respected when using indirect fixtures (#570)""" """Ensure that fixture scope is respected when using indirect fixtures (#570)"""
testdir.makepyfile( testdir.makepyfile(
""" """
import pytest import pytest
instantiated = [] instantiated = []
@pytest.fixture(scope="session") @pytest.fixture(scope="session")

View File

@ -1194,6 +1194,21 @@ def test_help_and_version_after_argument_error(testdir):
assert result.ret == ExitCode.USAGE_ERROR assert result.ret == ExitCode.USAGE_ERROR
def test_help_formatter_uses_py_get_terminal_width(testdir, monkeypatch):
from _pytest.config.argparsing import DropShorterLongHelpFormatter
monkeypatch.setenv("COLUMNS", "90")
formatter = DropShorterLongHelpFormatter("prog")
assert formatter._width == 90
monkeypatch.setattr("py.io.get_terminal_width", lambda: 160)
formatter = DropShorterLongHelpFormatter("prog")
assert formatter._width == 160
formatter = DropShorterLongHelpFormatter("prog", width=42)
assert formatter._width == 42
def test_config_does_not_load_blocked_plugin_from_args(testdir): def test_config_does_not_load_blocked_plugin_from_args(testdir):
"""This tests that pytest's config setup handles "-p no:X".""" """This tests that pytest's config setup handles "-p no:X"."""
p = testdir.makepyfile("def test(capfd): pass") p = testdir.makepyfile("def test(capfd): pass")

View File

@ -82,6 +82,47 @@ class TestPaste:
def pastebin(self, request): def pastebin(self, request):
return request.config.pluginmanager.getplugin("pastebin") return request.config.pluginmanager.getplugin("pastebin")
@pytest.fixture
def mocked_urlopen_fail(self, monkeypatch):
"""
monkeypatch the actual urlopen call to emulate a HTTP Error 400
"""
calls = []
import urllib.error
import urllib.request
def mocked(url, data):
calls.append((url, data))
raise urllib.error.HTTPError(url, 400, "Bad request", None, None)
monkeypatch.setattr(urllib.request, "urlopen", mocked)
return calls
@pytest.fixture
def mocked_urlopen_invalid(self, monkeypatch):
"""
monkeypatch the actual urlopen calls done by the internal plugin
function that connects to bpaste service, but return a url in an
unexpected format
"""
calls = []
def mocked(url, data):
calls.append((url, data))
class DummyFile:
def read(self):
# part of html of a normal response
return b'View <a href="/invalid/3c0c6750bd">raw</a>.'
return DummyFile()
import urllib.request
monkeypatch.setattr(urllib.request, "urlopen", mocked)
return calls
@pytest.fixture @pytest.fixture
def mocked_urlopen(self, monkeypatch): def mocked_urlopen(self, monkeypatch):
""" """
@ -105,13 +146,26 @@ class TestPaste:
monkeypatch.setattr(urllib.request, "urlopen", mocked) monkeypatch.setattr(urllib.request, "urlopen", mocked)
return calls return calls
def test_pastebin_invalid_url(self, pastebin, mocked_urlopen_invalid):
result = pastebin.create_new_paste(b"full-paste-contents")
assert (
result
== "bad response: invalid format ('View <a href=\"/invalid/3c0c6750bd\">raw</a>.')"
)
assert len(mocked_urlopen_invalid) == 1
def test_pastebin_http_error(self, pastebin, mocked_urlopen_fail):
result = pastebin.create_new_paste(b"full-paste-contents")
assert result == "bad response: HTTP Error 400: Bad request"
assert len(mocked_urlopen_fail) == 1
def test_create_new_paste(self, pastebin, mocked_urlopen): def test_create_new_paste(self, pastebin, mocked_urlopen):
result = pastebin.create_new_paste(b"full-paste-contents") result = pastebin.create_new_paste(b"full-paste-contents")
assert result == "https://bpaste.net/show/3c0c6750bd" assert result == "https://bpaste.net/show/3c0c6750bd"
assert len(mocked_urlopen) == 1 assert len(mocked_urlopen) == 1
url, data = mocked_urlopen[0] url, data = mocked_urlopen[0]
assert type(data) is bytes assert type(data) is bytes
lexer = "python3" lexer = "text"
assert url == "https://bpaste.net" assert url == "https://bpaste.net"
assert "lexer=%s" % lexer in data.decode() assert "lexer=%s" % lexer in data.decode()
assert "code=full-paste-contents" in data.decode() assert "code=full-paste-contents" in data.decode()
@ -127,4 +181,4 @@ class TestPaste:
monkeypatch.setattr(urllib.request, "urlopen", response) monkeypatch.setattr(urllib.request, "urlopen", response)
result = pastebin.create_new_paste(b"full-paste-contents") result = pastebin.create_new_paste(b"full-paste-contents")
assert result == "bad response: something bad occurred" assert result == "bad response: invalid format ('something bad occurred')"