Skip to content
244 changes: 153 additions & 91 deletions src/_pytest/_code/code.py

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion src/_pytest/_code/source.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from ast import PyCF_ONLY_AST as _AST_FLAG
from bisect import bisect_right
from types import FrameType
from typing import Iterator
from typing import List
from typing import Optional
from typing import Sequence
Expand Down Expand Up @@ -60,7 +61,7 @@ def __getitem__(self, key: int) -> str:
raise NotImplementedError()

@overload # noqa: F811
def __getitem__(self, key: slice) -> "Source":
def __getitem__(self, key: slice) -> "Source": # noqa: F811
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There seems to be a discrepancy between where CI flake8 and my flake8 report this - on the decorator or the def. Maybe because of Python 3.8? So for now I add it to both.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe because of Python 3.8?

Very likely, yes.
Isn't there a flake8 plugin to handle this automatically maybe?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There seems to be some discussion regarding this here, I'm too lazy to read through it though...

raise NotImplementedError()

def __getitem__(self, key: Union[int, slice]) -> Union[str, "Source"]: # noqa: F811
Expand All @@ -73,6 +74,9 @@ def __getitem__(self, key: Union[int, slice]) -> Union[str, "Source"]: # noqa:
newsource.lines = self.lines[key.start : key.stop]
return newsource

def __iter__(self) -> Iterator[str]:
return iter(self.lines)

def __len__(self) -> int:
return len(self.lines)

Expand Down
5 changes: 3 additions & 2 deletions src/_pytest/assertion/rewrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -1074,13 +1074,14 @@ def try_makedirs(cache_dir) -> bool:

def get_cache_dir(file_path: Path) -> Path:
"""Returns the cache directory to write .pyc files for the given .py file path"""
if sys.version_info >= (3, 8) and sys.pycache_prefix:
# Type ignored until added in next mypy release.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to link an issue in general, but ok for now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mypy can detect outdated type: ignores so it's easy to get rid of when updating mypy.

if sys.version_info >= (3, 8) and sys.pycache_prefix: # type: ignore
# given:
# prefix = '/tmp/pycs'
# path = '/home/user/proj/test_app.py'
# we want:
# '/tmp/pycs/home/user/proj'
return Path(sys.pycache_prefix) / Path(*file_path.parts[1:-1])
return Path(sys.pycache_prefix) / Path(*file_path.parts[1:-1]) # type: ignore
else:
# classic pycache directory
return file_path.parent / "__pycache__"
69 changes: 34 additions & 35 deletions src/_pytest/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@
from contextlib import contextmanager
from inspect import Parameter
from inspect import signature
from typing import Any
from typing import Callable
from typing import Generic
from typing import Optional
from typing import overload
from typing import Tuple
from typing import TypeVar
from typing import Union

import attr
import py
Expand All @@ -40,12 +43,13 @@


if sys.version_info >= (3, 8):
from importlib import metadata as importlib_metadata # noqa: F401
# Type ignored until next mypy release.
from importlib import metadata as importlib_metadata # type: ignore
else:
import importlib_metadata # noqa: F401


def _format_args(func):
def _format_args(func: Callable[..., Any]) -> str:
return str(signature(func))


Expand All @@ -66,12 +70,12 @@ def fspath(p):
fspath = os.fspath


def is_generator(func):
def is_generator(func: object) -> bool:
genfunc = inspect.isgeneratorfunction(func)
return genfunc and not iscoroutinefunction(func)


def iscoroutinefunction(func):
def iscoroutinefunction(func: object) -> bool:
"""
Return True if func is a coroutine function (a function defined with async
def syntax, and doesn't contain yield), or a function decorated with
Expand All @@ -84,7 +88,7 @@ def syntax, and doesn't contain yield), or a function decorated with
return inspect.iscoroutinefunction(func) or getattr(func, "_is_coroutine", False)


def getlocation(function, curdir=None):
def getlocation(function, curdir=None) -> str:
function = get_real_func(function)
fn = py.path.local(inspect.getfile(function))
lineno = function.__code__.co_firstlineno
Expand All @@ -93,7 +97,7 @@ def getlocation(function, curdir=None):
return "%s:%d" % (fn, lineno + 1)


def num_mock_patch_args(function):
def num_mock_patch_args(function) -> int:
""" return number of arguments used up by mock arguments (if any) """
patchings = getattr(function, "patchings", None)
if not patchings:
Expand All @@ -112,7 +116,13 @@ def num_mock_patch_args(function):
)


def getfuncargnames(function, *, name: str = "", is_method=False, cls=None):
def getfuncargnames(
function: Callable[..., Any],
*,
name: str = "",
is_method: bool = False,
cls: Optional[type] = None
) -> Tuple[str, ...]:
"""Returns the names of a function's mandatory arguments.

This should return the names of all function arguments that:
Expand Down Expand Up @@ -180,7 +190,7 @@ def nullcontext():
from contextlib import nullcontext # noqa


def get_default_arg_names(function):
def get_default_arg_names(function: Callable[..., Any]) -> Tuple[str, ...]:
# Note: this code intentionally mirrors the code at the beginning of getfuncargnames,
# to get the arguments which were excluded from its result because they had default values
return tuple(
Expand All @@ -199,18 +209,18 @@ def get_default_arg_names(function):
)


def _translate_non_printable(s):
def _translate_non_printable(s: str) -> str:
return s.translate(_non_printable_ascii_translate_table)


STRING_TYPES = bytes, str


def _bytes_to_ascii(val):
def _bytes_to_ascii(val: bytes) -> str:
return val.decode("ascii", "backslashreplace")


def ascii_escaped(val):
def ascii_escaped(val: Union[bytes, str]):
"""If val is pure ascii, returns it as a str(). Otherwise, escapes
bytes objects into a sequence of escaped bytes:

Expand Down Expand Up @@ -307,7 +317,7 @@ def getimfunc(func):
return func


def safe_getattr(object, name, default):
def safe_getattr(object: Any, name: str, default: Any) -> Any:
""" Like getattr but return default upon any Exception or any OutcomeException.

Attribute access can potentially fail for 'evil' Python objects.
Expand All @@ -321,7 +331,7 @@ def safe_getattr(object, name, default):
return default


def safe_isclass(obj):
def safe_isclass(obj: object) -> bool:
"""Ignore any exception via isinstance on Python 3."""
try:
return inspect.isclass(obj)
Expand All @@ -342,39 +352,26 @@ def safe_isclass(obj):
)


def _setup_collect_fakemodule():
def _setup_collect_fakemodule() -> None:
from types import ModuleType
import pytest

pytest.collect = ModuleType("pytest.collect")
pytest.collect.__all__ = [] # used for setns
# Types ignored because the module is created dynamically.
pytest.collect = ModuleType("pytest.collect") # type: ignore
pytest.collect.__all__ = [] # type: ignore # used for setns
for attr_name in COLLECT_FAKEMODULE_ATTRIBUTES:
setattr(pytest.collect, attr_name, getattr(pytest, attr_name))
setattr(pytest.collect, attr_name, getattr(pytest, attr_name)) # type: ignore


class CaptureIO(io.TextIOWrapper):
def __init__(self):
def __init__(self) -> None:
super().__init__(io.BytesIO(), encoding="UTF-8", newline="", write_through=True)

def getvalue(self):
def getvalue(self) -> str:
assert isinstance(self.buffer, io.BytesIO)
return self.buffer.getvalue().decode("UTF-8")


class FuncargnamesCompatAttr:
""" helper class so that Metafunc, Function and FixtureRequest
don't need to each define the "funcargnames" compatibility attribute.
"""

@property
def funcargnames(self):
""" alias attribute for ``fixturenames`` for pre-2.3 compatibility"""
import warnings
from _pytest.deprecated import FUNCARGNAMES

warnings.warn(FUNCARGNAMES, stacklevel=2)
return self.fixturenames


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm ok with inlining, but wondered how it made typing harder (from the commit message)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a mixin class, it uses fixturenames which is not defined in the class so mypy gets confused. There are ways to type this but it's not worth the bother in this case IMO.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed.

if sys.version_info < (3, 5, 2): # pragma: no cover

def overload(f): # noqa: F811
Expand Down Expand Up @@ -407,7 +404,9 @@ def __get__(
raise NotImplementedError()

@overload # noqa: F811
def __get__(self, instance: _S, owner: Optional["Type[_S]"] = ...) -> _T:
def __get__( # noqa: F811
self, instance: _S, owner: Optional["Type[_S]"] = ...
) -> _T:
raise NotImplementedError()

def __get__(self, instance, owner=None): # noqa: F811
Expand Down
10 changes: 8 additions & 2 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from _pytest._code.code import TerminalRepr
from _pytest.compat import _format_args
from _pytest.compat import _PytestWrapper
from _pytest.compat import FuncargnamesCompatAttr
from _pytest.compat import get_real_func
from _pytest.compat import get_real_method
from _pytest.compat import getfslineno
Expand All @@ -29,6 +28,7 @@
from _pytest.compat import NOTSET
from _pytest.compat import safe_getattr
from _pytest.deprecated import FIXTURE_POSITIONAL_ARGUMENTS
from _pytest.deprecated import FUNCARGNAMES
from _pytest.outcomes import fail
from _pytest.outcomes import TEST_OUTCOME

Expand Down Expand Up @@ -336,7 +336,7 @@ def prune_dependency_tree(self):
self.names_closure[:] = sorted(closure, key=self.names_closure.index)


class FixtureRequest(FuncargnamesCompatAttr):
class FixtureRequest:
""" A request for a fixture from a test or fixture function.

A request object gives access to the requesting test context
Expand All @@ -363,6 +363,12 @@ def fixturenames(self):
result.extend(set(self._fixture_defs).difference(result))
return result

@property
def funcargnames(self):
""" alias attribute for ``fixturenames`` for pre-2.3 compatibility"""
warnings.warn(FUNCARGNAMES, stacklevel=2)
return self.fixturenames

@property
def node(self):
""" underlying collection node (depends on current request scope)"""
Expand Down
17 changes: 15 additions & 2 deletions src/_pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from _pytest.compat import safe_isclass
from _pytest.compat import STRING_TYPES
from _pytest.config import hookimpl
from _pytest.deprecated import FUNCARGNAMES
from _pytest.main import FSHookProxy
from _pytest.mark import MARK_GEN
from _pytest.mark.structures import get_unpacked_marks
Expand Down Expand Up @@ -882,7 +883,7 @@ def setmulti2(self, valtypes, argnames, valset, id, marks, scopenum, param_index
self.marks.extend(normalize_mark_list(marks))


class Metafunc(fixtures.FuncargnamesCompatAttr):
class Metafunc:
"""
Metafunc objects are passed to the :func:`pytest_generate_tests <_pytest.hookspec.pytest_generate_tests>` hook.
They help to inspect a test function and to generate tests according to
Expand Down Expand Up @@ -916,6 +917,12 @@ def __init__(self, definition, fixtureinfo, config, cls=None, module=None):
self._ids = set()
self._arg2fixturedefs = fixtureinfo.name2fixturedefs

@property
def funcargnames(self):
""" alias attribute for ``fixturenames`` for pre-2.3 compatibility"""
warnings.warn(FUNCARGNAMES, stacklevel=2)
return self.fixturenames

def parametrize(self, argnames, argvalues, indirect=False, ids=None, scope=None):
""" Add new invocations to the underlying test function using the list
of argvalues for the given argnames. Parametrization is performed
Expand Down Expand Up @@ -1333,7 +1340,7 @@ def write_docstring(tw, doc, indent=" "):
tw.write(indent + line + "\n")


class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr):
class Function(FunctionMixin, nodes.Item):
""" a Function Item is responsible for setting up and executing a
Python test function.
"""
Expand Down Expand Up @@ -1420,6 +1427,12 @@ def _pyfuncitem(self):
"(compatonly) for code expecting pytest-2.2 style request objects"
return self

@property
def funcargnames(self):
""" alias attribute for ``fixturenames`` for pre-2.3 compatibility"""
warnings.warn(FUNCARGNAMES, stacklevel=2)
return self.fixturenames

def runtest(self):
""" execute the underlying test function. """
self.ihook.pytest_pyfunc_call(pyfuncitem=self)
Expand Down
2 changes: 1 addition & 1 deletion src/_pytest/python_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,7 @@ def raises(


@overload # noqa: F811
def raises(
def raises( # noqa: F811
expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]],
func: Callable,
*args: Any,
Expand Down
6 changes: 3 additions & 3 deletions src/_pytest/recwarn.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,18 +60,18 @@ def warns(
*,
match: "Optional[Union[str, Pattern]]" = ...
) -> "WarningsChecker":
... # pragma: no cover
raise NotImplementedError()


@overload # noqa: F811
def warns(
def warns( # noqa: F811
expected_warning: Union["Type[Warning]", Tuple["Type[Warning]", ...]],
func: Callable,
*args: Any,
match: Optional[Union[str, "Pattern"]] = ...,
**kwargs: Any
) -> Union[Any]:
... # pragma: no cover
raise NotImplementedError()


def warns( # noqa: F811
Expand Down
Loading