Skip to content

Commit 02bf620

Browse files
authored
Merge pull request #505 from pytest-dev/add-type-annotations
Add type annotations to the codebase.
2 parents ddcbeb5 + e378ccb commit 02bf620

26 files changed

+438
-268
lines changed

.github/workflows/main.yml

+5-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
runs-on: ubuntu-latest
1212
strategy:
1313
matrix:
14-
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]
14+
python-version: ["3.7", "3.8", "3.9", "3.10"]
1515

1616
steps:
1717
- uses: actions/checkout@v2
@@ -24,6 +24,10 @@ jobs:
2424
python -m pip install --upgrade pip
2525
pip install -U setuptools
2626
pip install tox tox-gh-actions codecov
27+
- name: Type checking
28+
continue-on-error: true
29+
run: |
30+
tox -e mypy
2731
- name: Test with tox
2832
run: |
2933
tox

.pre-commit-config.yaml

+7-1
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,10 @@ repos:
2222
rev: v2.31.0
2323
hooks:
2424
- id: pyupgrade
25-
args: [--py37-plus]
25+
args: ["--py37-plus"]
26+
# TODO: Enable mypy checker when the checks succeed
27+
#- repo: https://github.com/pre-commit/mirrors-mypy
28+
# rev: v0.931
29+
# hooks:
30+
# - id: mypy
31+
# additional_dependencies: [types-setuptools]

CHANGES.rst

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ This release introduces breaking changes in order to be more in line with the of
1313
- Drop support of python 3.6, pytest 4 (elchupanebrej)
1414
- 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)
1515
- Scenario outlines unused example parameter validation is removed (olegpidsadnyi)
16+
- Add type decorations
17+
- ``pytest_bdd.parsers.StepParser`` now is an Abstract Base Class. Subclasses must make sure to implement the abstract methods.
1618

1719

1820

pyproject.toml

+10
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,13 @@ target-version = ["py37", "py38", "py39", "py310"]
1010
profile = "black"
1111
line_length = 120
1212
multi_line_output = 3
13+
14+
[tool.mypy]
15+
python_version = "3.7"
16+
warn_return_any = true
17+
warn_unused_configs = true
18+
files = "pytest_bdd/**/*.py"
19+
20+
[[tool.mypy.overrides]]
21+
module = ["parse", "parse_type", "glob2"]
22+
ignore_missing_imports = true

pytest_bdd/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
"""pytest-bdd public API."""
2+
from __future__ import annotations
23

34
from pytest_bdd.scenario import scenario, scenarios
45
from pytest_bdd.steps import given, then, when
56

67
__version__ = "6.0.0"
78

8-
__all__ = [given.__name__, when.__name__, then.__name__, scenario.__name__, scenarios.__name__]
9+
__all__ = ["given", "when", "then", "scenario", "scenarios"]

pytest_bdd/cucumber_json.py

+24-17
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
"""Cucumber json output formatter."""
2+
from __future__ import annotations
23

34
import json
45
import math
56
import os
67
import time
8+
import typing
79

10+
if typing.TYPE_CHECKING:
11+
from typing import Any
812

9-
def add_options(parser):
13+
from _pytest.config import Config
14+
from _pytest.config.argparsing import Parser
15+
from _pytest.reports import TestReport
16+
from _pytest.terminal import TerminalReporter
17+
18+
19+
def add_options(parser: Parser) -> None:
1020
"""Add pytest-bdd options."""
1121
group = parser.getgroup("bdd", "Cucumber JSON")
1222
group.addoption(
@@ -20,15 +30,15 @@ def add_options(parser):
2030
)
2131

2232

23-
def configure(config):
33+
def configure(config: Config) -> None:
2434
cucumber_json_path = config.option.cucumber_json_path
2535
# prevent opening json log on worker nodes (xdist)
2636
if cucumber_json_path and not hasattr(config, "workerinput"):
2737
config._bddcucumberjson = LogBDDCucumberJSON(cucumber_json_path)
2838
config.pluginmanager.register(config._bddcucumberjson)
2939

3040

31-
def unconfigure(config):
41+
def unconfigure(config: Config) -> None:
3242
xml = getattr(config, "_bddcucumberjson", None)
3343
if xml is not None:
3444
del config._bddcucumberjson
@@ -39,22 +49,19 @@ class LogBDDCucumberJSON:
3949

4050
"""Logging plugin for cucumber like json output."""
4151

42-
def __init__(self, logfile):
52+
def __init__(self, logfile: str) -> None:
4353
logfile = os.path.expanduser(os.path.expandvars(logfile))
4454
self.logfile = os.path.normpath(os.path.abspath(logfile))
45-
self.features = {}
46-
47-
def append(self, obj):
48-
self.features[-1].append(obj)
55+
self.features: dict[str, dict] = {}
4956

50-
def _get_result(self, step, report, error_message=False):
57+
def _get_result(self, step: dict[str, Any], report: TestReport, error_message: bool = False) -> dict[str, Any]:
5158
"""Get scenario test run result.
5259
5360
:param step: `Step` step we get result for
5461
:param report: pytest `Report` object
5562
:return: `dict` in form {"status": "<passed|failed|skipped>", ["error_message": "<error_message>"]}
5663
"""
57-
result = {}
64+
result: dict[str, Any] = {}
5865
if report.passed or not step["failed"]: # ignore setup/teardown
5966
result = {"status": "passed"}
6067
elif report.failed and step["failed"]:
@@ -64,7 +71,7 @@ def _get_result(self, step, report, error_message=False):
6471
result["duration"] = int(math.floor((10**9) * step["duration"])) # nanosec
6572
return result
6673

67-
def _serialize_tags(self, item):
74+
def _serialize_tags(self, item: dict[str, Any]) -> list[dict[str, Any]]:
6875
"""Serialize item's tags.
6976
7077
:param item: json-serialized `Scenario` or `Feature`.
@@ -78,7 +85,7 @@ def _serialize_tags(self, item):
7885
"""
7986
return [{"name": tag, "line": item["line_number"] - 1} for tag in item["tags"]]
8087

81-
def pytest_runtest_logreport(self, report):
88+
def pytest_runtest_logreport(self, report: TestReport) -> None:
8289
try:
8390
scenario = report.scenario
8491
except AttributeError:
@@ -89,7 +96,7 @@ def pytest_runtest_logreport(self, report):
8996
# skip if there isn't a result or scenario has no steps
9097
return
9198

92-
def stepmap(step):
99+
def stepmap(step: dict[str, Any]) -> dict[str, Any]:
93100
error_message = False
94101
if step["failed"] and not scenario.setdefault("failed", False):
95102
scenario["failed"] = True
@@ -130,12 +137,12 @@ def stepmap(step):
130137
}
131138
)
132139

133-
def pytest_sessionstart(self):
140+
def pytest_sessionstart(self) -> None:
134141
self.suite_start_time = time.time()
135142

136-
def pytest_sessionfinish(self):
143+
def pytest_sessionfinish(self) -> None:
137144
with open(self.logfile, "w", encoding="utf-8") as logfile:
138145
logfile.write(json.dumps(list(self.features.values())))
139146

140-
def pytest_terminal_summary(self, terminalreporter):
141-
terminalreporter.write_sep("-", "generated json file: %s" % (self.logfile))
147+
def pytest_terminal_summary(self, terminalreporter: TerminalReporter) -> None:
148+
terminalreporter.write_sep("-", f"generated json file: {self.logfile}")

pytest_bdd/exceptions.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""pytest-bdd Exceptions."""
2+
from __future__ import annotations
23

34

45
class ScenarioIsDecoratorOnly(Exception):
@@ -30,6 +31,6 @@ class FeatureError(Exception):
3031

3132
message = "{0}.\nLine number: {1}.\nLine: {2}.\nFile: {3}"
3233

33-
def __str__(self):
34+
def __str__(self) -> str:
3435
"""String representation."""
3536
return self.message.format(*self.args)

pytest_bdd/feature.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,16 @@
2323
:note: There are no multiline steps, the description of the step must fit in
2424
one line.
2525
"""
26+
from __future__ import annotations
27+
2628
import os.path
27-
import typing
2829

2930
import glob2
3031

3132
from .parser import Feature, parse_feature
3233

3334
# Global features dictionary
34-
features: typing.Dict[str, Feature] = {}
35+
features: dict[str, Feature] = {}
3536

3637

3738
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
5657
return feature
5758

5859

59-
def get_features(paths: typing.List[str], **kwargs) -> typing.List[Feature]:
60+
def get_features(paths: list[str], **kwargs) -> list[Feature]:
6061
"""Get features for given paths.
6162
6263
:param list paths: `list` of paths (file or dirs)

pytest_bdd/generation.py

+28-17
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
"""pytest-bdd missing test code generation."""
2+
from __future__ import annotations
23

34
import itertools
45
import os.path
6+
from typing import TYPE_CHECKING, cast
57

68
import py
79
from mako.lookup import TemplateLookup
@@ -11,10 +13,21 @@
1113
from .steps import get_step_fixture_name
1214
from .types import STEP_TYPES
1315

16+
if TYPE_CHECKING:
17+
from typing import Any, Sequence
18+
19+
from _pytest.config import Config
20+
from _pytest.config.argparsing import Parser
21+
from _pytest.fixtures import FixtureDef, FixtureManager
22+
from _pytest.main import Session
23+
from _pytest.python import Function
24+
25+
from .parser import Feature, ScenarioTemplate, Step
26+
1427
template_lookup = TemplateLookup(directories=[os.path.join(os.path.dirname(__file__), "templates")])
1528

1629

17-
def add_options(parser):
30+
def add_options(parser: Parser) -> None:
1831
"""Add pytest-bdd options."""
1932
group = parser.getgroup("bdd", "Generation")
2033

@@ -35,34 +48,36 @@ def add_options(parser):
3548
)
3649

3750

38-
def cmdline_main(config):
51+
def cmdline_main(config: Config) -> int | None:
3952
"""Check config option to show missing code."""
4053
if config.option.generate_missing:
4154
return show_missing_code(config)
55+
return None # Make mypy happy
4256

4357

44-
def generate_code(features, scenarios, steps):
58+
def generate_code(features: list[Feature], scenarios: list[ScenarioTemplate], steps: list[Step]) -> str:
4559
"""Generate test code for the given filenames."""
4660
grouped_steps = group_steps(steps)
4761
template = template_lookup.get_template("test.py.mak")
48-
return template.render(
62+
code = template.render(
4963
features=features,
5064
scenarios=scenarios,
5165
steps=grouped_steps,
5266
make_python_name=make_python_name,
5367
make_python_docstring=make_python_docstring,
5468
make_string_literal=make_string_literal,
5569
)
70+
return cast(str, code)
5671

5772

58-
def show_missing_code(config):
73+
def show_missing_code(config: Config) -> int:
5974
"""Wrap pytest session to show missing code."""
6075
from _pytest.main import wrap_session
6176

6277
return wrap_session(config, _show_missing_code_main)
6378

6479

65-
def print_missing_code(scenarios, steps):
80+
def print_missing_code(scenarios: list[ScenarioTemplate], steps: list[Step]) -> None:
6681
"""Print missing code with TerminalWriter."""
6782
tw = py.io.TerminalWriter()
6883
scenario = step = None
@@ -108,14 +123,10 @@ def print_missing_code(scenarios, steps):
108123
tw.write(code)
109124

110125

111-
def _find_step_fixturedef(fixturemanager, item, name, type_):
112-
"""Find step fixturedef.
113-
114-
:param request: PyTest Item object.
115-
:param step: `Step`.
116-
117-
:return: Step function.
118-
"""
126+
def _find_step_fixturedef(
127+
fixturemanager: FixtureManager, item: Function, name: str, type_: str
128+
) -> Sequence[FixtureDef[Any]] | None:
129+
"""Find step fixturedef."""
119130
step_fixture_name = get_step_fixture_name(name, type_)
120131
fixturedefs = fixturemanager.getfixturedefs(step_fixture_name, item.nodeid)
121132
if fixturedefs is not None:
@@ -127,7 +138,7 @@ def _find_step_fixturedef(fixturemanager, item, name, type_):
127138
return None
128139

129140

130-
def parse_feature_files(paths, **kwargs):
141+
def parse_feature_files(paths: list[str], **kwargs: Any) -> tuple[list[Feature], list[ScenarioTemplate], list[Step]]:
131142
"""Parse feature files of given paths.
132143
133144
:param paths: `list` of paths (file or dirs)
@@ -146,7 +157,7 @@ def parse_feature_files(paths, **kwargs):
146157
return features, scenarios, steps
147158

148159

149-
def group_steps(steps):
160+
def group_steps(steps: list[Step]) -> list[Step]:
150161
"""Group steps by type."""
151162
steps = sorted(steps, key=lambda step: step.type)
152163
seen_steps = set()
@@ -161,7 +172,7 @@ def group_steps(steps):
161172
return grouped_steps
162173

163174

164-
def _show_missing_code_main(config, session):
175+
def _show_missing_code_main(config: Config, session: Session) -> None:
165176
"""Preparing fixture duplicates for output."""
166177
tw = py.io.TerminalWriter()
167178
session.perform_collect()

pytest_bdd/gherkin_terminal_reporter.py

+17-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
1+
from __future__ import annotations
2+
3+
import typing
4+
15
from _pytest.terminal import TerminalReporter
26

7+
if typing.TYPE_CHECKING:
8+
from typing import Any
9+
10+
from _pytest.config import Config
11+
from _pytest.config.argparsing import Parser
12+
from _pytest.reports import TestReport
13+
314

4-
def add_options(parser):
15+
def add_options(parser: Parser) -> None:
516
group = parser.getgroup("terminal reporting", "reporting", after="general")
617
group._addoption(
718
"--gherkin-terminal-reporter",
@@ -12,7 +23,7 @@ def add_options(parser):
1223
)
1324

1425

15-
def configure(config):
26+
def configure(config: Config) -> None:
1627
if config.option.gherkin_terminal_reporter:
1728
# Get the standard terminal reporter plugin and replace it with our
1829
current_reporter = config.pluginmanager.getplugin("terminalreporter")
@@ -33,17 +44,17 @@ def configure(config):
3344

3445

3546
class GherkinTerminalReporter(TerminalReporter):
36-
def __init__(self, config):
47+
def __init__(self, config: Config) -> None:
3748
super().__init__(config)
3849

39-
def pytest_runtest_logreport(self, report):
50+
def pytest_runtest_logreport(self, report: TestReport) -> Any:
4051
rep = report
4152
res = self.config.hook.pytest_report_teststatus(report=rep, config=self.config)
4253
cat, letter, word = res
4354

4455
if not letter and not word:
4556
# probably passed setup/teardown
46-
return
57+
return None
4758

4859
if isinstance(word, tuple):
4960
word, word_markup = word
@@ -88,3 +99,4 @@ def pytest_runtest_logreport(self, report):
8899
else:
89100
return super().pytest_runtest_logreport(rep)
90101
self.stats.setdefault(cat, []).append(rep)
102+
return None

0 commit comments

Comments
 (0)