diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index aa159eb9a..96b4e3599 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,33 +1,16 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
-- repo: https://github.com/psf/black
- # If you update the version here, also update it in tox.ini (py*-pytestlatest-linters)
- rev: 24.10.0
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: cef0300fd0fc4d2a87a85fa2093c6b283ea36f4b # frozen: v5.0.0
hooks:
- - id: black
-- repo: https://github.com/pycqa/isort
- rev: 5.13.2
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
+ - id: check-yaml
+ - id: check-added-large-files
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: f0b5944bef86f50d875305821a0ab0d8c601e465 # frozen: v0.8.4
hooks:
- - id: isort
- name: isort (python)
-- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v5.0.0
- hooks:
- - id: trailing-whitespace
- - id: end-of-file-fixer
- - id: check-yaml
- - id: check-added-large-files
-- repo: https://github.com/asottile/pyupgrade
- rev: v3.19.0
- hooks:
- - id: pyupgrade
- args: ["--py39-plus"]
-- repo: https://github.com/pycqa/flake8
- rev: "7.1.1"
- hooks:
- - id: flake8
- additional_dependencies: [
- "flake8-pyproject",
- "flake8-bugbear",
- ]
+ - id: ruff
+ args: [ --fix ]
+ - id: ruff-format
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 000000000..5d21c814d
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,5 @@
+{
+ "recommendations": [
+ "charliermarsh.ruff"
+ ]
+}
diff --git a/docs/conf.py b/docs/conf.py
index b9dbef4e7..d19163f1a 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -5,6 +5,7 @@
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
+from __future__ import annotations
from importlib import metadata as _metadata
diff --git a/pyproject.toml b/pyproject.toml
index f196671bb..952f4a192 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -57,21 +57,29 @@ sphinx-autobuild = "*"
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
-[tool.black]
+[tool.ruff]
line-length = 120
-target-version = ["py39", "py310", "py311", "py312", "py313"]
-
-[tool.flake8]
-# E1: indentation: already covered by `black`
-# E2: whitespace: already covered by `black`
-# E3: blank line: already covered by `black`
-# E501: line length: already covered by `black`
-extend-ignore = "E1,E2,E3,E501"
-
-[tool.isort]
-profile = "black"
-line_length = 120
-multi_line_output = 3
+target-version = "py39"
+lint.select = [
+ "B", # flake8-bugbear
+ "BLE", # flake8-blind-except
+ "C4", # flake8-comprehensions
+ "E4", # pycodestyle - error - import
+ "E7", # pycodestyle - error - statement
+ "E9", # pycodestyle - error - runtime
+ "F", # pyflakes
+ "I", # isort
+ "ISC", # flake8-implicit-str-concat
+ "PERF", # perflint
+ "UP", # pyupgrade
+]
+lint.ignore = [
+ # Covered by formatter
+ "ISC001" # single-line-implicit-string-concatenation
+]
+lint.isort.required-imports = [
+ "from __future__ import annotations",
+]
[tool.coverage.report]
exclude_lines = [
diff --git a/src/pytest_bdd/gherkin_terminal_reporter.py b/src/pytest_bdd/gherkin_terminal_reporter.py
index 264aea2d9..e18719c17 100644
--- a/src/pytest_bdd/gherkin_terminal_reporter.py
+++ b/src/pytest_bdd/gherkin_terminal_reporter.py
@@ -31,10 +31,9 @@ def configure(config: Config) -> None:
raise Exception(
"gherkin-terminal-reporter is not compatible with any other terminal reporter."
"You can use only one terminal reporter."
- "Currently '{0}' is used."
- "Please decide to use one by deactivating {0} or gherkin-terminal-reporter.".format(
- current_reporter.__class__
- )
+ f"Currently '{current_reporter.__class__}' is used."
+ f"Please decide to use one by deactivating {current_reporter.__class__} "
+ "or gherkin-terminal-reporter."
)
gherkin_reporter = GherkinTerminalReporter(config)
config.pluginmanager.unregister(current_reporter)
diff --git a/src/pytest_bdd/parser.py b/src/pytest_bdd/parser.py
index a1b6eb264..e2be84829 100644
--- a/src/pytest_bdd/parser.py
+++ b/src/pytest_bdd/parser.py
@@ -10,14 +10,12 @@
from .exceptions import StepError
from .gherkin_parser import Background as GherkinBackground
-from .gherkin_parser import DataTable
+from .gherkin_parser import DataTable, GherkinDocument, get_gherkin_document
from .gherkin_parser import Feature as GherkinFeature
-from .gherkin_parser import GherkinDocument
from .gherkin_parser import Rule as GherkinRule
from .gherkin_parser import Scenario as GherkinScenario
from .gherkin_parser import Step as GherkinStep
from .gherkin_parser import Tag as GherkinTag
-from .gherkin_parser import get_gherkin_document
from .types import STEP_TYPE_BY_PARSER_KEYWORD
PARAM_RE = re.compile(r"<(.+?)>")
@@ -48,7 +46,7 @@ def get_tag_names(tag_data: list[GherkinTag]) -> set[str]:
"""Extract tag names from tag data.
Args:
- tag_data (List[dict]): The tag data to extract names from.
+ tag_data (list[dict]): The tag data to extract names from.
Returns:
set[str]: A set of tag names.
@@ -66,7 +64,7 @@ class Feature:
rel_filename (str): The relative path of the feature file.
name (str): The name of the feature.
tags (set[str]): A set of tags associated with the feature.
- background (Optional[Background]): The background steps for the feature, if any.
+ background (Background | None): The background steps for the feature, if any.
line_number (int): The line number where the feature starts in the file.
description (str): The description of the feature.
"""
@@ -88,10 +86,10 @@ class Examples:
"""Represents examples used in scenarios for parameterization.
Attributes:
- line_number (Optional[int]): The line number where the examples start.
- name (Optional[str]): The name of the examples.
- example_params (List[str]): The names of the parameters for the examples.
- examples (List[Sequence[str]]): The list of example rows.
+ line_number (int | None): The line number where the examples start.
+ name (str | None): The name of the examples.
+ example_params (list[str]): The names of the parameters for the examples.
+ examples (list[Sequence[str]]): The list of example rows.
"""
line_number: int | None = None
@@ -154,11 +152,11 @@ class ScenarioTemplate:
name (str): The name of the scenario.
line_number (int): The line number where the scenario starts in the file.
templated (bool): Whether the scenario is templated.
- description (Optional[str]): The description of the scenario.
+ description (str | None): The description of the scenario.
tags (set[str]): A set of tags associated with the scenario.
- _steps (List[Step]): The list of steps in the scenario (internal use only).
- examples (Optional[Examples]): The examples used for parameterization in the scenario.
- rule (Optional[Rule]): The rule to which the scenario may belong (None = no rule).
+ _steps (list[Step]): The list of steps in the scenario (internal use only).
+ examples (Examples | None): The examples used for parameterization in the scenario.
+ rule (Rule | None): The rule to which the scenario may belong (None = no rule).
"""
feature: Feature
@@ -197,7 +195,7 @@ def steps(self) -> list[Step]:
"""Get all steps for the scenario, including background steps.
Returns:
- List[Step]: A list of steps, including any background steps from the feature.
+ list[Step]: A list of steps, including any background steps from the feature.
"""
return self.all_background_steps + self._steps
@@ -244,8 +242,8 @@ class Scenario:
keyword (str): The keyword used to define the scenario.
name (str): The name of the scenario.
line_number (int): The line number where the scenario starts in the file.
- steps (List[Step]): The list of steps in the scenario.
- description (Optional[str]): The description of the scenario.
+ steps (list[Step]): The list of steps in the scenario.
+ description (str | None): The description of the scenario.
tags (set[str]): A set of tags associated with the scenario.
"""
@@ -270,8 +268,8 @@ class Step:
indent (int): The indentation level of the step.
keyword (str): The keyword used for the step (e.g., 'Given', 'When', 'Then').
failed (bool): Whether the step has failed (internal use only).
- scenario (Optional[ScenarioTemplate]): The scenario to which this step belongs (internal use only).
- background (Optional[Background]): The background to which this step belongs (internal use only).
+ scenario (ScenarioTemplate | None): The scenario to which this step belongs (internal use only).
+ background (Background | None): The background to which this step belongs (internal use only).
"""
type: str
@@ -346,7 +344,7 @@ class Background:
Attributes:
line_number (int): The line number where the background starts in the file.
- steps (List[Step]): The list of steps in the background.
+ steps (list[Step]): The list of steps in the background.
"""
line_number: int
@@ -371,7 +369,7 @@ class FeatureParser:
encoding (str): File encoding of the feature file to parse.
"""
- def __init__(self, basedir: str, filename: str, encoding: str = "utf-8"):
+ def __init__(self, basedir: str, filename: str, encoding: str = "utf-8") -> None:
self.abs_filename = os.path.abspath(os.path.join(basedir, filename))
self.rel_filename = os.path.join(os.path.basename(basedir), filename)
self.encoding = encoding
@@ -380,10 +378,10 @@ def parse_steps(self, steps_data: list[GherkinStep]) -> list[Step]:
"""Parse a list of step data into Step objects.
Args:
- steps_data (List[dict]): The list of step data.
+ steps_data (list[dict]): The list of step data.
Returns:
- List[Step]: A list of Step objects.
+ list[Step]: A list of Step objects.
"""
if not steps_data:
@@ -423,7 +421,7 @@ def parse_scenario(
Args:
scenario_data (dict): The dictionary containing scenario data.
feature (Feature): The feature to which this scenario belongs.
- rule (Optional[Rule]): The rule to which this scenario may belong. (None = no rule)
+ rule (Rule | None): The rule to which this scenario may belong. (None = no rule)
Returns:
ScenarioTemplate: A ScenarioTemplate object representing the parsed scenario.
diff --git a/src/pytest_bdd/scenario.py b/src/pytest_bdd/scenario.py
index e8e842777..1d98ebc55 100644
--- a/src/pytest_bdd/scenario.py
+++ b/src/pytest_bdd/scenario.py
@@ -188,9 +188,9 @@ def parse_step_arguments(step: Step, context: StepFunctionContext) -> dict[str,
"""Parse step arguments."""
parsed_args = context.parser.parse_arguments(step.name)
- assert parsed_args is not None, (
- f"Unexpected `NoneType` returned from " f"parse_arguments(...) in parser: {context.parser!r}"
- )
+ assert (
+ parsed_args is not None
+ ), f"Unexpected `NoneType` returned from parse_arguments(...) in parser: {context.parser!r}"
reserved_args = set(parsed_args.keys()) & STEP_ARGUMENTS_RESERVED_NAMES
if reserved_args:
@@ -386,7 +386,7 @@ def scenario(
feature_name = feature.name or "[Empty]"
raise exceptions.ScenarioNotFound(
f'Scenario "{scenario_name}" in feature "{feature_name}" in {feature.filename} is not found.'
- )
+ ) from None
return _get_scenario_decorator(
feature=feature, feature_name=feature_name, templated_scenario=scenario, scenario_name=scenario_name
diff --git a/tests/args/cfparse/test_args.py b/tests/args/cfparse/test_args.py
index 3b731f588..4c1b51a34 100644
--- a/tests/args/cfparse/test_args.py
+++ b/tests/args/cfparse/test_args.py
@@ -1,5 +1,7 @@
"""Step arguments tests."""
+from __future__ import annotations
+
import textwrap
diff --git a/tests/args/parse/test_args.py b/tests/args/parse/test_args.py
index e02ea18c4..9591162d1 100644
--- a/tests/args/parse/test_args.py
+++ b/tests/args/parse/test_args.py
@@ -1,5 +1,7 @@
"""Step arguments tests."""
+from __future__ import annotations
+
import textwrap
diff --git a/tests/args/regex/test_args.py b/tests/args/regex/test_args.py
index cce2d599e..d7b3a1b7b 100644
--- a/tests/args/regex/test_args.py
+++ b/tests/args/regex/test_args.py
@@ -1,5 +1,7 @@
"""Step arguments tests."""
+from __future__ import annotations
+
import textwrap
diff --git a/tests/args/test_common.py b/tests/args/test_common.py
index 9192e8128..273f7f497 100644
--- a/tests/args/test_common.py
+++ b/tests/args/test_common.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import textwrap
from pytest_bdd.utils import collect_dumped_objects
diff --git a/tests/conftest.py b/tests/conftest.py
index f1d2199cd..bd4ea9a83 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
pytest_plugins = "pytester"
diff --git a/tests/datatable/test_datatable.py b/tests/datatable/test_datatable.py
index 47a04d119..606ec32ad 100644
--- a/tests/datatable/test_datatable.py
+++ b/tests/datatable/test_datatable.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import textwrap
from src.pytest_bdd.utils import collect_dumped_objects
diff --git a/tests/feature/test_alias.py b/tests/feature/test_alias.py
index ce2b68cf5..97e015f02 100644
--- a/tests/feature/test_alias.py
+++ b/tests/feature/test_alias.py
@@ -1,5 +1,7 @@
"""Test step alias when decorated multiple times."""
+from __future__ import annotations
+
import textwrap
diff --git a/tests/feature/test_background.py b/tests/feature/test_background.py
index d541e965f..86428bb87 100644
--- a/tests/feature/test_background.py
+++ b/tests/feature/test_background.py
@@ -1,5 +1,7 @@
"""Test feature background."""
+from __future__ import annotations
+
import textwrap
FEATURE = '''\
diff --git a/tests/feature/test_description.py b/tests/feature/test_description.py
index c1426a286..b02c125b7 100644
--- a/tests/feature/test_description.py
+++ b/tests/feature/test_description.py
@@ -1,5 +1,7 @@
"""Test descriptions."""
+from __future__ import annotations
+
import textwrap
diff --git a/tests/feature/test_feature_base_dir.py b/tests/feature/test_feature_base_dir.py
index 669571c74..514b554f0 100644
--- a/tests/feature/test_feature_base_dir.py
+++ b/tests/feature/test_feature_base_dir.py
@@ -1,5 +1,7 @@
"""Test feature base dir."""
+from __future__ import annotations
+
import os
import pytest
@@ -59,12 +61,10 @@ def test_feature_path_by_param_ok(pytester, base_dir):
def prepare_testdir(pytester, ini_base_dir):
pytester.makeini(
- """
+ f"""
[pytest]
- bdd_features_base_dir={}
- """.format(
- ini_base_dir
- )
+ bdd_features_base_dir={ini_base_dir}
+ """
)
feature_file = pytester.mkdir("features").joinpath("steps.feature")
@@ -77,7 +77,7 @@ def prepare_testdir(pytester, ini_base_dir):
)
pytester.makepyfile(
- """
+ f"""
import os.path
import pytest
@@ -103,7 +103,7 @@ def test_not_found_by_ini(scenario_name, multiple):
scenarios(FEATURE)
else:
scenario(FEATURE, scenario_name)
- assert os.path.abspath(os.path.join('{}', FEATURE)) in str(exc.value)
+ assert os.path.abspath(os.path.join('{ini_base_dir}', FEATURE)) in str(exc.value)
@pytest.mark.parametrize(
@@ -145,7 +145,5 @@ def test_ok_by_param(scenario_name, multiple):
else:
scenario(FEATURE, scenario_name, features_base_dir='features')
- """.format(
- ini_base_dir
- )
+ """
)
diff --git a/tests/feature/test_gherkin_terminal_reporter.py b/tests/feature/test_gherkin_terminal_reporter.py
index 09f7db276..cd558b7ab 100644
--- a/tests/feature/test_gherkin_terminal_reporter.py
+++ b/tests/feature/test_gherkin_terminal_reporter.py
@@ -200,9 +200,7 @@ def test_step_parameters_should_be_replaced_by_their_values(pytester):
Examples:
| start | eat | left |
|{start}|{eat}|{left}|
- """.format(
- **example
- )
+ """.format(**example)
),
)
pytester.makepyfile(
diff --git a/tests/feature/test_no_scenario.py b/tests/feature/test_no_scenario.py
index f3bcd7d3c..49dcc472c 100644
--- a/tests/feature/test_no_scenario.py
+++ b/tests/feature/test_no_scenario.py
@@ -1,5 +1,7 @@
"""Test no scenarios defined in the feature file."""
+from __future__ import annotations
+
import textwrap
diff --git a/tests/feature/test_outline.py b/tests/feature/test_outline.py
index fc5dd1817..ddeb5d562 100644
--- a/tests/feature/test_outline.py
+++ b/tests/feature/test_outline.py
@@ -1,5 +1,7 @@
"""Scenario Outline tests."""
+from __future__ import annotations
+
import textwrap
from pytest_bdd.utils import collect_dumped_objects
@@ -333,7 +335,7 @@ def test_variable_reuse(pytester):
outline=textwrap.dedent(
"""\
Feature: Example parameters reuse
- Scenario Outline: Check for example parameter re-use
+ Scenario Outline: Check for example parameter reuse
Given the param is initially set from the example table as
When a step arg of the same name is set to "other"
Then the param is still set from the example table as
diff --git a/tests/feature/test_outline_empty_values.py b/tests/feature/test_outline_empty_values.py
index 329434b6f..50c7211a1 100644
--- a/tests/feature/test_outline_empty_values.py
+++ b/tests/feature/test_outline_empty_values.py
@@ -1,5 +1,7 @@
"""Scenario Outline with empty example values tests."""
+from __future__ import annotations
+
import textwrap
from pytest_bdd.utils import collect_dumped_objects
diff --git a/tests/feature/test_report.py b/tests/feature/test_report.py
index deac0ab52..2fb3813e3 100644
--- a/tests/feature/test_report.py
+++ b/tests/feature/test_report.py
@@ -1,7 +1,8 @@
"""Test scenario reporting."""
+from __future__ import annotations
+
import textwrap
-from typing import Optional
import pytest
@@ -11,7 +12,7 @@
class OfType:
"""Helper object comparison to which is always 'equal'."""
- def __init__(self, type: Optional[type] = None) -> None:
+ def __init__(self, type: type | None = None) -> None:
self.type = type
def __eq__(self, other: object) -> bool:
diff --git a/tests/feature/test_rule_example_format.py b/tests/feature/test_rule_example_format.py
index de73d6108..904cd05d1 100644
--- a/tests/feature/test_rule_example_format.py
+++ b/tests/feature/test_rule_example_format.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import textwrap
diff --git a/tests/feature/test_same_function_name.py b/tests/feature/test_same_function_name.py
index dd91896a3..93a726bc7 100644
--- a/tests/feature/test_same_function_name.py
+++ b/tests/feature/test_same_function_name.py
@@ -1,5 +1,7 @@
"""Function name same as step name."""
+from __future__ import annotations
+
import textwrap
diff --git a/tests/feature/test_scenario.py b/tests/feature/test_scenario.py
index 19c5ccf69..bd092f510 100644
--- a/tests/feature/test_scenario.py
+++ b/tests/feature/test_scenario.py
@@ -1,5 +1,7 @@
"""Test scenario decorator."""
+from __future__ import annotations
+
import textwrap
from pytest_bdd.utils import collect_dumped_objects
diff --git a/tests/feature/test_scenarios.py b/tests/feature/test_scenarios.py
index 9e0407c4f..b38fb93d6 100644
--- a/tests/feature/test_scenarios.py
+++ b/tests/feature/test_scenarios.py
@@ -1,5 +1,7 @@
"""Test scenarios shortcut."""
+from __future__ import annotations
+
import textwrap
diff --git a/tests/feature/test_steps.py b/tests/feature/test_steps.py
index f1058547d..dd09d1b6f 100644
--- a/tests/feature/test_steps.py
+++ b/tests/feature/test_steps.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import textwrap
diff --git a/tests/feature/test_tags.py b/tests/feature/test_tags.py
index 786c3f10a..b933b6483 100644
--- a/tests/feature/test_tags.py
+++ b/tests/feature/test_tags.py
@@ -1,5 +1,7 @@
"""Test tags."""
+from __future__ import annotations
+
import textwrap
diff --git a/tests/feature/test_wrong.py b/tests/feature/test_wrong.py
index f8c405439..15b74558a 100644
--- a/tests/feature/test_wrong.py
+++ b/tests/feature/test_wrong.py
@@ -1,5 +1,7 @@
"""Test wrong feature syntax."""
+from __future__ import annotations
+
import textwrap
diff --git a/tests/generation/test_generate_missing.py b/tests/generation/test_generate_missing.py
index faa077783..45b5f8941 100644
--- a/tests/generation/test_generate_missing.py
+++ b/tests/generation/test_generate_missing.py
@@ -1,5 +1,7 @@
"""Code generation and assertion tests."""
+from __future__ import annotations
+
import itertools
import textwrap
diff --git a/tests/library/test_parent.py b/tests/library/test_parent.py
index a18151df1..08f4584ba 100644
--- a/tests/library/test_parent.py
+++ b/tests/library/test_parent.py
@@ -3,6 +3,8 @@
Check the parent givens are collected and overridden in the local conftest.
"""
+from __future__ import annotations
+
import textwrap
from pytest_bdd.utils import collect_dumped_objects
@@ -233,7 +235,7 @@ def test_local(request):
def test_uses_correct_step_in_the_hierarchy(pytester):
"""
- Test regression found in issue #524, where we couldn't find the correct step implemntation in the
+ Test regression found in issue #524, where we couldn't find the correct step implementation in the
hierarchy of files/folder as expected.
This test uses many files and folders that act as decoy, while the real step implementation is defined
in the last file (test_b/test_b.py).
diff --git a/tests/parser/test_errors.py b/tests/parser/test_errors.py
index 93ff90388..5c616388a 100644
--- a/tests/parser/test_errors.py
+++ b/tests/parser/test_errors.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import textwrap
diff --git a/tests/parser/test_parser.py b/tests/parser/test_parser.py
index b742e63d7..09be7c853 100644
--- a/tests/parser/test_parser.py
+++ b/tests/parser/test_parser.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
from pathlib import Path
from src.pytest_bdd.gherkin_parser import (
diff --git a/tests/scripts/test_generate.py b/tests/scripts/test_generate.py
index fcb979099..e194076d1 100644
--- a/tests/scripts/test_generate.py
+++ b/tests/scripts/test_generate.py
@@ -1,5 +1,7 @@
"""Test code generation command."""
+from __future__ import annotations
+
import os
import sys
import textwrap
diff --git a/tests/scripts/test_main.py b/tests/scripts/test_main.py
index 4cb2265fe..2053eb115 100644
--- a/tests/scripts/test_main.py
+++ b/tests/scripts/test_main.py
@@ -1,5 +1,7 @@
"""Main command."""
+from __future__ import annotations
+
import os
import sys
import textwrap
diff --git a/tests/scripts/test_migrate.py b/tests/scripts/test_migrate.py
index 9bda496d8..ba99f5a30 100644
--- a/tests/scripts/test_migrate.py
+++ b/tests/scripts/test_migrate.py
@@ -1,5 +1,7 @@
"""Test code generation command."""
+from __future__ import annotations
+
import os
import sys
import textwrap
@@ -31,11 +33,7 @@ def test_migrate(monkeypatch, capsys, pytester):
expected = textwrap.dedent(
"""
migrated: {0}/test_foo.py
- skipped: {0}/__init__.py""".format(
- str(tests)
- )[
- 1:
- ]
+ skipped: {0}/__init__.py""".format(str(tests))[1:]
)
assert out == expected
assert tests.joinpath("test_foo.py").read_text() == textwrap.dedent(
diff --git a/tests/steps/test_common.py b/tests/steps/test_common.py
index a2eb8b04c..fad26c9d9 100644
--- a/tests/steps/test_common.py
+++ b/tests/steps/test_common.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import textwrap
from typing import Any, Callable
from unittest import mock
diff --git a/tests/steps/test_docstring.py b/tests/steps/test_docstring.py
index 81d63c2b9..c2e3abc19 100644
--- a/tests/steps/test_docstring.py
+++ b/tests/steps/test_docstring.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import textwrap
from src.pytest_bdd.utils import collect_dumped_objects
diff --git a/tests/steps/test_given.py b/tests/steps/test_given.py
index ff1e74911..469befbe6 100644
--- a/tests/steps/test_given.py
+++ b/tests/steps/test_given.py
@@ -1,5 +1,7 @@
"""Given tests."""
+from __future__ import annotations
+
import textwrap
diff --git a/tests/steps/test_keyword.py b/tests/steps/test_keyword.py
index 8255ce60f..a5c56938c 100644
--- a/tests/steps/test_keyword.py
+++ b/tests/steps/test_keyword.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import textwrap
diff --git a/tests/steps/test_unicode.py b/tests/steps/test_unicode.py
index c71491371..1ad2a7351 100644
--- a/tests/steps/test_unicode.py
+++ b/tests/steps/test_unicode.py
@@ -1,5 +1,7 @@
"""Tests for testing cases when we have unicode in feature file."""
+from __future__ import annotations
+
import textwrap
diff --git a/tests/test_hooks.py b/tests/test_hooks.py
index 53b44f728..768d2eda1 100644
--- a/tests/test_hooks.py
+++ b/tests/test_hooks.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import textwrap
from pytest_bdd.utils import collect_dumped_objects