From 51a30872c449ce551a17c95277dda67e51e4b401 Mon Sep 17 00:00:00 2001 From: Kieran Ryan Date: Mon, 30 Dec 2024 00:17:26 +0000 Subject: [PATCH 01/10] Migrate linting to ruff - Configure `ruff` as a drop in replacement for `flake8`, `isort` and `pyupgrade` - Lint for required `from __future__ import annotations` in `src/` - Enable automatic fixing of linting errors in pre-commit - Applied ruff `--fix` for `I001` on `src/pytest_bdd/parser.py` - Applied ruff `--fix` for `UP032` on `src/pytest_bdd/gherkin_terminal_reporter.py` - Applied ruff `--fix` for `tests/feature/test_feature_base_dir.py` - Applied ruff `--fix` for `tests/feature/test_feature_base_dir.py` - Ignore `B904` error on `src/pytest_bdd/scenario.py` - Fix invalid pre-commit config error due to indentation syntax error - Freeze pre-commit dependency versions --- .pre-commit-config.yaml | 39 ++++++--------------- pyproject.toml | 30 +++++++++------- src/pytest_bdd/gherkin_terminal_reporter.py | 7 ++-- src/pytest_bdd/parser.py | 4 +-- src/pytest_bdd/scenario.py | 2 +- tests/feature/test_feature_base_dir.py | 16 ++++----- 6 files changed, 39 insertions(+), 59 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aa159eb9..96b4e359 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/pyproject.toml b/pyproject.toml index f196671b..c06e0817 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,21 +57,25 @@ 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" +target-version = "py39" +lint.select = [ + "B", # flake8-bugbear + "E4", # pycodestyle - error - import + "E7", # pycodestyle - error - statement + "E9", # pycodestyle - error - runtime + "F", # pyflakes + "I", # isort + "UP", # pyupgrade +] +lint.isort.required-imports = [ + "from __future__ import annotations", +] -[tool.isort] -profile = "black" -line_length = 120 -multi_line_output = 3 +[tool.ruff.lint.per-file-ignores] +# Lint `I002` (required imports) for `from __future__ import annotations` in `src/` +"!src/**.py" = ["I002"] [tool.coverage.report] exclude_lines = [ diff --git a/src/pytest_bdd/gherkin_terminal_reporter.py b/src/pytest_bdd/gherkin_terminal_reporter.py index 264aea2d..e18719c1 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 a1b6eb26..3ae4671e 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"<(.+?)>") diff --git a/src/pytest_bdd/scenario.py b/src/pytest_bdd/scenario.py index e8e84277..687a1d7e 100644 --- a/src/pytest_bdd/scenario.py +++ b/src/pytest_bdd/scenario.py @@ -384,7 +384,7 @@ def scenario( scenario = feature.scenarios[scenario_name] except KeyError: feature_name = feature.name or "[Empty]" - raise exceptions.ScenarioNotFound( + raise exceptions.ScenarioNotFound( # noqa: B904 f'Scenario "{scenario_name}" in feature "{feature_name}" in {feature.filename} is not found.' ) diff --git a/tests/feature/test_feature_base_dir.py b/tests/feature/test_feature_base_dir.py index 669571c7..f6d83600 100644 --- a/tests/feature/test_feature_base_dir.py +++ b/tests/feature/test_feature_base_dir.py @@ -59,12 +59,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 +75,7 @@ def prepare_testdir(pytester, ini_base_dir): ) pytester.makepyfile( - """ + f""" import os.path import pytest @@ -103,7 +101,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 +143,5 @@ def test_ok_by_param(scenario_name, multiple): else: scenario(FEATURE, scenario_name, features_base_dir='features') - """.format( - ini_base_dir - ) + """ ) From f5eedc981b42e66e8b676680bb29467fbe1d9e78 Mon Sep 17 00:00:00 2001 From: Kieran Ryan Date: Mon, 30 Dec 2024 00:23:32 +0000 Subject: [PATCH 02/10] Recommend ruff Visual Studio Code extension - Ensure linting tight feedback loop for contributors --- .vscode/extensions.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .vscode/extensions.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..5d21c814 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "charliermarsh.ruff" + ] +} From 06bc96e8abd1b4e10a6028589a948d4bf8380212 Mon Sep 17 00:00:00 2001 From: Kieran Ryan Date: Mon, 30 Dec 2024 01:09:38 +0000 Subject: [PATCH 03/10] Require future annotations import for tests (I002) - Applied through ruff `--fix` - Subsequently simplified optional in `tests/feature/test_report.py` --- docs/conf.py | 1 + pyproject.toml | 4 ---- tests/args/cfparse/test_args.py | 2 ++ tests/args/parse/test_args.py | 2 ++ tests/args/regex/test_args.py | 2 ++ tests/args/test_common.py | 2 ++ tests/conftest.py | 2 ++ tests/datatable/test_datatable.py | 2 ++ tests/feature/test_alias.py | 2 ++ tests/feature/test_background.py | 2 ++ tests/feature/test_description.py | 2 ++ tests/feature/test_feature_base_dir.py | 2 ++ tests/feature/test_gherkin_terminal_reporter.py | 4 +--- tests/feature/test_no_scenario.py | 2 ++ tests/feature/test_outline.py | 2 ++ tests/feature/test_outline_empty_values.py | 2 ++ tests/feature/test_report.py | 5 +++-- tests/feature/test_rule_example_format.py | 2 ++ tests/feature/test_same_function_name.py | 2 ++ tests/feature/test_scenario.py | 2 ++ tests/feature/test_scenarios.py | 2 ++ tests/feature/test_steps.py | 2 ++ tests/feature/test_tags.py | 2 ++ tests/feature/test_wrong.py | 2 ++ tests/generation/test_generate_missing.py | 2 ++ tests/library/test_parent.py | 2 ++ tests/parser/test_errors.py | 2 ++ tests/parser/test_parser.py | 2 ++ tests/scripts/test_generate.py | 2 ++ tests/scripts/test_main.py | 2 ++ tests/scripts/test_migrate.py | 8 +++----- tests/steps/test_common.py | 2 ++ tests/steps/test_docstring.py | 2 ++ tests/steps/test_given.py | 2 ++ tests/steps/test_keyword.py | 2 ++ tests/steps/test_unicode.py | 2 ++ tests/test_hooks.py | 2 ++ 37 files changed, 72 insertions(+), 14 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index b9dbef4e..d19163f1 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 c06e0817..00dc0dd7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,10 +73,6 @@ lint.isort.required-imports = [ "from __future__ import annotations", ] -[tool.ruff.lint.per-file-ignores] -# Lint `I002` (required imports) for `from __future__ import annotations` in `src/` -"!src/**.py" = ["I002"] - [tool.coverage.report] exclude_lines = [ "if TYPE_CHECKING:", diff --git a/tests/args/cfparse/test_args.py b/tests/args/cfparse/test_args.py index 3b731f58..4c1b51a3 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 e02ea18c..9591162d 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 cce2d599..d7b3a1b7 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 9192e812..273f7f49 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 f1d2199c..bd4ea9a8 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 47a04d11..606ec32a 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 ce2b68cf..97e015f0 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 d541e965..86428bb8 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 c1426a28..b02c125b 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 f6d83600..514b554f 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 diff --git a/tests/feature/test_gherkin_terminal_reporter.py b/tests/feature/test_gherkin_terminal_reporter.py index 09f7db27..cd558b7a 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 f3bcd7d3..49dcc472 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 fc5dd181..74379db9 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 diff --git a/tests/feature/test_outline_empty_values.py b/tests/feature/test_outline_empty_values.py index 329434b6..50c7211a 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 deac0ab5..2fb3813e 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 de73d610..904cd05d 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 dd91896a..93a726bc 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 19c5ccf6..bd092f51 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 9e0407c4..b38fb93d 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 f1058547..dd09d1b6 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 786c3f10..b933b648 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 f8c40543..15b74558 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 faa07778..45b5f894 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 a18151df..5900750a 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 diff --git a/tests/parser/test_errors.py b/tests/parser/test_errors.py index 93ff9038..5c616388 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 b742e63d..09be7c85 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 fcb97909..e194076d 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 4cb2265f..2053eb11 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 9bda496d..ba99f5a3 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 a2eb8b04..fad26c9d 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 81d63c2b..c2e3abc1 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 ff1e7491..469befbe 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 8255ce60..a5c56938 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 c7149137..1ad2a735 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 53b44f72..768d2eda 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 From 330652549ef2858f5f663088d93f98b8fd4c7108 Mon Sep 17 00:00:00 2001 From: Kieran Ryan Date: Tue, 31 Dec 2024 14:29:42 +0000 Subject: [PATCH 04/10] Apply codespell spelling fixes --- tests/feature/test_outline.py | 2 +- tests/library/test_parent.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/feature/test_outline.py b/tests/feature/test_outline.py index 74379db9..ddeb5d56 100644 --- a/tests/feature/test_outline.py +++ b/tests/feature/test_outline.py @@ -335,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/library/test_parent.py b/tests/library/test_parent.py index 5900750a..08f4584b 100644 --- a/tests/library/test_parent.py +++ b/tests/library/test_parent.py @@ -235,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). From d029b9e71a3126712f6b095a090a0c81e9473a6e Mon Sep 17 00:00:00 2001 From: Kieran Ryan Date: Tue, 31 Dec 2024 15:32:16 +0000 Subject: [PATCH 05/10] docs: Align docstring type hints with __future__ annotations - `Optional` -> `... | None` - `List` -> `list` --- pyproject.toml | 6 +++--- src/pytest_bdd/parser.py | 38 +++++++++++++++++++------------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 00dc0dd7..2d9da465 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,12 +61,12 @@ build-backend = "poetry.core.masonry.api" line-length = 120 target-version = "py39" lint.select = [ - "B", # flake8-bugbear + "B", # flake8-bugbear "E4", # pycodestyle - error - import "E7", # pycodestyle - error - statement "E9", # pycodestyle - error - runtime - "F", # pyflakes - "I", # isort + "F", # pyflakes + "I", # isort "UP", # pyupgrade ] lint.isort.required-imports = [ diff --git a/src/pytest_bdd/parser.py b/src/pytest_bdd/parser.py index 3ae4671e..f195050f 100644 --- a/src/pytest_bdd/parser.py +++ b/src/pytest_bdd/parser.py @@ -46,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. @@ -64,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. """ @@ -86,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 @@ -152,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 @@ -195,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 @@ -242,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. """ @@ -268,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 @@ -344,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 @@ -378,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: @@ -421,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. From eb5ca7452465d572e9018e014fc58973abd108e8 Mon Sep 17 00:00:00 2001 From: Kieran Ryan Date: Tue, 31 Dec 2024 15:51:24 +0000 Subject: [PATCH 06/10] refactor: Missing None return type hint on FeatureParser __init__ (ANN204) - Detected through ruff --- src/pytest_bdd/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pytest_bdd/parser.py b/src/pytest_bdd/parser.py index f195050f..e2be8482 100644 --- a/src/pytest_bdd/parser.py +++ b/src/pytest_bdd/parser.py @@ -369,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 From d70029dde9cf090396483c8b9f05efd744694db9 Mon Sep 17 00:00:00 2001 From: Kieran Ryan Date: Tue, 31 Dec 2024 16:17:38 +0000 Subject: [PATCH 07/10] refactor: Drop implicit str concatentation (ISC001) - Applied through ruff (`ruff check --select=TC --unsafe-fixes --fix`) --- pyproject.toml | 15 ++++++++------- src/pytest_bdd/scenario.py | 6 +++--- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2d9da465..4449b1fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,13 +61,14 @@ build-backend = "poetry.core.masonry.api" line-length = 120 target-version = "py39" lint.select = [ - "B", # flake8-bugbear - "E4", # pycodestyle - error - import - "E7", # pycodestyle - error - statement - "E9", # pycodestyle - error - runtime - "F", # pyflakes - "I", # isort - "UP", # pyupgrade + "B", # flake8-bugbear + "E4", # pycodestyle - error - import + "E7", # pycodestyle - error - statement + "E9", # pycodestyle - error - runtime + "F", # pyflakes + "I", # isort + "ISC", # flake8-implicit-str-concat + "UP", # pyupgrade ] lint.isort.required-imports = [ "from __future__ import annotations", diff --git a/src/pytest_bdd/scenario.py b/src/pytest_bdd/scenario.py index 687a1d7e..20a4883b 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: From 59c638c622bbe4817f4a1776fc52f7c821045589 Mon Sep 17 00:00:00 2001 From: Kieran Ryan Date: Tue, 31 Dec 2024 16:19:40 +0000 Subject: [PATCH 08/10] Lint perflint, comprehensions and blind-except (PERF, C4, BLE) - flake8-blind-except: https://docs.astral.sh/ruff/rules/#flake8-blind-except-ble - flake8-comprehensions: https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4 - Perflint: https://docs.astral.sh/ruff/rules/#perflint-perf --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 4449b1fb..d9c6066d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,12 +62,15 @@ line-length = 120 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.isort.required-imports = [ From 4427f194030ab28290c1b58d59d2cfd1ad4e95b0 Mon Sep 17 00:00:00 2001 From: Kieran Ryan Date: Wed, 1 Jan 2025 19:59:10 +0000 Subject: [PATCH 09/10] refactor: Explicit exception 'raise ... from None' (B904) https://docs.astral.sh/ruff/rules/raise-without-from-inside-except/ --- src/pytest_bdd/scenario.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pytest_bdd/scenario.py b/src/pytest_bdd/scenario.py index 20a4883b..1d98ebc5 100644 --- a/src/pytest_bdd/scenario.py +++ b/src/pytest_bdd/scenario.py @@ -384,9 +384,9 @@ def scenario( scenario = feature.scenarios[scenario_name] except KeyError: feature_name = feature.name or "[Empty]" - raise exceptions.ScenarioNotFound( # noqa: B904 + 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 From 4bf4af284628137785e8854748aa172616e5da4e Mon Sep 17 00:00:00 2001 From: Kieran Ryan Date: Wed, 1 Jan 2025 21:44:01 +0000 Subject: [PATCH 10/10] refactor: Ignore ISC001 as covered by formatter --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index d9c6066d..952f4a19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,6 +73,10 @@ lint.select = [ "PERF", # perflint "UP", # pyupgrade ] +lint.ignore = [ + # Covered by formatter + "ISC001" # single-line-implicit-string-concatenation +] lint.isort.required-imports = [ "from __future__ import annotations", ]