Merge pull request #505 from pytest-dev/add-type-annotations
Add type annotations to the codebase.
This commit is contained in:
commit
02bf62014f
|
@ -11,7 +11,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
@ -24,6 +24,10 @@ jobs:
|
|||
python -m pip install --upgrade pip
|
||||
pip install -U setuptools
|
||||
pip install tox tox-gh-actions codecov
|
||||
- name: Type checking
|
||||
continue-on-error: true
|
||||
run: |
|
||||
tox -e mypy
|
||||
- name: Test with tox
|
||||
run: |
|
||||
tox
|
||||
|
|
|
@ -22,4 +22,10 @@ repos:
|
|||
rev: v2.31.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py37-plus]
|
||||
args: ["--py37-plus"]
|
||||
# TODO: Enable mypy checker when the checks succeed
|
||||
#- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
# rev: v0.931
|
||||
# hooks:
|
||||
# - id: mypy
|
||||
# additional_dependencies: [types-setuptools]
|
||||
|
|
|
@ -13,6 +13,8 @@ This release introduces breaking changes in order to be more in line with the of
|
|||
- Drop support of python 3.6, pytest 4 (elchupanebrej)
|
||||
- Step definitions can have "yield" statements again (4.0 release broke it). They will be executed as normal fixtures: code after the yield is executed during teardown of the test. (youtux)
|
||||
- Scenario outlines unused example parameter validation is removed (olegpidsadnyi)
|
||||
- Add type decorations
|
||||
- ``pytest_bdd.parsers.StepParser`` now is an Abstract Base Class. Subclasses must make sure to implement the abstract methods.
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -10,3 +10,13 @@ target-version = ["py37", "py38", "py39", "py310"]
|
|||
profile = "black"
|
||||
line_length = 120
|
||||
multi_line_output = 3
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.7"
|
||||
warn_return_any = true
|
||||
warn_unused_configs = true
|
||||
files = "pytest_bdd/**/*.py"
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = ["parse", "parse_type", "glob2"]
|
||||
ignore_missing_imports = true
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
"""pytest-bdd public API."""
|
||||
from __future__ import annotations
|
||||
|
||||
from pytest_bdd.scenario import scenario, scenarios
|
||||
from pytest_bdd.steps import given, then, when
|
||||
|
||||
__version__ = "6.0.0"
|
||||
|
||||
__all__ = [given.__name__, when.__name__, then.__name__, scenario.__name__, scenarios.__name__]
|
||||
__all__ = ["given", "when", "then", "scenario", "scenarios"]
|
||||
|
|
|
@ -1,12 +1,22 @@
|
|||
"""Cucumber json output formatter."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import time
|
||||
import typing
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
from _pytest.config import Config
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.reports import TestReport
|
||||
from _pytest.terminal import TerminalReporter
|
||||
|
||||
|
||||
def add_options(parser):
|
||||
def add_options(parser: Parser) -> None:
|
||||
"""Add pytest-bdd options."""
|
||||
group = parser.getgroup("bdd", "Cucumber JSON")
|
||||
group.addoption(
|
||||
|
@ -20,7 +30,7 @@ def add_options(parser):
|
|||
)
|
||||
|
||||
|
||||
def configure(config):
|
||||
def configure(config: Config) -> None:
|
||||
cucumber_json_path = config.option.cucumber_json_path
|
||||
# prevent opening json log on worker nodes (xdist)
|
||||
if cucumber_json_path and not hasattr(config, "workerinput"):
|
||||
|
@ -28,7 +38,7 @@ def configure(config):
|
|||
config.pluginmanager.register(config._bddcucumberjson)
|
||||
|
||||
|
||||
def unconfigure(config):
|
||||
def unconfigure(config: Config) -> None:
|
||||
xml = getattr(config, "_bddcucumberjson", None)
|
||||
if xml is not None:
|
||||
del config._bddcucumberjson
|
||||
|
@ -39,22 +49,19 @@ class LogBDDCucumberJSON:
|
|||
|
||||
"""Logging plugin for cucumber like json output."""
|
||||
|
||||
def __init__(self, logfile):
|
||||
def __init__(self, logfile: str) -> None:
|
||||
logfile = os.path.expanduser(os.path.expandvars(logfile))
|
||||
self.logfile = os.path.normpath(os.path.abspath(logfile))
|
||||
self.features = {}
|
||||
self.features: dict[str, dict] = {}
|
||||
|
||||
def append(self, obj):
|
||||
self.features[-1].append(obj)
|
||||
|
||||
def _get_result(self, step, report, error_message=False):
|
||||
def _get_result(self, step: dict[str, Any], report: TestReport, error_message: bool = False) -> dict[str, Any]:
|
||||
"""Get scenario test run result.
|
||||
|
||||
:param step: `Step` step we get result for
|
||||
:param report: pytest `Report` object
|
||||
:return: `dict` in form {"status": "<passed|failed|skipped>", ["error_message": "<error_message>"]}
|
||||
"""
|
||||
result = {}
|
||||
result: dict[str, Any] = {}
|
||||
if report.passed or not step["failed"]: # ignore setup/teardown
|
||||
result = {"status": "passed"}
|
||||
elif report.failed and step["failed"]:
|
||||
|
@ -64,7 +71,7 @@ class LogBDDCucumberJSON:
|
|||
result["duration"] = int(math.floor((10**9) * step["duration"])) # nanosec
|
||||
return result
|
||||
|
||||
def _serialize_tags(self, item):
|
||||
def _serialize_tags(self, item: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"""Serialize item's tags.
|
||||
|
||||
:param item: json-serialized `Scenario` or `Feature`.
|
||||
|
@ -78,7 +85,7 @@ class LogBDDCucumberJSON:
|
|||
"""
|
||||
return [{"name": tag, "line": item["line_number"] - 1} for tag in item["tags"]]
|
||||
|
||||
def pytest_runtest_logreport(self, report):
|
||||
def pytest_runtest_logreport(self, report: TestReport) -> None:
|
||||
try:
|
||||
scenario = report.scenario
|
||||
except AttributeError:
|
||||
|
@ -89,7 +96,7 @@ class LogBDDCucumberJSON:
|
|||
# skip if there isn't a result or scenario has no steps
|
||||
return
|
||||
|
||||
def stepmap(step):
|
||||
def stepmap(step: dict[str, Any]) -> dict[str, Any]:
|
||||
error_message = False
|
||||
if step["failed"] and not scenario.setdefault("failed", False):
|
||||
scenario["failed"] = True
|
||||
|
@ -130,12 +137,12 @@ class LogBDDCucumberJSON:
|
|||
}
|
||||
)
|
||||
|
||||
def pytest_sessionstart(self):
|
||||
def pytest_sessionstart(self) -> None:
|
||||
self.suite_start_time = time.time()
|
||||
|
||||
def pytest_sessionfinish(self):
|
||||
def pytest_sessionfinish(self) -> None:
|
||||
with open(self.logfile, "w", encoding="utf-8") as logfile:
|
||||
logfile.write(json.dumps(list(self.features.values())))
|
||||
|
||||
def pytest_terminal_summary(self, terminalreporter):
|
||||
terminalreporter.write_sep("-", "generated json file: %s" % (self.logfile))
|
||||
def pytest_terminal_summary(self, terminalreporter: TerminalReporter) -> None:
|
||||
terminalreporter.write_sep("-", f"generated json file: {self.logfile}")
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""pytest-bdd Exceptions."""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
class ScenarioIsDecoratorOnly(Exception):
|
||||
|
@ -30,6 +31,6 @@ class FeatureError(Exception):
|
|||
|
||||
message = "{0}.\nLine number: {1}.\nLine: {2}.\nFile: {3}"
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
"""String representation."""
|
||||
return self.message.format(*self.args)
|
||||
|
|
|
@ -23,15 +23,16 @@ Syntax example:
|
|||
:note: There are no multiline steps, the description of the step must fit in
|
||||
one line.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os.path
|
||||
import typing
|
||||
|
||||
import glob2
|
||||
|
||||
from .parser import Feature, parse_feature
|
||||
|
||||
# Global features dictionary
|
||||
features: typing.Dict[str, Feature] = {}
|
||||
features: dict[str, Feature] = {}
|
||||
|
||||
|
||||
def get_feature(base_path: str, filename: str, encoding: str = "utf-8") -> Feature:
|
||||
|
@ -56,7 +57,7 @@ def get_feature(base_path: str, filename: str, encoding: str = "utf-8") -> Featu
|
|||
return feature
|
||||
|
||||
|
||||
def get_features(paths: typing.List[str], **kwargs) -> typing.List[Feature]:
|
||||
def get_features(paths: list[str], **kwargs) -> list[Feature]:
|
||||
"""Get features for given paths.
|
||||
|
||||
:param list paths: `list` of paths (file or dirs)
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
"""pytest-bdd missing test code generation."""
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import os.path
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
import py
|
||||
from mako.lookup import TemplateLookup
|
||||
|
@ -11,10 +13,21 @@ from .scenario import find_argumented_step_fixture_name, make_python_docstring,
|
|||
from .steps import get_step_fixture_name
|
||||
from .types import STEP_TYPES
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Sequence
|
||||
|
||||
from _pytest.config import Config
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.fixtures import FixtureDef, FixtureManager
|
||||
from _pytest.main import Session
|
||||
from _pytest.python import Function
|
||||
|
||||
from .parser import Feature, ScenarioTemplate, Step
|
||||
|
||||
template_lookup = TemplateLookup(directories=[os.path.join(os.path.dirname(__file__), "templates")])
|
||||
|
||||
|
||||
def add_options(parser):
|
||||
def add_options(parser: Parser) -> None:
|
||||
"""Add pytest-bdd options."""
|
||||
group = parser.getgroup("bdd", "Generation")
|
||||
|
||||
|
@ -35,17 +48,18 @@ def add_options(parser):
|
|||
)
|
||||
|
||||
|
||||
def cmdline_main(config):
|
||||
def cmdline_main(config: Config) -> int | None:
|
||||
"""Check config option to show missing code."""
|
||||
if config.option.generate_missing:
|
||||
return show_missing_code(config)
|
||||
return None # Make mypy happy
|
||||
|
||||
|
||||
def generate_code(features, scenarios, steps):
|
||||
def generate_code(features: list[Feature], scenarios: list[ScenarioTemplate], steps: list[Step]) -> str:
|
||||
"""Generate test code for the given filenames."""
|
||||
grouped_steps = group_steps(steps)
|
||||
template = template_lookup.get_template("test.py.mak")
|
||||
return template.render(
|
||||
code = template.render(
|
||||
features=features,
|
||||
scenarios=scenarios,
|
||||
steps=grouped_steps,
|
||||
|
@ -53,16 +67,17 @@ def generate_code(features, scenarios, steps):
|
|||
make_python_docstring=make_python_docstring,
|
||||
make_string_literal=make_string_literal,
|
||||
)
|
||||
return cast(str, code)
|
||||
|
||||
|
||||
def show_missing_code(config):
|
||||
def show_missing_code(config: Config) -> int:
|
||||
"""Wrap pytest session to show missing code."""
|
||||
from _pytest.main import wrap_session
|
||||
|
||||
return wrap_session(config, _show_missing_code_main)
|
||||
|
||||
|
||||
def print_missing_code(scenarios, steps):
|
||||
def print_missing_code(scenarios: list[ScenarioTemplate], steps: list[Step]) -> None:
|
||||
"""Print missing code with TerminalWriter."""
|
||||
tw = py.io.TerminalWriter()
|
||||
scenario = step = None
|
||||
|
@ -108,14 +123,10 @@ def print_missing_code(scenarios, steps):
|
|||
tw.write(code)
|
||||
|
||||
|
||||
def _find_step_fixturedef(fixturemanager, item, name, type_):
|
||||
"""Find step fixturedef.
|
||||
|
||||
:param request: PyTest Item object.
|
||||
:param step: `Step`.
|
||||
|
||||
:return: Step function.
|
||||
"""
|
||||
def _find_step_fixturedef(
|
||||
fixturemanager: FixtureManager, item: Function, name: str, type_: str
|
||||
) -> Sequence[FixtureDef[Any]] | None:
|
||||
"""Find step fixturedef."""
|
||||
step_fixture_name = get_step_fixture_name(name, type_)
|
||||
fixturedefs = fixturemanager.getfixturedefs(step_fixture_name, item.nodeid)
|
||||
if fixturedefs is not None:
|
||||
|
@ -127,7 +138,7 @@ def _find_step_fixturedef(fixturemanager, item, name, type_):
|
|||
return None
|
||||
|
||||
|
||||
def parse_feature_files(paths, **kwargs):
|
||||
def parse_feature_files(paths: list[str], **kwargs: Any) -> tuple[list[Feature], list[ScenarioTemplate], list[Step]]:
|
||||
"""Parse feature files of given paths.
|
||||
|
||||
:param paths: `list` of paths (file or dirs)
|
||||
|
@ -146,7 +157,7 @@ def parse_feature_files(paths, **kwargs):
|
|||
return features, scenarios, steps
|
||||
|
||||
|
||||
def group_steps(steps):
|
||||
def group_steps(steps: list[Step]) -> list[Step]:
|
||||
"""Group steps by type."""
|
||||
steps = sorted(steps, key=lambda step: step.type)
|
||||
seen_steps = set()
|
||||
|
@ -161,7 +172,7 @@ def group_steps(steps):
|
|||
return grouped_steps
|
||||
|
||||
|
||||
def _show_missing_code_main(config, session):
|
||||
def _show_missing_code_main(config: Config, session: Session) -> None:
|
||||
"""Preparing fixture duplicates for output."""
|
||||
tw = py.io.TerminalWriter()
|
||||
session.perform_collect()
|
||||
|
|
|
@ -1,7 +1,18 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
from _pytest.terminal import TerminalReporter
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
def add_options(parser):
|
||||
from _pytest.config import Config
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.reports import TestReport
|
||||
|
||||
|
||||
def add_options(parser: Parser) -> None:
|
||||
group = parser.getgroup("terminal reporting", "reporting", after="general")
|
||||
group._addoption(
|
||||
"--gherkin-terminal-reporter",
|
||||
|
@ -12,7 +23,7 @@ def add_options(parser):
|
|||
)
|
||||
|
||||
|
||||
def configure(config):
|
||||
def configure(config: Config) -> None:
|
||||
if config.option.gherkin_terminal_reporter:
|
||||
# Get the standard terminal reporter plugin and replace it with our
|
||||
current_reporter = config.pluginmanager.getplugin("terminalreporter")
|
||||
|
@ -33,17 +44,17 @@ def configure(config):
|
|||
|
||||
|
||||
class GherkinTerminalReporter(TerminalReporter):
|
||||
def __init__(self, config):
|
||||
def __init__(self, config: Config) -> None:
|
||||
super().__init__(config)
|
||||
|
||||
def pytest_runtest_logreport(self, report):
|
||||
def pytest_runtest_logreport(self, report: TestReport) -> Any:
|
||||
rep = report
|
||||
res = self.config.hook.pytest_report_teststatus(report=rep, config=self.config)
|
||||
cat, letter, word = res
|
||||
|
||||
if not letter and not word:
|
||||
# probably passed setup/teardown
|
||||
return
|
||||
return None
|
||||
|
||||
if isinstance(word, tuple):
|
||||
word, word_markup = word
|
||||
|
@ -88,3 +99,4 @@ class GherkinTerminalReporter(TerminalReporter):
|
|||
else:
|
||||
return super().pytest_runtest_logreport(rep)
|
||||
self.stats.setdefault(cat, []).append(rep)
|
||||
return None
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
"""Pytest-bdd pytest hooks."""
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os.path
|
||||
import re
|
||||
import textwrap
|
||||
import typing
|
||||
from collections import OrderedDict
|
||||
from typing import cast
|
||||
|
||||
from . import exceptions, types
|
||||
|
||||
|
@ -24,8 +27,11 @@ STEP_PREFIXES = [
|
|||
("But ", None),
|
||||
]
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from typing import Any, Iterable, Mapping, Match
|
||||
|
||||
def split_line(line):
|
||||
|
||||
def split_line(line: str) -> list[str]:
|
||||
"""Split the given Examples line.
|
||||
|
||||
:param str|unicode line: Feature file Examples line.
|
||||
|
@ -35,7 +41,7 @@ def split_line(line):
|
|||
return [cell.replace("\\|", "|").strip() for cell in SPLIT_LINE_RE.split(line)[1:-1]]
|
||||
|
||||
|
||||
def parse_line(line):
|
||||
def parse_line(line: str) -> tuple[str, str]:
|
||||
"""Parse step line to get the step prefix (Scenario, Given, When, Then or And) and the actual step name.
|
||||
|
||||
:param line: Line of the Feature file.
|
||||
|
@ -48,7 +54,7 @@ def parse_line(line):
|
|||
return "", line
|
||||
|
||||
|
||||
def strip_comments(line):
|
||||
def strip_comments(line: str) -> str:
|
||||
"""Remove comments.
|
||||
|
||||
:param str line: Line of the Feature file.
|
||||
|
@ -61,7 +67,7 @@ def strip_comments(line):
|
|||
return line.strip()
|
||||
|
||||
|
||||
def get_step_type(line):
|
||||
def get_step_type(line: str) -> str | None:
|
||||
"""Detect step type by the beginning of the line.
|
||||
|
||||
:param str line: Line of the Feature file.
|
||||
|
@ -71,9 +77,10 @@ def get_step_type(line):
|
|||
for prefix, _type in STEP_PREFIXES:
|
||||
if line.startswith(prefix):
|
||||
return _type
|
||||
return None
|
||||
|
||||
|
||||
def parse_feature(basedir: str, filename: str, encoding: str = "utf-8") -> "Feature":
|
||||
def parse_feature(basedir: str, filename: str, encoding: str = "utf-8") -> Feature:
|
||||
"""Parse the feature file.
|
||||
|
||||
:param str basedir: Feature files base directory.
|
||||
|
@ -92,10 +99,10 @@ def parse_feature(basedir: str, filename: str, encoding: str = "utf-8") -> "Feat
|
|||
background=None,
|
||||
description="",
|
||||
)
|
||||
scenario: typing.Optional[ScenarioTemplate] = None
|
||||
mode = None
|
||||
scenario: ScenarioTemplate | None = None
|
||||
mode: str | None = None
|
||||
prev_mode = None
|
||||
description: typing.List[str] = []
|
||||
description: list[str] = []
|
||||
step = None
|
||||
multiline_step = False
|
||||
prev_line = None
|
||||
|
@ -164,10 +171,10 @@ def parse_feature(basedir: str, filename: str, encoding: str = "utf-8") -> "Feat
|
|||
elif mode and mode not in (types.FEATURE, types.TAG):
|
||||
step = Step(name=parsed_line, type=mode, indent=line_indent, line_number=line_number, keyword=keyword)
|
||||
if feature.background and not scenario:
|
||||
target = feature.background
|
||||
feature.background.add_step(step)
|
||||
else:
|
||||
target = scenario
|
||||
target.add_step(step)
|
||||
scenario = cast(ScenarioTemplate, scenario)
|
||||
scenario.add_step(step)
|
||||
prev_line = clean_line
|
||||
|
||||
feature.description = "\n".join(description).strip()
|
||||
|
@ -177,15 +184,25 @@ def parse_feature(basedir: str, filename: str, encoding: str = "utf-8") -> "Feat
|
|||
class Feature:
|
||||
"""Feature."""
|
||||
|
||||
def __init__(self, scenarios, filename, rel_filename, name, tags, background, line_number, description):
|
||||
self.scenarios: typing.Dict[str, ScenarioTemplate] = scenarios
|
||||
self.rel_filename = rel_filename
|
||||
self.filename = filename
|
||||
self.tags = tags
|
||||
self.name = name
|
||||
self.line_number = line_number
|
||||
self.description = description
|
||||
self.background = background
|
||||
def __init__(
|
||||
self,
|
||||
scenarios: OrderedDict,
|
||||
filename: str,
|
||||
rel_filename: str,
|
||||
name: str | None,
|
||||
tags: set,
|
||||
background: Background | None,
|
||||
line_number: int,
|
||||
description: str,
|
||||
) -> None:
|
||||
self.scenarios: dict[str, ScenarioTemplate] = scenarios
|
||||
self.rel_filename: str = rel_filename
|
||||
self.filename: str = filename
|
||||
self.tags: set = tags
|
||||
self.name: str | None = name
|
||||
self.line_number: int = line_number
|
||||
self.description: str = description
|
||||
self.background: Background | None = background
|
||||
|
||||
|
||||
class ScenarioTemplate:
|
||||
|
@ -193,7 +210,7 @@ class ScenarioTemplate:
|
|||
|
||||
Created when parsing the feature file, it will then be combined with the examples to create a Scenario."""
|
||||
|
||||
def __init__(self, feature: Feature, name: str, line_number: int, tags=None):
|
||||
def __init__(self, feature: Feature, name: str, line_number: int, tags=None) -> None:
|
||||
"""
|
||||
|
||||
:param str name: Scenario name.
|
||||
|
@ -202,12 +219,12 @@ class ScenarioTemplate:
|
|||
"""
|
||||
self.feature = feature
|
||||
self.name = name
|
||||
self._steps: typing.List[Step] = []
|
||||
self._steps: list[Step] = []
|
||||
self.examples = Examples()
|
||||
self.line_number = line_number
|
||||
self.tags = tags or set()
|
||||
|
||||
def add_step(self, step):
|
||||
def add_step(self, step: Step) -> None:
|
||||
"""Add step to the scenario.
|
||||
|
||||
:param pytest_bdd.parser.Step step: Step.
|
||||
|
@ -216,11 +233,11 @@ class ScenarioTemplate:
|
|||
self._steps.append(step)
|
||||
|
||||
@property
|
||||
def steps(self):
|
||||
def steps(self) -> list[Step]:
|
||||
background = self.feature.background
|
||||
return (background.steps if background else []) + self._steps
|
||||
|
||||
def render(self, context: typing.Mapping[str, typing.Any]) -> "Scenario":
|
||||
def render(self, context: Mapping[str, Any]) -> Scenario:
|
||||
steps = [
|
||||
Step(
|
||||
name=templated_step.render(context),
|
||||
|
@ -238,7 +255,7 @@ class Scenario:
|
|||
|
||||
"""Scenario."""
|
||||
|
||||
def __init__(self, feature: Feature, name: str, line_number: int, steps: "typing.List[Step]", tags=None):
|
||||
def __init__(self, feature: Feature, name: str, line_number: int, steps: list[Step], tags=None) -> None:
|
||||
"""Scenario constructor.
|
||||
|
||||
:param pytest_bdd.parser.Feature feature: Feature.
|
||||
|
@ -258,7 +275,7 @@ class Step:
|
|||
|
||||
"""Step."""
|
||||
|
||||
def __init__(self, name, type, indent, line_number, keyword):
|
||||
def __init__(self, name: str, type: str, indent: int, line_number: int, keyword: str) -> None:
|
||||
"""Step constructor.
|
||||
|
||||
:param str name: step name.
|
||||
|
@ -267,19 +284,17 @@ class Step:
|
|||
:param int line_number: line number.
|
||||
:param str keyword: step keyword.
|
||||
"""
|
||||
self.name = name
|
||||
self.keyword = keyword
|
||||
self.lines = []
|
||||
self.indent = indent
|
||||
self.type = type
|
||||
self.line_number = line_number
|
||||
self.failed = False
|
||||
self.start = 0
|
||||
self.stop = 0
|
||||
self.scenario = None
|
||||
self.background = None
|
||||
self.name: str = name
|
||||
self.keyword: str = keyword
|
||||
self.lines: list[str] = []
|
||||
self.indent: int = indent
|
||||
self.type: str = type
|
||||
self.line_number: int = line_number
|
||||
self.failed: bool = False
|
||||
self.scenario: ScenarioTemplate | None = None
|
||||
self.background: Background | None = None
|
||||
|
||||
def add_line(self, line):
|
||||
def add_line(self, line: str) -> None:
|
||||
"""Add line to the multiple step.
|
||||
|
||||
:param str line: Line of text - the continuation of the step name.
|
||||
|
@ -287,7 +302,7 @@ class Step:
|
|||
self.lines.append(line)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def name(self) -> str:
|
||||
"""Get step name."""
|
||||
multilines_content = textwrap.dedent("\n".join(self.lines)) if self.lines else ""
|
||||
|
||||
|
@ -303,21 +318,21 @@ class Step:
|
|||
return "\n".join(lines).strip()
|
||||
|
||||
@name.setter
|
||||
def name(self, value):
|
||||
def name(self, value: str) -> None:
|
||||
"""Set step name."""
|
||||
self._name = value
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
"""Full step name including the type."""
|
||||
return f'{self.type.capitalize()} "{self.name}"'
|
||||
|
||||
@property
|
||||
def params(self):
|
||||
def params(self) -> tuple[str, ...]:
|
||||
"""Get step params."""
|
||||
return tuple(frozenset(STEP_PARAM_RE.findall(self.name)))
|
||||
|
||||
def render(self, context: typing.Mapping[str, typing.Any]):
|
||||
def replacer(m: typing.Match):
|
||||
def render(self, context: Mapping[str, Any]):
|
||||
def replacer(m: Match):
|
||||
varname = m.group(1)
|
||||
return str(context[varname])
|
||||
|
||||
|
@ -328,17 +343,17 @@ class Background:
|
|||
|
||||
"""Background."""
|
||||
|
||||
def __init__(self, feature, line_number):
|
||||
def __init__(self, feature: Feature, line_number: int) -> None:
|
||||
"""Background constructor.
|
||||
|
||||
:param pytest_bdd.parser.Feature feature: Feature.
|
||||
:param int line_number: Line number.
|
||||
"""
|
||||
self.feature = feature
|
||||
self.line_number = line_number
|
||||
self.steps = []
|
||||
self.feature: Feature = feature
|
||||
self.line_number: int = line_number
|
||||
self.steps: list[Step] = []
|
||||
|
||||
def add_step(self, step):
|
||||
def add_step(self, step: Step) -> None:
|
||||
"""Add step to the background."""
|
||||
step.background = self
|
||||
self.steps.append(step)
|
||||
|
@ -348,40 +363,28 @@ class Examples:
|
|||
|
||||
"""Example table."""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
"""Initialize examples instance."""
|
||||
self.example_params = []
|
||||
self.examples = []
|
||||
self.line_number = None
|
||||
self.example_params: list[str] = []
|
||||
self.examples: list[list[str]] = []
|
||||
self.line_number: int | None = None
|
||||
self.name = None
|
||||
|
||||
def set_param_names(self, keys):
|
||||
def set_param_names(self, keys: list[str]) -> None:
|
||||
"""Set parameter names.
|
||||
|
||||
:param names: `list` of `string` parameter names.
|
||||
"""
|
||||
self.example_params = [str(key) for key in keys]
|
||||
|
||||
def add_example(self, values):
|
||||
def add_example(self, values: list[str]) -> None:
|
||||
"""Add example.
|
||||
|
||||
:param values: `list` of `string` parameter values.
|
||||
"""
|
||||
self.examples.append(values)
|
||||
|
||||
def add_example_row(self, param, values):
|
||||
"""Add example row.
|
||||
|
||||
:param param: `str` parameter name
|
||||
:param values: `list` of `string` parameter values
|
||||
"""
|
||||
if param in self.example_params:
|
||||
raise exceptions.ExamplesNotValidError(
|
||||
f"""Example rows should contain unique parameters. "{param}" appeared more than once"""
|
||||
)
|
||||
self.example_params.append(param)
|
||||
|
||||
def as_contexts(self) -> typing.Iterable[typing.Dict[str, typing.Any]]:
|
||||
def as_contexts(self) -> Iterable[dict[str, Any]]:
|
||||
if not self.examples:
|
||||
return
|
||||
|
||||
|
@ -391,12 +394,12 @@ class Examples:
|
|||
assert len(header) == len(row)
|
||||
yield dict(zip(header, row))
|
||||
|
||||
def __bool__(self):
|
||||
def __bool__(self) -> bool:
|
||||
"""Bool comparison."""
|
||||
return bool(self.examples)
|
||||
|
||||
|
||||
def get_tags(line):
|
||||
def get_tags(line: str | None) -> set[str]:
|
||||
"""Get tags out of the given line.
|
||||
|
||||
:param str line: Feature file text line.
|
||||
|
|
|
@ -1,47 +1,53 @@
|
|||
"""Step parsers."""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
import abc
|
||||
import re as base_re
|
||||
from functools import partial
|
||||
from typing import Any, Dict, cast
|
||||
|
||||
import parse as base_parse
|
||||
from parse_type import cfparse as base_cfparse
|
||||
|
||||
|
||||
class StepParser:
|
||||
class StepParser(abc.ABC):
|
||||
"""Parser of the individual step."""
|
||||
|
||||
def __init__(self, name):
|
||||
def __init__(self, name: str) -> None:
|
||||
self.name = name
|
||||
|
||||
def parse_arguments(self, name):
|
||||
@abc.abstractmethod
|
||||
def parse_arguments(self, name: str) -> dict[str, Any] | None:
|
||||
"""Get step arguments from the given step name.
|
||||
|
||||
:return: `dict` of step arguments
|
||||
"""
|
||||
raise NotImplementedError() # pragma: no cover
|
||||
...
|
||||
|
||||
def is_matching(self, name):
|
||||
@abc.abstractmethod
|
||||
def is_matching(self, name: str) -> bool:
|
||||
"""Match given name with the step name."""
|
||||
raise NotImplementedError() # pragma: no cover
|
||||
...
|
||||
|
||||
|
||||
class re(StepParser):
|
||||
"""Regex step parser."""
|
||||
|
||||
def __init__(self, name, *args, **kwargs):
|
||||
def __init__(self, name: str, *args: Any, **kwargs: Any) -> None:
|
||||
"""Compile regex."""
|
||||
super().__init__(name)
|
||||
self.regex = base_re.compile(self.name, *args, **kwargs)
|
||||
|
||||
def parse_arguments(self, name):
|
||||
def parse_arguments(self, name: str) -> dict[str, str] | None:
|
||||
"""Get step arguments.
|
||||
|
||||
:return: `dict` of step arguments
|
||||
"""
|
||||
return self.regex.match(name).groupdict()
|
||||
match = self.regex.match(name)
|
||||
if match is None:
|
||||
return None
|
||||
return match.groupdict()
|
||||
|
||||
def is_matching(self, name):
|
||||
def is_matching(self, name: str) -> bool:
|
||||
"""Match given name with the step name."""
|
||||
return bool(self.regex.match(name))
|
||||
|
||||
|
@ -49,19 +55,19 @@ class re(StepParser):
|
|||
class parse(StepParser):
|
||||
"""parse step parser."""
|
||||
|
||||
def __init__(self, name, *args, **kwargs):
|
||||
def __init__(self, name: str, *args: Any, **kwargs: Any) -> None:
|
||||
"""Compile parse expression."""
|
||||
super().__init__(name)
|
||||
self.parser = base_parse.compile(self.name, *args, **kwargs)
|
||||
|
||||
def parse_arguments(self, name):
|
||||
def parse_arguments(self, name: str) -> dict[str, Any]:
|
||||
"""Get step arguments.
|
||||
|
||||
:return: `dict` of step arguments
|
||||
"""
|
||||
return self.parser.parse(name).named
|
||||
return cast(Dict[str, Any], self.parser.parse(name).named)
|
||||
|
||||
def is_matching(self, name):
|
||||
def is_matching(self, name: str) -> bool:
|
||||
"""Match given name with the step name."""
|
||||
try:
|
||||
return bool(self.parser.parse(name))
|
||||
|
@ -72,7 +78,7 @@ class parse(StepParser):
|
|||
class cfparse(parse):
|
||||
"""cfparse step parser."""
|
||||
|
||||
def __init__(self, name, *args, **kwargs):
|
||||
def __init__(self, name: str, *args: Any, **kwargs: Any) -> None:
|
||||
"""Compile parse expression."""
|
||||
super(parse, self).__init__(name)
|
||||
self.parser = base_cfparse.Parser(self.name, *args, **kwargs)
|
||||
|
@ -81,36 +87,22 @@ class cfparse(parse):
|
|||
class string(StepParser):
|
||||
"""Exact string step parser."""
|
||||
|
||||
def __init__(self, name):
|
||||
"""Stringify"""
|
||||
name = str(name, **({"encoding": "utf-8"} if isinstance(name, bytes) else {}))
|
||||
super().__init__(name)
|
||||
|
||||
def parse_arguments(self, name):
|
||||
def parse_arguments(self, name: str) -> dict:
|
||||
"""No parameters are available for simple string step.
|
||||
|
||||
:return: `dict` of step arguments
|
||||
"""
|
||||
return {}
|
||||
|
||||
def is_matching(self, name):
|
||||
def is_matching(self, name: str) -> bool:
|
||||
"""Match given name with the step name."""
|
||||
return self.name == name
|
||||
|
||||
|
||||
def get_parser(step_name):
|
||||
"""Get parser by given name.
|
||||
def get_parser(step_name: Any) -> StepParser:
|
||||
"""Get parser by given name."""
|
||||
|
||||
:param step_name: name of the step to parse
|
||||
|
||||
:return: step parser object
|
||||
:rtype: StepArgumentParser
|
||||
"""
|
||||
|
||||
def does_support_parser_interface(obj):
|
||||
return all(map(partial(hasattr, obj), ["is_matching", "parse_arguments"]))
|
||||
|
||||
if does_support_parser_interface(step_name):
|
||||
if isinstance(step_name, StepParser):
|
||||
return step_name
|
||||
else:
|
||||
return string(step_name)
|
||||
|
||||
return string(step_name)
|
||||
|
|
|
@ -1,12 +1,27 @@
|
|||
"""Pytest plugin entry point. Used for any fixtures needed."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Callable, cast
|
||||
|
||||
import pytest
|
||||
|
||||
from . import cucumber_json, generation, gherkin_terminal_reporter, given, reporting, then, when
|
||||
from .utils import CONFIG_STACK
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Generator
|
||||
|
||||
def pytest_addhooks(pluginmanager):
|
||||
from _pytest.config import Config, PytestPluginManager
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.fixtures import FixtureRequest
|
||||
from _pytest.nodes import Item
|
||||
from _pytest.runner import CallInfo
|
||||
from pluggy._result import _Result
|
||||
|
||||
from .parser import Feature, Scenario, Step
|
||||
|
||||
|
||||
def pytest_addhooks(pluginmanager: PytestPluginManager) -> None:
|
||||
"""Register plugin hooks."""
|
||||
from pytest_bdd import hooks
|
||||
|
||||
|
@ -16,13 +31,13 @@ def pytest_addhooks(pluginmanager):
|
|||
@given("trace")
|
||||
@when("trace")
|
||||
@then("trace")
|
||||
def trace():
|
||||
def trace() -> None:
|
||||
"""Enter pytest's pdb trace."""
|
||||
pytest.set_trace()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def _pytest_bdd_example():
|
||||
def _pytest_bdd_example() -> dict:
|
||||
"""The current scenario outline parametrization.
|
||||
|
||||
This is used internally by pytest_bdd.
|
||||
|
@ -35,7 +50,7 @@ def _pytest_bdd_example():
|
|||
return {}
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
def pytest_addoption(parser: Parser) -> None:
|
||||
"""Add pytest-bdd options."""
|
||||
add_bdd_ini(parser)
|
||||
cucumber_json.add_options(parser)
|
||||
|
@ -43,54 +58,72 @@ def pytest_addoption(parser):
|
|||
gherkin_terminal_reporter.add_options(parser)
|
||||
|
||||
|
||||
def add_bdd_ini(parser):
|
||||
def add_bdd_ini(parser: Parser) -> None:
|
||||
parser.addini("bdd_features_base_dir", "Base features directory.")
|
||||
|
||||
|
||||
@pytest.mark.trylast
|
||||
def pytest_configure(config):
|
||||
def pytest_configure(config: Config) -> None:
|
||||
"""Configure all subplugins."""
|
||||
CONFIG_STACK.append(config)
|
||||
cucumber_json.configure(config)
|
||||
gherkin_terminal_reporter.configure(config)
|
||||
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
def pytest_unconfigure(config: Config) -> None:
|
||||
"""Unconfigure all subplugins."""
|
||||
CONFIG_STACK.pop()
|
||||
cucumber_json.unconfigure(config)
|
||||
|
||||
|
||||
@pytest.mark.hookwrapper
|
||||
def pytest_runtest_makereport(item, call):
|
||||
def pytest_runtest_makereport(item: Item, call: CallInfo) -> Generator[None, _Result, None]:
|
||||
outcome = yield
|
||||
reporting.runtest_makereport(item, call, outcome.get_result())
|
||||
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
def pytest_bdd_before_scenario(request, feature, scenario):
|
||||
def pytest_bdd_before_scenario(request: FixtureRequest, feature: Feature, scenario: Scenario) -> None:
|
||||
reporting.before_scenario(request, feature, scenario)
|
||||
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
def pytest_bdd_step_error(request, feature, scenario, step, step_func, step_func_args, exception):
|
||||
def pytest_bdd_step_error(
|
||||
request: FixtureRequest,
|
||||
feature: Feature,
|
||||
scenario: Scenario,
|
||||
step: Step,
|
||||
step_func: Callable,
|
||||
step_func_args: dict,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
reporting.step_error(request, feature, scenario, step, step_func, step_func_args, exception)
|
||||
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
def pytest_bdd_before_step(request, feature, scenario, step, step_func):
|
||||
def pytest_bdd_before_step(
|
||||
request: FixtureRequest, feature: Feature, scenario: Scenario, step: Step, step_func: Callable
|
||||
) -> None:
|
||||
reporting.before_step(request, feature, scenario, step, step_func)
|
||||
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
def pytest_bdd_after_step(request, feature, scenario, step, step_func, step_func_args):
|
||||
def pytest_bdd_after_step(
|
||||
request: FixtureRequest,
|
||||
feature: Feature,
|
||||
scenario: Scenario,
|
||||
step: Step,
|
||||
step_func: Callable,
|
||||
step_func_args: dict[str, Any],
|
||||
) -> None:
|
||||
reporting.after_step(request, feature, scenario, step, step_func, step_func_args)
|
||||
|
||||
|
||||
def pytest_cmdline_main(config):
|
||||
def pytest_cmdline_main(config: Config) -> int | None:
|
||||
return generation.cmdline_main(config)
|
||||
|
||||
|
||||
def pytest_bdd_apply_tag(tag, function):
|
||||
def pytest_bdd_apply_tag(tag: str, function: Callable) -> Callable:
|
||||
mark = getattr(pytest.mark, tag)
|
||||
return mark(function)
|
||||
marked = mark(function)
|
||||
return cast(Callable, marked)
|
||||
|
|
|
@ -3,8 +3,20 @@
|
|||
Collection of the scenario execution statuses, timing and other information
|
||||
that enriches the pytest test reporting.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Callable
|
||||
|
||||
from _pytest.fixtures import FixtureRequest
|
||||
from _pytest.nodes import Item
|
||||
from _pytest.reports import TestReport
|
||||
from _pytest.runner import CallInfo
|
||||
|
||||
from .parser import Feature, Scenario, Step
|
||||
|
||||
|
||||
class StepReport:
|
||||
|
@ -13,7 +25,7 @@ class StepReport:
|
|||
failed = False
|
||||
stopped = None
|
||||
|
||||
def __init__(self, step):
|
||||
def __init__(self, step: Step) -> None:
|
||||
"""Step report constructor.
|
||||
|
||||
:param pytest_bdd.parser.Step step: Step.
|
||||
|
@ -21,7 +33,7 @@ class StepReport:
|
|||
self.step = step
|
||||
self.started = time.perf_counter()
|
||||
|
||||
def serialize(self):
|
||||
def serialize(self) -> dict[str, Any]:
|
||||
"""Serialize the step execution report.
|
||||
|
||||
:return: Serialized step execution report.
|
||||
|
@ -36,7 +48,7 @@ class StepReport:
|
|||
"duration": self.duration,
|
||||
}
|
||||
|
||||
def finalize(self, failed):
|
||||
def finalize(self, failed: bool) -> None:
|
||||
"""Stop collecting information and finalize the report.
|
||||
|
||||
:param bool failed: Whether the step execution is failed.
|
||||
|
@ -45,7 +57,7 @@ class StepReport:
|
|||
self.failed = failed
|
||||
|
||||
@property
|
||||
def duration(self):
|
||||
def duration(self) -> float:
|
||||
"""Step execution duration.
|
||||
|
||||
:return: Step execution duration.
|
||||
|
@ -60,17 +72,17 @@ class StepReport:
|
|||
class ScenarioReport:
|
||||
"""Scenario execution report."""
|
||||
|
||||
def __init__(self, scenario, node):
|
||||
def __init__(self, scenario: Scenario) -> None:
|
||||
"""Scenario report constructor.
|
||||
|
||||
:param pytest_bdd.parser.Scenario scenario: Scenario.
|
||||
:param node: pytest test node object
|
||||
"""
|
||||
self.scenario = scenario
|
||||
self.step_reports = []
|
||||
self.scenario: Scenario = scenario
|
||||
self.step_reports: list[StepReport] = []
|
||||
|
||||
@property
|
||||
def current_step_report(self):
|
||||
def current_step_report(self) -> StepReport:
|
||||
"""Get current step report.
|
||||
|
||||
:return: Last or current step report.
|
||||
|
@ -78,7 +90,7 @@ class ScenarioReport:
|
|||
"""
|
||||
return self.step_reports[-1]
|
||||
|
||||
def add_step_report(self, step_report):
|
||||
def add_step_report(self, step_report: StepReport) -> None:
|
||||
"""Add new step report.
|
||||
|
||||
:param step_report: New current step report.
|
||||
|
@ -86,7 +98,7 @@ class ScenarioReport:
|
|||
"""
|
||||
self.step_reports.append(step_report)
|
||||
|
||||
def serialize(self):
|
||||
def serialize(self) -> dict[str, Any]:
|
||||
"""Serialize scenario execution report in order to transfer reporting from nodes in the distributed mode.
|
||||
|
||||
:return: Serialized report.
|
||||
|
@ -110,7 +122,7 @@ class ScenarioReport:
|
|||
},
|
||||
}
|
||||
|
||||
def fail(self):
|
||||
def fail(self) -> None:
|
||||
"""Stop collecting information and finalize the report as failed."""
|
||||
self.current_step_report.finalize(failed=True)
|
||||
remaining_steps = self.scenario.steps[len(self.step_reports) :]
|
||||
|
@ -122,10 +134,10 @@ class ScenarioReport:
|
|||
self.add_step_report(report)
|
||||
|
||||
|
||||
def runtest_makereport(item, call, rep):
|
||||
def runtest_makereport(item: Item, call: CallInfo, rep: TestReport) -> None:
|
||||
"""Store item in the report object."""
|
||||
try:
|
||||
scenario_report = item.__scenario_report__
|
||||
scenario_report: ScenarioReport = item.__scenario_report__
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
|
@ -133,21 +145,36 @@ def runtest_makereport(item, call, rep):
|
|||
rep.item = {"name": item.name}
|
||||
|
||||
|
||||
def before_scenario(request, feature, scenario):
|
||||
def before_scenario(request: FixtureRequest, feature: Feature, scenario: Scenario) -> None:
|
||||
"""Create scenario report for the item."""
|
||||
request.node.__scenario_report__ = ScenarioReport(scenario=scenario, node=request.node)
|
||||
request.node.__scenario_report__ = ScenarioReport(scenario=scenario)
|
||||
|
||||
|
||||
def step_error(request, feature, scenario, step, step_func, step_func_args, exception):
|
||||
def step_error(
|
||||
request: FixtureRequest,
|
||||
feature: Feature,
|
||||
scenario: Scenario,
|
||||
step: Step,
|
||||
step_func: Callable,
|
||||
step_func_args: dict,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
"""Finalize the step report as failed."""
|
||||
request.node.__scenario_report__.fail()
|
||||
|
||||
|
||||
def before_step(request, feature, scenario, step, step_func):
|
||||
def before_step(request: FixtureRequest, feature: Feature, scenario: Scenario, step: Step, step_func: Callable) -> None:
|
||||
"""Store step start time."""
|
||||
request.node.__scenario_report__.add_step_report(StepReport(step=step))
|
||||
|
||||
|
||||
def after_step(request, feature, scenario, step, step_func, step_func_args):
|
||||
def after_step(
|
||||
request: FixtureRequest,
|
||||
feature: Feature,
|
||||
scenario: Scenario,
|
||||
step: Step,
|
||||
step_func: Callable,
|
||||
step_func_args: dict,
|
||||
) -> None:
|
||||
"""Finalize the step report as successful."""
|
||||
request.node.__scenario_report__.current_step_report.finalize(failed=False)
|
||||
|
|
|
@ -10,29 +10,35 @@ test_publish_article = scenario(
|
|||
scenario_name="Publishing the article",
|
||||
)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import os
|
||||
import re
|
||||
import typing
|
||||
from typing import TYPE_CHECKING, Callable, cast
|
||||
|
||||
import pytest
|
||||
from _pytest.fixtures import FixtureLookupError, call_fixture_func
|
||||
from _pytest.fixtures import FixtureLookupError, FixtureManager, FixtureRequest, call_fixture_func
|
||||
|
||||
from . import exceptions
|
||||
from .feature import get_feature, get_features
|
||||
from .steps import get_step_fixture_name, inject_fixture
|
||||
from .utils import CONFIG_STACK, get_args, get_caller_module_locals, get_caller_module_path
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Iterable
|
||||
|
||||
from _pytest.mark.structures import ParameterSet
|
||||
|
||||
from .parser import Feature, Scenario, ScenarioTemplate
|
||||
from .parser import Feature, Scenario, ScenarioTemplate, Step
|
||||
|
||||
PYTHON_REPLACE_REGEX = re.compile(r"\W")
|
||||
ALPHA_REGEX = re.compile(r"^\d+_*")
|
||||
|
||||
|
||||
def find_argumented_step_fixture_name(name, type_, fixturemanager, request=None):
|
||||
def find_argumented_step_fixture_name(
|
||||
name: str, type_: str, fixturemanager: FixtureManager, request: FixtureRequest | None = None
|
||||
) -> str | None:
|
||||
"""Find argumented step fixture name."""
|
||||
# happens to be that _arg2fixturedefs is changed during the iteration so we use a copy
|
||||
for fixturename, fixturedefs in list(fixturemanager._arg2fixturedefs.items()):
|
||||
|
@ -51,9 +57,10 @@ def find_argumented_step_fixture_name(name, type_, fixturemanager, request=None)
|
|||
except FixtureLookupError:
|
||||
continue
|
||||
return parser_name
|
||||
return None
|
||||
|
||||
|
||||
def _find_step_function(request, step, scenario):
|
||||
def _find_step_function(request: FixtureRequest, step: Step, scenario: Scenario) -> Any:
|
||||
"""Match the step defined by the regular expression pattern.
|
||||
|
||||
:param request: PyTest request object.
|
||||
|
@ -70,9 +77,9 @@ def _find_step_function(request, step, scenario):
|
|||
except FixtureLookupError:
|
||||
try:
|
||||
# Could not find a fixture with the same name, let's see if there is a parser involved
|
||||
name = find_argumented_step_fixture_name(name, step.type, request._fixturemanager, request)
|
||||
if name:
|
||||
return request.getfixturevalue(name)
|
||||
argumented_name = find_argumented_step_fixture_name(name, step.type, request._fixturemanager, request)
|
||||
if argumented_name:
|
||||
return request.getfixturevalue(argumented_name)
|
||||
raise
|
||||
except FixtureLookupError:
|
||||
raise exceptions.StepDefinitionNotFoundError(
|
||||
|
@ -81,7 +88,7 @@ def _find_step_function(request, step, scenario):
|
|||
)
|
||||
|
||||
|
||||
def _execute_step_function(request, scenario, step, step_func):
|
||||
def _execute_step_function(request: FixtureRequest, scenario: Scenario, step: Step, step_func: Callable) -> None:
|
||||
"""Execute step function.
|
||||
|
||||
:param request: PyTest request.
|
||||
|
@ -124,7 +131,7 @@ def _execute_step_function(request, scenario, step, step_func):
|
|||
raise
|
||||
|
||||
|
||||
def _execute_scenario(feature: "Feature", scenario: "Scenario", request):
|
||||
def _execute_scenario(feature: Feature, scenario: Scenario, request: FixtureRequest) -> None:
|
||||
"""Execute the scenario.
|
||||
|
||||
:param feature: Feature.
|
||||
|
@ -153,29 +160,29 @@ FakeRequest = collections.namedtuple("FakeRequest", ["module"])
|
|||
|
||||
|
||||
def _get_scenario_decorator(
|
||||
feature: "Feature", feature_name: str, templated_scenario: "ScenarioTemplate", scenario_name: str
|
||||
):
|
||||
feature: Feature, feature_name: str, templated_scenario: ScenarioTemplate, scenario_name: str
|
||||
) -> Callable[[Callable], Callable]:
|
||||
# HACK: Ideally we would use `def decorator(fn)`, but we want to return a custom exception
|
||||
# when the decorator is misused.
|
||||
# Pytest inspect the signature to determine the required fixtures, and in that case it would look
|
||||
# for a fixture called "fn" that doesn't exist (if it exists then it's even worse).
|
||||
# It will error with a "fixture 'fn' not found" message instead.
|
||||
# We can avoid this hack by using a pytest hook and check for misuse instead.
|
||||
def decorator(*args):
|
||||
def decorator(*args: Callable) -> Callable:
|
||||
if not args:
|
||||
raise exceptions.ScenarioIsDecoratorOnly(
|
||||
"scenario function can only be used as a decorator. Refer to the documentation."
|
||||
)
|
||||
[fn] = args
|
||||
args = get_args(fn)
|
||||
func_args = get_args(fn)
|
||||
|
||||
# We need to tell pytest that the original function requires its fixtures,
|
||||
# otherwise indirect fixtures would not work.
|
||||
@pytest.mark.usefixtures(*args)
|
||||
def scenario_wrapper(request, _pytest_bdd_example):
|
||||
@pytest.mark.usefixtures(*func_args)
|
||||
def scenario_wrapper(request: FixtureRequest, _pytest_bdd_example: dict[str, str]) -> Any:
|
||||
scenario = templated_scenario.render(_pytest_bdd_example)
|
||||
_execute_scenario(feature, scenario, request)
|
||||
fixture_values = [request.getfixturevalue(arg) for arg in args]
|
||||
fixture_values = [request.getfixturevalue(arg) for arg in func_args]
|
||||
return fn(*fixture_values)
|
||||
|
||||
example_parametrizations = collect_example_parametrizations(templated_scenario)
|
||||
|
@ -192,14 +199,14 @@ def _get_scenario_decorator(
|
|||
|
||||
scenario_wrapper.__doc__ = f"{feature_name}: {scenario_name}"
|
||||
scenario_wrapper.__scenario__ = templated_scenario
|
||||
return scenario_wrapper
|
||||
return cast(Callable, scenario_wrapper)
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def collect_example_parametrizations(
|
||||
templated_scenario: "ScenarioTemplate",
|
||||
) -> "typing.Optional[typing.List[ParameterSet]]":
|
||||
templated_scenario: ScenarioTemplate,
|
||||
) -> list[ParameterSet] | None:
|
||||
# We need to evaluate these iterators and store them as lists, otherwise
|
||||
# we won't be able to do the cartesian product later (the second iterator will be consumed)
|
||||
contexts = list(templated_scenario.examples.as_contexts())
|
||||
|
@ -209,7 +216,9 @@ def collect_example_parametrizations(
|
|||
return [pytest.param(context, id="-".join(context.values())) for context in contexts]
|
||||
|
||||
|
||||
def scenario(feature_name: str, scenario_name: str, encoding: str = "utf-8", features_base_dir=None):
|
||||
def scenario(
|
||||
feature_name: str, scenario_name: str, encoding: str = "utf-8", features_base_dir=None
|
||||
) -> Callable[[Callable], Callable]:
|
||||
"""Scenario decorator.
|
||||
|
||||
:param str feature_name: Feature file name. Absolute or relative to the configured feature base path.
|
||||
|
@ -239,44 +248,46 @@ def scenario(feature_name: str, scenario_name: str, encoding: str = "utf-8", fea
|
|||
)
|
||||
|
||||
|
||||
def get_features_base_dir(caller_module_path):
|
||||
def get_features_base_dir(caller_module_path: str) -> str:
|
||||
default_base_dir = os.path.dirname(caller_module_path)
|
||||
return get_from_ini("bdd_features_base_dir", default_base_dir)
|
||||
|
||||
|
||||
def get_from_ini(key, default):
|
||||
def get_from_ini(key: str, default: str) -> str:
|
||||
"""Get value from ini config. Return default if value has not been set.
|
||||
|
||||
Use if the default value is dynamic. Otherwise set default on addini call.
|
||||
"""
|
||||
config = CONFIG_STACK[-1]
|
||||
value = config.getini(key)
|
||||
if not isinstance(value, str):
|
||||
raise TypeError(f"Expected a string for configuration option {value!r}, got a {type(value)} instead")
|
||||
return value if value != "" else default
|
||||
|
||||
|
||||
def make_python_name(string):
|
||||
def make_python_name(string: str) -> str:
|
||||
"""Make python attribute name out of a given string."""
|
||||
string = re.sub(PYTHON_REPLACE_REGEX, "", string.replace(" ", "_"))
|
||||
return re.sub(ALPHA_REGEX, "", string).lower()
|
||||
|
||||
|
||||
def make_python_docstring(string):
|
||||
def make_python_docstring(string: str) -> str:
|
||||
"""Make a python docstring literal out of a given string."""
|
||||
return '"""{}."""'.format(string.replace('"""', '\\"\\"\\"'))
|
||||
|
||||
|
||||
def make_string_literal(string):
|
||||
def make_string_literal(string: str) -> str:
|
||||
"""Make python string literal out of a given string."""
|
||||
return "'{}'".format(string.replace("'", "\\'"))
|
||||
|
||||
|
||||
def get_python_name_generator(name):
|
||||
def get_python_name_generator(name: str) -> Iterable[str]:
|
||||
"""Generate a sequence of suitable python names out of given arbitrary string name."""
|
||||
python_name = make_python_name(name)
|
||||
suffix = ""
|
||||
index = 0
|
||||
|
||||
def get_name():
|
||||
def get_name() -> str:
|
||||
return f"test_{python_name}{suffix}"
|
||||
|
||||
while True:
|
||||
|
@ -285,7 +296,7 @@ def get_python_name_generator(name):
|
|||
suffix = f"_{index}"
|
||||
|
||||
|
||||
def scenarios(*feature_paths, **kwargs):
|
||||
def scenarios(*feature_paths: str, **kwargs: Any) -> None:
|
||||
"""Parse features from the paths and put all found scenarios in the caller module.
|
||||
|
||||
:param *feature_paths: feature file paths to use for scenarios
|
||||
|
@ -316,7 +327,7 @@ def scenarios(*feature_paths, **kwargs):
|
|||
if (scenario_object.feature.filename, scenario_name) not in module_scenarios:
|
||||
|
||||
@scenario(feature.filename, scenario_name, **kwargs)
|
||||
def _scenario():
|
||||
def _scenario() -> None:
|
||||
pass # pragma: no cover
|
||||
|
||||
for test_name in get_python_name_generator(scenario_name):
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""pytest-bdd scripts."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os.path
|
||||
|
@ -11,14 +12,14 @@ from .generation import generate_code, parse_feature_files
|
|||
MIGRATE_REGEX = re.compile(r"\s?(\w+)\s=\sscenario\((.+)\)", flags=re.MULTILINE)
|
||||
|
||||
|
||||
def migrate_tests(args):
|
||||
def migrate_tests(args: argparse.Namespace) -> None:
|
||||
"""Migrate outdated tests to the most recent form."""
|
||||
path = args.path
|
||||
for file_path in glob2.iglob(os.path.join(os.path.abspath(path), "**", "*.py")):
|
||||
migrate_tests_in_file(file_path)
|
||||
|
||||
|
||||
def migrate_tests_in_file(file_path):
|
||||
def migrate_tests_in_file(file_path: str) -> None:
|
||||
"""Migrate all bdd-based tests in the given test file."""
|
||||
try:
|
||||
with open(file_path, "r+") as fd:
|
||||
|
@ -37,21 +38,21 @@ def migrate_tests_in_file(file_path):
|
|||
pass
|
||||
|
||||
|
||||
def check_existense(file_name):
|
||||
def check_existense(file_name: str) -> str:
|
||||
"""Check file or directory name for existence."""
|
||||
if not os.path.exists(file_name):
|
||||
raise argparse.ArgumentTypeError(f"{file_name} is an invalid file or directory name")
|
||||
return file_name
|
||||
|
||||
|
||||
def print_generated_code(args):
|
||||
def print_generated_code(args: argparse.Namespace) -> None:
|
||||
"""Print generated test code for the given filenames."""
|
||||
features, scenarios, steps = parse_feature_files(args.files)
|
||||
code = generate_code(features, scenarios, steps)
|
||||
print(code)
|
||||
|
||||
|
||||
def main():
|
||||
def main() -> None:
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser(prog="pytest-bdd")
|
||||
subparsers = parser.add_subparsers(help="sub-command help", dest="command")
|
||||
|
|
|
@ -34,16 +34,22 @@ def given_beautiful_article(article):
|
|||
pass
|
||||
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
import pytest
|
||||
from _pytest.fixtures import FixtureDef
|
||||
from _pytest.fixtures import FixtureDef, FixtureRequest
|
||||
|
||||
from .parsers import get_parser
|
||||
from .types import GIVEN, THEN, WHEN
|
||||
from .utils import get_caller_module_locals
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from typing import Any, Callable
|
||||
|
||||
def get_step_fixture_name(name, type_):
|
||||
|
||||
def get_step_fixture_name(name: str, type_: str) -> str:
|
||||
"""Get step fixture name.
|
||||
|
||||
:param name: string
|
||||
|
@ -54,7 +60,11 @@ def get_step_fixture_name(name, type_):
|
|||
return f"pytestbdd_{type_}_{name}"
|
||||
|
||||
|
||||
def given(name, converters=None, target_fixture=None):
|
||||
def given(
|
||||
name: Any,
|
||||
converters: dict[str, Callable] | None = None,
|
||||
target_fixture: str | None = None,
|
||||
) -> Callable:
|
||||
"""Given step decorator.
|
||||
|
||||
:param name: Step name or a parser object.
|
||||
|
@ -67,7 +77,7 @@ def given(name, converters=None, target_fixture=None):
|
|||
return _step_decorator(GIVEN, name, converters=converters, target_fixture=target_fixture)
|
||||
|
||||
|
||||
def when(name, converters=None, target_fixture=None):
|
||||
def when(name: Any, converters: dict[str, Callable] | None = None, target_fixture: str | None = None) -> Callable:
|
||||
"""When step decorator.
|
||||
|
||||
:param name: Step name or a parser object.
|
||||
|
@ -80,7 +90,7 @@ def when(name, converters=None, target_fixture=None):
|
|||
return _step_decorator(WHEN, name, converters=converters, target_fixture=target_fixture)
|
||||
|
||||
|
||||
def then(name, converters=None, target_fixture=None):
|
||||
def then(name: Any, converters: dict[str, Callable] | None = None, target_fixture: str | None = None) -> Callable:
|
||||
"""Then step decorator.
|
||||
|
||||
:param name: Step name or a parser object.
|
||||
|
@ -93,7 +103,12 @@ def then(name, converters=None, target_fixture=None):
|
|||
return _step_decorator(THEN, name, converters=converters, target_fixture=target_fixture)
|
||||
|
||||
|
||||
def _step_decorator(step_type, step_name, converters=None, target_fixture=None):
|
||||
def _step_decorator(
|
||||
step_type: str,
|
||||
step_name: Any,
|
||||
converters: dict[str, Callable] | None = None,
|
||||
target_fixture: str | None = None,
|
||||
) -> Callable:
|
||||
"""Step decorator for the type and the name.
|
||||
|
||||
:param str step_type: Step type (GIVEN, WHEN or THEN).
|
||||
|
@ -104,14 +119,14 @@ def _step_decorator(step_type, step_name, converters=None, target_fixture=None):
|
|||
:return: Decorator function for the step.
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
def decorator(func: Callable) -> Callable:
|
||||
step_func = func
|
||||
parser_instance = get_parser(step_name)
|
||||
parsed_step_name = parser_instance.name
|
||||
|
||||
step_func.__name__ = str(parsed_step_name)
|
||||
|
||||
def lazy_step_func():
|
||||
def lazy_step_func() -> Callable:
|
||||
return step_func
|
||||
|
||||
step_func.step_type = step_type
|
||||
|
@ -136,7 +151,7 @@ def _step_decorator(step_type, step_name, converters=None, target_fixture=None):
|
|||
return decorator
|
||||
|
||||
|
||||
def inject_fixture(request, arg, value):
|
||||
def inject_fixture(request: FixtureRequest, arg: str, value: Any) -> None:
|
||||
"""Inject fixture into pytest fixture request.
|
||||
|
||||
:param request: pytest fixture request
|
||||
|
@ -157,7 +172,7 @@ def inject_fixture(request, arg, value):
|
|||
old_fd = request._fixture_defs.get(arg)
|
||||
add_fixturename = arg not in request.fixturenames
|
||||
|
||||
def fin():
|
||||
def fin() -> None:
|
||||
request._fixturemanager._arg2fixturedefs[arg].remove(fd)
|
||||
request._fixture_defs[arg] = old_fd
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""Common type definitions."""
|
||||
from __future__ import annotations
|
||||
|
||||
FEATURE = "feature"
|
||||
SCENARIO_OUTLINE = "scenario outline"
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
"""Various utility functions."""
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import pickle
|
||||
import re
|
||||
|
@ -7,12 +9,15 @@ from inspect import getframeinfo, signature
|
|||
from sys import _getframe
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from typing import Any, Callable
|
||||
|
||||
from _pytest.config import Config
|
||||
from _pytest.pytester import RunResult
|
||||
|
||||
CONFIG_STACK = []
|
||||
CONFIG_STACK: list[Config] = []
|
||||
|
||||
|
||||
def get_args(func):
|
||||
def get_args(func: Callable) -> list[str]:
|
||||
"""Get a list of argument names for a function.
|
||||
|
||||
:param func: The function to inspect.
|
||||
|
@ -24,7 +29,7 @@ def get_args(func):
|
|||
return [param.name for param in params if param.kind == param.POSITIONAL_OR_KEYWORD]
|
||||
|
||||
|
||||
def get_caller_module_locals(depth=2):
|
||||
def get_caller_module_locals(depth: int = 2) -> dict[str, Any]:
|
||||
"""Get the caller module locals dictionary.
|
||||
|
||||
We use sys._getframe instead of inspect.stack(0) because the latter is way slower, since it iterates over
|
||||
|
@ -33,7 +38,7 @@ def get_caller_module_locals(depth=2):
|
|||
return _getframe(depth).f_locals
|
||||
|
||||
|
||||
def get_caller_module_path(depth=2):
|
||||
def get_caller_module_path(depth: int = 2) -> str:
|
||||
"""Get the caller module path.
|
||||
|
||||
We use sys._getframe instead of inspect.stack(0) because the latter is way slower, since it iterates over
|
||||
|
@ -47,7 +52,7 @@ _DUMP_START = "_pytest_bdd_>>>"
|
|||
_DUMP_END = "<<<_pytest_bdd_"
|
||||
|
||||
|
||||
def dump_obj(*objects):
|
||||
def dump_obj(*objects: Any) -> None:
|
||||
"""Dump objects to stdout so that they can be inspected by the test suite."""
|
||||
for obj in objects:
|
||||
dump = pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL)
|
||||
|
@ -55,7 +60,7 @@ def dump_obj(*objects):
|
|||
print(f"{_DUMP_START}{encoded}{_DUMP_END}")
|
||||
|
||||
|
||||
def collect_dumped_objects(result: "RunResult"):
|
||||
def collect_dumped_objects(result: RunResult) -> list:
|
||||
"""Parse all the objects dumped with `dump_object` from the result.
|
||||
|
||||
Note: You must run the result with output to stdout enabled.
|
||||
|
|
|
@ -34,10 +34,14 @@ install_requires =
|
|||
py
|
||||
pytest>=5.0
|
||||
|
||||
tests_require = tox
|
||||
packages = pytest_bdd
|
||||
include_package_data = True
|
||||
|
||||
[options.extras_require]
|
||||
testing =
|
||||
tox
|
||||
mypy==0.910
|
||||
|
||||
[options.entry_points]
|
||||
pytest11 =
|
||||
pytest-bdd = pytest_bdd.plugin
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
"""Test cucumber json output."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os.path
|
||||
import textwrap
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.pytester import RunResult, Testdir
|
||||
|
||||
|
||||
def runandparse(testdir, *args):
|
||||
def runandparse(testdir: Testdir, *args: Any) -> tuple[RunResult, list[dict[str, Any]]]:
|
||||
"""Run tests in testdir and parse json output."""
|
||||
resultpath = testdir.tmpdir.join("cucumber.json")
|
||||
result = testdir.runpytest(f"--cucumberjson={resultpath}", "-s", *args)
|
||||
|
@ -16,10 +22,10 @@ def runandparse(testdir, *args):
|
|||
class OfType:
|
||||
"""Helper object to help compare object type to initialization type"""
|
||||
|
||||
def __init__(self, type=None):
|
||||
def __init__(self, type: type = None) -> None:
|
||||
self.type = type
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return isinstance(other, self.type) if self.type else True
|
||||
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import textwrap
|
||||
|
||||
import pytest
|
||||
|
@ -43,7 +45,7 @@ def test_default_output_should_be_the_same_as_regular_terminal_reporter(testdir)
|
|||
regular.assert_outcomes(passed=1, failed=0)
|
||||
gherkin.assert_outcomes(passed=1, failed=0)
|
||||
|
||||
def parse_lines(lines):
|
||||
def parse_lines(lines: list[str]) -> list[str]:
|
||||
return [line for line in lines if not line.startswith("===")]
|
||||
|
||||
assert all(l1 == l2 for l1, l2 in zip(parse_lines(regular.stdout.lines), parse_lines(gherkin.stdout.lines)))
|
||||
|
|
|
@ -7,10 +7,10 @@ import pytest
|
|||
class OfType:
|
||||
"""Helper object comparison to which is always 'equal'."""
|
||||
|
||||
def __init__(self, type=None):
|
||||
def __init__(self, type: type = None) -> None:
|
||||
self.type = type
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return isinstance(other, self.type) if self.type else True
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
import pytest
|
||||
from packaging.utils import Version
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from _pytest.pytester import RunResult
|
||||
|
||||
PYTEST_VERSION = Version(pytest.__version__)
|
||||
PYTEST_6 = PYTEST_VERSION >= Version("6")
|
||||
|
||||
|
@ -8,32 +15,32 @@ PYTEST_6 = PYTEST_VERSION >= Version("6")
|
|||
if PYTEST_6:
|
||||
|
||||
def assert_outcomes(
|
||||
result,
|
||||
passed=0,
|
||||
skipped=0,
|
||||
failed=0,
|
||||
errors=0,
|
||||
xpassed=0,
|
||||
xfailed=0,
|
||||
):
|
||||
result: RunResult,
|
||||
passed: int = 0,
|
||||
skipped: int = 0,
|
||||
failed: int = 0,
|
||||
errors: int = 0,
|
||||
xpassed: int = 0,
|
||||
xfailed: int = 0,
|
||||
) -> None:
|
||||
"""Compatibility function for result.assert_outcomes"""
|
||||
return result.assert_outcomes(
|
||||
result.assert_outcomes(
|
||||
errors=errors, passed=passed, skipped=skipped, failed=failed, xpassed=xpassed, xfailed=xfailed
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
def assert_outcomes(
|
||||
result,
|
||||
passed=0,
|
||||
skipped=0,
|
||||
failed=0,
|
||||
errors=0,
|
||||
xpassed=0,
|
||||
xfailed=0,
|
||||
):
|
||||
result: RunResult,
|
||||
passed: int = 0,
|
||||
skipped: int = 0,
|
||||
failed: int = 0,
|
||||
errors: int = 0,
|
||||
xpassed: int = 0,
|
||||
xfailed: int = 0,
|
||||
) -> None:
|
||||
"""Compatibility function for result.assert_outcomes"""
|
||||
return result.assert_outcomes(
|
||||
result.assert_outcomes(
|
||||
error=errors, # Pytest < 6 uses the singular form
|
||||
passed=passed,
|
||||
skipped=skipped,
|
||||
|
|
Loading…
Reference in New Issue