Merge pull request #505 from pytest-dev/add-type-annotations

Add type annotations to the codebase.
This commit is contained in:
Alessio Bogon 2022-03-05 16:49:08 +01:00 committed by GitHub
commit 02bf62014f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 435 additions and 265 deletions

View File

@ -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

View File

@ -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]

View File

@ -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.

View File

@ -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

View File

@ -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"]

View File

@ -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}")

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import pytest
"""Pytest-bdd pytest hooks."""

View File

@ -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.

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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):

View File

@ -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")

View File

@ -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

View File

@ -1,4 +1,5 @@
"""Common type definitions."""
from __future__ import annotations
FEATURE = "feature"
SCENARIO_OUTLINE = "scenario outline"

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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)))

View File

@ -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

View File

@ -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,

View File

@ -34,6 +34,12 @@ commands = {env:_PYTEST_CMD:pytest} {env:_PYTEST_MORE_ARGS:} {posargs:-vvl}
deps = black==22.1.0
commands = black --check --verbose setup.py docs pytest_bdd tests
[testenv:mypy]
deps =
mypy==0.931
types-setuptools
commands = mypy
[gh-actions]
python =
3.7: py37