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