Merge remote-tracking branch 'origin/master' into tatsu-parser

# Conflicts:
#	.gitignore
#	MANIFEST.in
#	Makefile
#	pyproject.toml
#	requirements-testing.txt
#	setup.cfg
#	src/pytest_bdd/__init__.py
#	src/pytest_bdd/parser.py
#	src/pytest_bdd/steps.py
#	src/pytest_bdd/utils.py
#	tests/conftest.py
#	tests/feature/test_tags.py
#	tox.ini
This commit is contained in:
Alessio Bogon 2022-08-19 21:44:48 +02:00
commit b26487aea8
56 changed files with 2292 additions and 758 deletions

View File

@ -1,5 +0,0 @@
[run]
branch = true
include =
pytest_bdd/*
tests/*

View File

@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10"]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11-dev"]
steps:
- uses: actions/checkout@v2

7
.gitignore vendored
View File

@ -48,10 +48,3 @@ nosetests.xml
#PyCharm
/.idea
# virtualenv
/.Python
/lib
/include
/share
/local

View File

@ -3,7 +3,7 @@
repos:
- repo: https://github.com/psf/black
# If you update the version here, also update it in tox.ini (py*-pytestlatest-linters)
rev: 22.1.0
rev: 22.6.0
hooks:
- id: black
- repo: https://github.com/pycqa/isort
@ -12,7 +12,7 @@ repos:
- id: isort
name: isort (python)
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.1.0
rev: v4.3.0
hooks:
# disabled because it goes in the way when testing
# gherkin files with weird whitespaces on purpose
@ -21,7 +21,7 @@ repos:
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/asottile/pyupgrade
rev: v2.31.0
rev: v2.37.3
hooks:
- id: pyupgrade
args: ["--py37-plus"]

22
.readthedocs.yaml Normal file
View File

@ -0,0 +1,22 @@
# .readthedocs.yaml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
version: 2
build:
os: ubuntu-22.04
tools:
python: "3"
sphinx:
configuration: docs/conf.py
formats:
- epub
- pdf
- htmlzip
python:
install:
- method: pip
path: .

View File

@ -3,18 +3,34 @@ Changelog
Unreleased
----------
- Fix bug where steps without parsers would take precedence over steps with parsers. `#534 <https://github.com/pytest-dev/pytest-bdd/pull/534>`_
- Step functions can now be decorated multiple times with @given, @when, @then. Previously every decorator would override ``converters`` and ``target_fixture`` every at every application. `#534 <https://github.com/pytest-dev/pytest-bdd/pull/534>`_ `#544 <https://github.com/pytest-dev/pytest-bdd/pull/544>`_ `#525 <https://github.com/pytest-dev/pytest-bdd/issues/525>`_
- ``parsers.re`` now does a `fullmatch <https://docs.python.org/3/library/re.html#re.fullmatch>`_ instead of a partial match. This is to make it work just like the other parsers, since they don't ignore non-matching characters at the end of the string. `#539 <https://github.com/pytest-dev/pytest-bdd/pull/539>`_
- Require pytest>=6.2 `#534 <https://github.com/pytest-dev/pytest-bdd/pull/534>`_
- Using modern way to specify hook options to avoid deprecation warnings with pytest >=7.2.
- Add generic ``step`` decorator that will be used for all kind of steps `#548 <https://github.com/pytest-dev/pytest-bdd/pull/548>`_
- Add ``stacklevel`` param to ``given``, ``when``, ``then``, ``step`` decorators. This allows for programmatic step generation `#548 <https://github.com/pytest-dev/pytest-bdd/pull/548>`_
6.0.1
-----
- Fix regression introduced in 6.0.0 where a step function decorated multiple using a parsers times would not be executed correctly. `#530 <https://github.com/pytest-dev/pytest-bdd/pull/530>`_ `#528 <https://github.com/pytest-dev/pytest-bdd/issues/528>`_
6.0.0
-----
This release introduces breaking changes in order to be more in line with the official gherkin specification.
- Cleanup of the documentation and tests related to parametrization (elchupanebrej)
- Removed feature level examples for the gherkin compatibility (olegpidsadnyi)
- Removed vertical examples for the gherkin compatibility (olegpidsadnyi)
- Step arguments are no longer fixtures (olegpidsadnyi)
- Drop support of python 3.6, pytest 4 (elchupanebrej)
- Step definitions can have "yield" statements again (4.0 release broke it). They will be executed as normal fixtures: code after the yield is executed during teardown of the test. (youtux)
- Scenario outlines unused example parameter validation is removed (olegpidsadnyi)
- Add type decorations
- ``pytest_bdd.parsers.StepParser`` now is an Abstract Base Class. Subclasses must make sure to implement the abstract methods.
- Cleanup of the documentation and tests related to parametrization (elchupanebrej) `#469 <https://github.com/pytest-dev/pytest-bdd/pull/469>`_
- Removed feature level examples for the gherkin compatibility (olegpidsadnyi) `#490 <https://github.com/pytest-dev/pytest-bdd/pull/490>`_
- Removed vertical examples for the gherkin compatibility (olegpidsadnyi) `#492 <https://github.com/pytest-dev/pytest-bdd/pull/492>`_
- Step arguments are no longer fixtures (olegpidsadnyi) `#493 <https://github.com/pytest-dev/pytest-bdd/pull/493>`_
- Drop support of python 3.6, pytest 4 (elchupanebrej) `#495 <https://github.com/pytest-dev/pytest-bdd/pull/495>`_ `#504 <https://github.com/pytest-dev/pytest-bdd/issues/504>`_
- 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) `#503 <https://github.com/pytest-dev/pytest-bdd/issues/503>`_
- Scenario outlines unused example parameter validation is removed (olegpidsadnyi) `#499 <https://github.com/pytest-dev/pytest-bdd/pull/499>`_
- Add type annotations (youtux) `#505 <https://github.com/pytest-dev/pytest-bdd/pull/505>`_
- ``pytest_bdd.parsers.StepParser`` now is an Abstract Base Class. Subclasses must make sure to implement the abstract methods. (youtux) `#505 <https://github.com/pytest-dev/pytest-bdd/pull/505>`_
- Angular brackets in step definitions are only parsed in "Scenario Outline" (previously they were parsed also in normal "Scenario"s) (youtux) `#524 <https://github.com/pytest-dev/pytest-bdd/pull/524>`_.

24
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,24 @@
# How to setup development environment
- Install poetry: https://python-poetry.org/docs/#installation
- (Optional) Install pre-commit: https://pre-commit.com/#install
- Run `poetry install` to install dependencies
- Run `pre-commit install` to install pre-commit hooks
# How to run tests
- Run `poetry run pytest`
- or run `tox`
# How to make a release
```shell
python -m pip install --upgrade build twine
# cleanup the ./dist folder
rm -rf ./dist
# Build the distributions
python -m build
# Upload them
twine upload dist/*
```

View File

@ -1,7 +0,0 @@
include *.rst
include *.txt
include setup.py
include src/pytest_bdd/templates/*.mak
include src/pytest_bdd/parser_data/*
include src/pytest_bdd/gherkin.tatsu
include src/build_parser.py

View File

@ -1,17 +1,4 @@
# create virtual environment
PATH := .env/bin:$(PATH)
.env:
virtualenv .env
.PHONY: dependencies
dependencies:
.env/bin/pip install -e . -r requirements-testing.txt tox python-coveralls
.PHONY: develop
develop: | .env dependencies src/pytest_bdd/_gherkin.py
# TODO: Try to remove this file
.PHONY: live-reload
@ -22,21 +9,3 @@ live-reload:
src/pytest_bdd/_gherkin.py:
tatsu pytest_bdd/gherkin.tatsu --generate-parser > pytest_bdd/_gherkin.py
.PHONY: coverage
coverage: develop
coverage run --source=pytest_bdd .env/bin/pytest tests
coverage report -m
.PHONY: test
test: develop
tox
.PHONY: coveralls
coveralls: coverage
coveralls
.PHONY: clean
clean:
-rm -rf .env

View File

@ -35,19 +35,16 @@ Install pytest-bdd
pip install pytest-bdd
The minimum required version of pytest is 4.3.
Example
-------
An example test for a blog hosting software could look like this.
Note that pytest-splinter_ is used to get the browser fixture.
publish_article.feature:
.. code-block:: gherkin
# content of publish_article.feature
Feature: Blog
A site where you can publish your articles.
@ -63,10 +60,10 @@ publish_article.feature:
Note that only one feature is allowed per feature file.
test_publish_article.py:
.. code-block:: python
# content of test_publish_article.py
from pytest_bdd import scenario, given, when, then
@scenario('publish_article.feature', 'Publishing the article')
@ -208,12 +205,11 @@ for `cfparse` parser
from pytest_bdd import parsers
@given(
parsers.cfparse("there are {start:Number} cucumbers",
extra_types=dict(Number=int)),
parsers.cfparse("there are {start:Number} cucumbers", extra_types={"Number": int}),
target_fixture="cucumbers",
)
def given_cucumbers(start):
return dict(start=start, eat=0)
return {"start": start, "eat": 0}
for `re` parser
@ -223,11 +219,11 @@ for `re` parser
@given(
parsers.re(r"there are (?P<start>\d+) cucumbers"),
converters=dict(start=int),
converters={"start": int},
target_fixture="cucumbers",
)
def given_cucumbers(start):
return dict(start=start, eat=0)
return {"start": start, "eat": 0}
Example:
@ -248,28 +244,25 @@ The code will look like:
.. code-block:: python
import re
from pytest_bdd import scenario, given, when, then, parsers
from pytest_bdd import scenarios, given, when, then, parsers
@scenario("arguments.feature", "Arguments for given, when, then")
def test_arguments():
pass
scenarios("arguments.feature")
@given(parsers.parse("there are {start:d} cucumbers"), target_fixture="cucumbers")
def given_cucumbers(start):
return dict(start=start, eat=0)
return {"start": start, "eat": 0}
@when(parsers.parse("I eat {eat:d} cucumbers"))
def eat_cucumbers(cucumbers, eat):
start_cucumbers["eat"] += eat
cucumbers["eat"] += eat
@then(parsers.parse("I should have {left:d} cucumbers"))
def should_have_left_cucumbers(cucumbers, left):
assert cucumbers['start'] - cucumbers['eat'] == left
assert cucumbers["start"] - cucumbers["eat"] == left
Example code also shows possibility to pass argument converters which may be useful if you need to postprocess step
arguments after the parser.
@ -304,7 +297,7 @@ You can implement your own step parser. It's interface is quite simple. The code
@given(parsers.parse("there are %start% cucumbers"), target_fixture="cucumbers")
def given_cucumbers(start):
return dict(start=start, eat=0)
return {"start": start, "eat": 0}
Override fixtures via given steps
@ -349,7 +342,7 @@ A common use case is when we have to assert the outcome of an HTTP request:
.. code-block:: python
# test_blog.py
# content of test_blog.py
from pytest_bdd import scenarios, given, when, then
@ -375,7 +368,7 @@ A common use case is when we have to assert the outcome of an HTTP request:
.. code-block:: gherkin
# blog.feature
# content of blog.feature
Feature: Blog
Scenario: Deleting the article
@ -390,7 +383,7 @@ Multiline steps
---------------
As Gherkin, pytest-bdd supports multiline steps
(aka `PyStrings <http://behat.org/en/v3.0/user_guide/writing_scenarios.html#pystrings>`_).
(a.k.a. `Doc Strings <https://cucumber.io/docs/gherkin/reference/#doc-strings>`_).
But in much cleaner and powerful way:
.. code-block:: gherkin
@ -416,41 +409,27 @@ step arguments and capture lines after first line (or some subset of them) into
.. code-block:: python
import re
from pytest_bdd import given, then, scenario, parsers
@scenario(
'multiline.feature',
'Multiline step using sub indentation',
)
def test_multiline():
pass
scenarios("multiline.feature")
@given(parsers.parse("I have a step with:\n{text}"), target_fixture="i_have_text")
def i_have_text(text):
return text
@given(parsers.parse("I have a step with:\n{content}"), target_fixture="text")
def given_text(content):
return content
@then("the text should be parsed with correct indentation")
def text_should_be_correct(i_have_text, text):
assert i_have_text == text == 'Some\nExtra\nLines'
Note that `then` step definition (`text_should_be_correct`) in this example uses `text` fixture which is provided
by a `given` step (`i_have_text`) argument with the same name (`text`). This possibility is described in
the `Step arguments are fixtures as well!`_ section.
def text_should_be_correct(text):
assert text == "Some\nExtra\nLines"
Scenarios shortcut
------------------
If you have relatively large set of feature files, it's boring to manually bind scenarios to the tests using the
scenario decorator. Of course with the manual approach you get all the power to be able to additionally parametrize
the test, give the test function a nice name, document it, etc, but in the majority of the cases you don't need that.
Instead you want to bind `all` scenarios found in the `feature` folder(s) recursively automatically.
For this - there's a `scenarios` helper.
If you have relatively large set of feature files, it's boring to manually bind scenarios to the tests using the scenario decorator. Of course with the manual approach you get all the power to be able to additionally parametrize the test, give the test function a nice name, document it, etc, but in the majority of the cases you don't need that.
Instead, you want to bind all the scenarios found in the ``features`` folder(s) recursively automatically, by using the ``scenarios`` helper.
.. code-block:: python
@ -459,7 +438,7 @@ For this - there's a `scenarios` helper.
# assume 'features' subfolder is in this file's directory
scenarios('features')
That's all you need to do to bind all scenarios found in the `features` folder!
That's all you need to do to bind all scenarios found in the ``features`` folder!
Note that you can pass multiple paths, and those paths can be either feature files or feature folders.
@ -471,7 +450,7 @@ Note that you can pass multiple paths, and those paths can be either feature fil
scenarios('features', 'other_features/some.feature', 'some_other_features')
But what if you need to manually bind certain scenario, leaving others to be automatically bound?
Just write your scenario in a `normal` way, but ensure you do it `BEFORE` the call of `scenarios` helper.
Just write your scenario in a "normal" way, but ensure you do it **before** the call of ``scenarios`` helper.
.. code-block:: python
@ -485,22 +464,20 @@ Just write your scenario in a `normal` way, but ensure you do it `BEFORE` the ca
# assume 'features' subfolder is in this file's directory
scenarios('features')
In the example above `test_something` scenario binding will be kept manual, other scenarios found in the `features`
folder will be bound automatically.
In the example above, the ``test_something`` scenario binding will be kept manual, other scenarios found in the ``features`` folder will be bound automatically.
Scenario outlines
-----------------
Scenarios can be parametrized to cover few cases. In Gherkin the variable
templates are written using corner braces as ``<somevalue>``.
`Gherkin scenario outlines <http://behat.org/en/v3.0/user_guide/writing_scenarios.html#scenario-outlines>`_ are supported by pytest-bdd
exactly as it's described in the behave_ docs.
Scenarios can be parametrized to cover few cases. These are called `Scenario Outlines <https://cucumber.io/docs/gherkin/reference/#scenario-outline>`_ in Gherkin, and the variable templates are written using angular brackets (e.g. ``<var_name>``).
Example:
.. code-block:: gherkin
# content of scenario_outlines.feature
Feature: Scenario outlines
Scenario Outline: Outlined given, when, then
Given there are <start> cucumbers
@ -511,6 +488,28 @@ Example:
| start | eat | left |
| 12 | 5 | 7 |
.. code-block:: python
from pytest_bdd import scenarios, given, when, then, parsers
scenarios("scenario_outlines.feature")
@given(parsers.parse("there are {start:d} cucumbers"), target_fixture="cucumbers")
def given_cucumbers(start):
return {"start": start, "eat": 0}
@when(parsers.parse("I eat {eat:d} cucumbers"))
def eat_cucumbers(cucumbers, eat):
cucumbers["eat"] += eat
@then(parsers.parse("I should have {left:d} cucumbers"))
def should_have_left_cucumbers(cucumbers, left):
assert cucumbers["start"] - cucumbers["eat"] == left
Organizing your scenarios
-------------------------
@ -561,9 +560,9 @@ completely different:
For picking up tests to run we can use
`tests selection <http://pytest.org/latest/usage.html#specifying-tests-selecting-tests>`_ technique. The problem is that
`tests selection <https://pytest.org/en/7.1.x/how-to/usage.html#specifying-which-tests-to-run>`_ technique. The problem is that
you have to know how your tests are organized, knowing only the feature files organization is not enough.
`cucumber tags <https://github.com/cucumber/cucumber/wiki/Tags>`_ introduce standard way of categorizing your features
Cucumber uses `tags <https://cucumber.io/docs/cucumber/api/#tags>`_ as a way of categorizing your features
and scenarios, which pytest-bdd supports. For example, we could have:
.. code-block:: gherkin
@ -575,19 +574,15 @@ and scenarios, which pytest-bdd supports. For example, we could have:
Scenario: Successful login
pytest-bdd uses `pytest markers <http://pytest.org/latest/mark.html#mark>`_ as a `storage` of the tags for the given
pytest-bdd uses `pytest markers <http://pytest.org/latest/mark.html>`_ as a `storage` of the tags for the given
scenario test, so we can use standard test selection:
.. code-block:: bash
pytest -m "backend and login and successful"
The feature and scenario markers are not different from standard pytest markers, and the ``@`` symbol is stripped out
automatically to allow test selector expressions. If you want to have bdd-related tags to be distinguishable from the
other test markers, use prefix like `bdd`.
Note that if you use pytest `--strict` option, all bdd tags mentioned in the feature files should be also in the
`markers` setting of the `pytest.ini` config. Also for tags please use names which are python-compatible variable
names, eg starts with a non-number, underscore alphanumeric, etc. That way you can safely use tags for tests filtering.
The feature and scenario markers are not different from standard pytest markers, and the ``@`` symbol is stripped out automatically to allow test selector expressions. If you want to have bdd-related tags to be distinguishable from the other test markers, use prefix like ``bdd``.
Note that if you use pytest ``--strict`` option, all bdd tags mentioned in the feature files should be also in the ``markers`` setting of the ``pytest.ini`` config. Also for tags please use names which are python-compatible variable names, eg starts with a non-number, underscore alphanumeric, etc. That way you can safely use tags for tests filtering.
You can customize how tags are converted to pytest marks by implementing the
``pytest_bdd_apply_tag`` hook and returning ``True`` from it:
@ -686,7 +681,7 @@ Backgrounds
It's often the case that to cover certain feature, you'll need multiple scenarios. And it's logical that the
setup for those scenarios will have some common parts (if not equal). For this, there are `backgrounds`.
pytest-bdd implements `Gherkin backgrounds <http://behat.org/en/v3.0/user_guide/writing_scenarios.html#backgrounds>`_ for
pytest-bdd implements `Gherkin backgrounds <https://cucumber.io/docs/gherkin/reference/#background>`_ for
features.
.. code-block:: gherkin
@ -711,8 +706,8 @@ features.
In this example, all steps from the background will be executed before all the scenario's own given
steps, adding possibility to prepare some common setup for multiple scenarios in a single feature.
About background best practices, please read
`here <https://github.com/cucumber/cucumber/wiki/Background#good-practices-for-using-background>`_.
About background best practices, please read Gherkin's
`Tips for using Background <https://cucumber.io/docs/gherkin/reference/#tips-for-using-background>`_.
.. NOTE:: There is only step "Given" should be used in "Background" section,
steps "When" and "Then" are prohibited, because their purpose are
@ -754,18 +749,18 @@ Reusing steps
It is possible to define some common steps in the parent conftest.py and
simply expect them in the child test file.
common_steps.feature:
.. code-block:: gherkin
# content of common_steps.feature
Scenario: All steps are declared in the conftest
Given I have a bar
Then bar should have value "bar"
conftest.py:
.. code-block:: python
# content of conftest.py
from pytest_bdd import given, then
@ -778,10 +773,10 @@ conftest.py:
def bar_is_bar(bar):
assert bar == "bar"
test_common.py:
.. code-block:: python
# content of test_common.py
@scenario("common_steps.feature", "All steps are declared in the conftest")
def test_conftest():
pass
@ -846,10 +841,10 @@ Avoid retyping the feature file name
If you want to avoid retyping the feature file name when defining your scenarios in a test file, use ``functools.partial``.
This will make your life much easier when defining multiple scenarios in a test file. For example:
test_publish_article.py:
.. code-block:: python
# content of test_publish_article.py
from functools import partial
import pytest_bdd
@ -868,15 +863,152 @@ test_publish_article.py:
pass
You can learn more about `functools.partial <http://docs.python.org/2/library/functools.html#functools.partial>`_
You can learn more about `functools.partial <https://docs.python.org/3/library/functools.html#functools.partial>`_
in the Python docs.
Programmatic step generation
----------------------------
Sometimes you have step definitions that would be much easier to automate rather than writing manually over and over again.
This is common, for example, when using libraries like `pytest-factoryboy <https://pytest-factoryboy.readthedocs.io/>`_ that automatically creates fixtures.
Writing step definitions for every model can become a tedious task.
For this reason, pytest-bdd provides a way to generate step definitions automatically.
The trick is to pass the ``stacklevel`` parameter to the ``given``, ``when``, ``then``, ``step`` decorators. This will instruct them to inject the step fixtures in the appropriate module, rather than just injecting them in the caller frame.
Let's look at a concrete example; let's say you have a class ``Wallet`` that has some amount for each currency:
.. code-block:: python
# contents of wallet.py
import dataclass
@dataclass
class Wallet:
verified: bool
amount_eur: int
amount_usd: int
amount_gbp: int
amount_jpy: int
You can use pytest-factoryboy to automatically create model fixtures for this class:
.. code-block:: python
# contents of wallet_factory.py
from wallet import Wallet
import factory
from pytest_factoryboy import register
class WalletFactory(factory.Factory):
class Meta:
model = Wallet
amount_eur = 0
amount_usd = 0
amount_gbp = 0
amount_jpy = 0
register(Wallet) # creates the "wallet" fixture
register(Wallet, "second_wallet") # creates the "second_wallet" fixture
Now we can define a function ``generate_wallet_steps(...)`` that creates the steps for any wallet fixture (in our case, it will be ``wallet`` and ``second_wallet``):
.. code-block:: python
# contents of wallet_steps.py
import re
from dataclasses import fields
import factory
import pytest
from pytest_bdd import given, when, then, scenarios, parsers
def generate_wallet_steps(model_name="wallet", stacklevel=1):
stacklevel += 1
human_name = model_name.replace("_", " ") # "second_wallet" -> "second wallet"
@given(f"I have a {human_name}", target_fixture=model_name, stacklevel=stacklevel)
def _(request):
return request.getfixturevalue(model_name)
# Generate steps for currency fields:
for field in fields(Wallet):
match = re.fullmatch(r"amount_(?P<currency>[a-z]{3})", field.name)
if not match:
continue
currency = match["currency"]
@given(
parsers.parse(f"I have {{value:d}} {currency.upper()} in my {human_name}"),
target_fixture=f"{model_name}__amount_{currency}",
stacklevel=stacklevel,
)
def _(value: int) -> int:
return value
@then(
parsers.parse(f"I should have {{value:d}} {currency.upper()} in my {human_name}"),
stacklevel=stacklevel,
)
def _(value: int, _currency=currency, _model_name=model_name) -> None:
wallet = request.getfixturevalue(_model_name)
assert getattr(wallet, f"amount_{_currency}") == value
# Inject the steps into the current module
generate_wallet_steps("wallet")
generate_wallet_steps("second_wallet")
This last file, ``wallet_steps.py``, now contains all the step definitions for our "wallet" and "second_wallet" fixtures.
We can now define a scenario like this:
.. code-block:: gherkin
# contents of wallet.feature
Feature: A feature
Scenario: Wallet EUR amount stays constant
Given I have 10 EUR in my wallet
And I have a wallet
Then I should have 10 EUR in my wallet
Scenario: Second wallet JPY amount stays constant
Given I have 100 JPY in my second wallet
And I have a second wallet
Then I should have 100 JPY in my second wallet
and finally a test file that puts it all together and run the scenarios:
.. code-block:: python
# contents of test_wallet.py
from pytest_factoryboy import scenarios
from wallet_factory import * # import the registered fixtures "wallet" and "second_wallet"
from wallet_steps import * # import all the step definitions into this test file
scenarios("wallet.feature")
Hooks
-----
pytest-bdd exposes several `pytest hooks <http://pytest.org/latest/plugins.html#well-specified-hooks>`_
which might be helpful building useful reporting, visualization, etc on top of it:
pytest-bdd exposes several `pytest hooks <https://docs.pytest.org/en/7.1.x/reference/reference.html#hooks>`_
which might be helpful building useful reporting, visualization, etc. on top of it:
* pytest_bdd_before_scenario(request, feature, scenario) - Called before scenario is executed
@ -903,7 +1035,7 @@ Browser testing
Tools recommended to use for browser testing:
* pytest-splinter_ - pytest `splinter <http://splinter.cobrateam.info/>`_ integration for the real browser testing
* pytest-splinter_ - pytest `splinter <https://splinter.readthedocs.io/>`_ integration for the real browser testing
Reporting
@ -1002,8 +1134,7 @@ gherkin reference. This means deprecation of some non-standard features that wer
Removal of the feature examples
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The example tables on the feature level are no longer supported. The tests should be parametrized with the example tables
on the scenario level.
The example tables on the feature level are no longer supported. If you had examples on the feature level, you should copy them to each individual scenario.
Removal of the vertical examples
@ -1018,6 +1149,12 @@ Step parsed arguments conflicted with the fixtures. Now they no longer define fi
If the fixture has to be defined by the step the target_fixture param should be used.
Variable templates in steps are only parsed for Scenario Outlines
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In previous versions of pytest, steps containing ``<variable>`` would be parsed both by ``Scenario`` and ``Scenario Outline``.
Now they are only parsed within a ``Scenario Outline``.
.. _Migration from 4.x.x:
Migration of your tests from versions 4.x.x
@ -1106,6 +1243,6 @@ Step validation handlers for the hook ``pytest_bdd_step_validation_error`` shoul
License
-------
This software is licensed under the `MIT license <http://en.wikipedia.org/wiki/MIT_License>`_.
This software is licensed under the `MIT License <https://opensource.org/licenses/MIT>`_.
© 2013-2014 Oleg Pidsadnyi, Anatoly Bubenkov and others
© 2013 Oleg Pidsadnyi, Anatoly Bubenkov and others

View File

@ -1,15 +0,0 @@
# How to make a release
```shell
python -m pip install --upgrade build twine
# cleanup the ./dist folder
rm -rf ./dist
# Build the distributions
python -m build
# Upload them
twine upload dist/*
```

View File

@ -16,6 +16,7 @@
import os
import sys
from importlib import metadata
sys.path.insert(0, os.path.abspath(".."))
@ -51,9 +52,9 @@ copyright = "2013, Oleg Pidsadnyi"
# built documents.
#
# The short X.Y version.
version = pytest_bdd.__version__
version = metadata.version("pytest-bdd")
# The full version, including alpha/beta/rc tags.
release = pytest_bdd.__version__
release = version
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

661
poetry.lock generated Normal file
View File

@ -0,0 +1,661 @@
[[package]]
name = "atomicwrites"
version = "1.4.1"
description = "Atomic file writes."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "attrs"
version = "22.1.0"
description = "Classes Without Boilerplate"
category = "main"
optional = false
python-versions = ">=3.5"
[package.extras]
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"]
[[package]]
name = "colorama"
version = "0.4.5"
description = "Cross-platform colored terminal text."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "coverage"
version = "6.4.3"
description = "Code coverage measurement for Python"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
[package.extras]
toml = ["tomli"]
[[package]]
name = "distlib"
version = "0.3.5"
description = "Distribution utilities"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "execnet"
version = "1.9.0"
description = "execnet: rapid multi-Python deployment"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.extras]
testing = ["pre-commit"]
[[package]]
name = "filelock"
version = "3.8.0"
description = "A platform independent file lock."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.extras]
testing = ["pytest-timeout (>=2.1)", "pytest-cov (>=3)", "pytest (>=7.1.2)", "coverage (>=6.4.2)", "covdefaults (>=2.2)"]
docs = ["sphinx-autodoc-typehints (>=1.19.1)", "sphinx (>=5.1.1)", "furo (>=2022.6.21)"]
[[package]]
name = "glob2"
version = "0.7"
description = "Version of the glob module that can capture patterns and supports recursive wildcards"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "importlib-metadata"
version = "4.12.0"
description = "Read metadata from Python packages"
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
zipp = ">=0.5"
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"]
perf = ["ipython"]
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"]
[[package]]
name = "iniconfig"
version = "1.1.1"
description = "iniconfig: brain-dead simple config-ini parsing"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "mako"
version = "1.2.1"
description = "A super-fast templating language that borrows the best ideas from the existing templating languages."
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
MarkupSafe = ">=0.9.2"
[package.extras]
babel = ["babel"]
lingua = ["lingua"]
testing = ["pytest"]
[[package]]
name = "markupsafe"
version = "2.1.1"
description = "Safely add untrusted strings to HTML/XML markup."
category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "mypy"
version = "0.971"
description = "Optional static typing for Python"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
mypy-extensions = ">=0.4.3"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""}
typing-extensions = ">=3.10"
[package.extras]
dmypy = ["psutil (>=4.0)"]
python2 = ["typed-ast (>=1.4.0,<2)"]
reports = ["lxml"]
[[package]]
name = "mypy-extensions"
version = "0.4.3"
description = "Experimental type system extensions for programs checked with the mypy typechecker."
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "packaging"
version = "21.3"
description = "Core utilities for Python packages"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
[[package]]
name = "parse"
version = "1.19.0"
description = "parse() is the opposite of format()"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "parse-type"
version = "0.6.0"
description = "Simplifies to build parse types based on the parse module"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*"
[package.dependencies]
parse = {version = ">=1.18.0", markers = "python_version >= \"3.0\""}
six = ">=1.11"
[package.extras]
develop = ["coverage (>=4.4)", "pytest-html (>=1.19.0)", "pytest-cov", "tox (>=2.8)", "pytest (<5.0)", "pytest (>=5.0)"]
docs = ["sphinx (>=1.2)"]
[[package]]
name = "platformdirs"
version = "2.5.2"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"]
test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"]
[[package]]
name = "pluggy"
version = "1.0.0"
description = "plugin and hook calling mechanisms for python"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "py"
version = "1.11.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "pygments"
version = "2.12.0"
description = "Pygments is a syntax highlighting package written in Python."
category = "dev"
optional = false
python-versions = ">=3.6"
[[package]]
name = "pyparsing"
version = "3.0.9"
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
category = "main"
optional = false
python-versions = ">=3.6.8"
[package.extras]
diagrams = ["railroad-diagrams", "jinja2"]
[[package]]
name = "pytest"
version = "7.1.2"
description = "pytest: simple powerful testing with Python"
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<2.0"
py = ">=1.8.2"
tomli = ">=1.0.0"
[package.extras]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
[[package]]
name = "pytest-forked"
version = "1.4.0"
description = "run tests in isolated forked subprocesses"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
py = "*"
pytest = ">=3.10"
[[package]]
name = "pytest-xdist"
version = "2.5.0"
description = "pytest xdist plugin for distributed testing and loop-on-failing modes"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
execnet = ">=1.1"
pytest = ">=6.2.0"
pytest-forked = "*"
[package.extras]
psutil = ["psutil (>=3.0)"]
setproctitle = ["setproctitle"]
testing = ["filelock"]
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "tox"
version = "3.25.1"
description = "tox is a generic virtualenv management and test command line tool"
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[package.dependencies]
colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""}
filelock = ">=3.0.0"
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
packaging = ">=14"
pluggy = ">=0.12.0"
py = ">=1.4.17"
six = ">=1.14.0"
toml = ">=0.9.4"
virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7"
[package.extras]
docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"]
testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "psutil (>=5.6.1)", "pathlib2 (>=2.3.3)"]
[[package]]
name = "typed-ast"
version = "1.5.4"
description = "a fork of Python 2 and 3 ast modules with type comment support"
category = "dev"
optional = false
python-versions = ">=3.6"
[[package]]
name = "types-setuptools"
version = "64.0.1"
description = "Typing stubs for setuptools"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "typing-extensions"
version = "4.3.0"
description = "Backported and Experimental Type Hints for Python 3.7+"
category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "virtualenv"
version = "20.16.3"
description = "Virtual Python Environment builder"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
distlib = ">=0.3.5,<1"
filelock = ">=3.4.1,<4"
importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.8\""}
platformdirs = ">=2.4,<3"
[package.extras]
docs = ["proselint (>=0.13)", "sphinx (>=5.1.1)", "sphinx-argparse (>=0.3.1)", "sphinx-rtd-theme (>=1)", "towncrier (>=21.9)"]
testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"]
[[package]]
name = "zipp"
version = "3.8.1"
description = "Backport of pathlib-compatible object wrapper for zip files"
category = "main"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"]
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"]
[metadata]
lock-version = "1.1"
python-versions = "^3.7"
content-hash = "f8d199095c9f4627a22a8a7b6804d0b95fa623b40dafd7dca7615c3e4fef9b34"
[metadata.files]
atomicwrites = [
{file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"},
]
attrs = [
{file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
{file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
]
colorama = [
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
]
coverage = [
{file = "coverage-6.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f50d3a822947572496ea922ee7825becd8e3ae6fbd2400cd8236b7d64b17f285"},
{file = "coverage-6.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d5191d53afbe5b6059895fa7f58223d3751c42b8101fb3ce767e1a0b1a1d8f87"},
{file = "coverage-6.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04010af3c06ce2bfeb3b1e4e05d136f88d88c25f76cd4faff5d1fd84d11581ea"},
{file = "coverage-6.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6630d8d943644ea62132789940ca97d05fac83f73186eaf0930ffa715fbdab6b"},
{file = "coverage-6.4.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05de0762c1caed4a162b3e305f36cf20a548ff4da0be6766ad5c870704be3660"},
{file = "coverage-6.4.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0e3a41aad5919613483aad9ebd53336905cab1bd6788afd3995c2a972d89d795"},
{file = "coverage-6.4.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a2738ba1ee544d6f294278cfb6de2dc1f9a737a780469b5366e662a218f806c3"},
{file = "coverage-6.4.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a0d2df4227f645a879010461df2cea6b7e3fb5a97d7eafa210f7fb60345af9e8"},
{file = "coverage-6.4.3-cp310-cp310-win32.whl", hash = "sha256:73a10939dc345460ca0655356a470dd3de9759919186a82383c87b6eb315faf2"},
{file = "coverage-6.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:53c8edd3b83a4ddba3d8c506f1359401e7770b30f2188f15c17a338adf5a14db"},
{file = "coverage-6.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f1eda5cae434282712e40b42aaf590b773382afc3642786ac3ed39053973f61f"},
{file = "coverage-6.4.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59fc88bc13e30f25167e807b8cad3c41b7218ef4473a20c86fd98a7968733083"},
{file = "coverage-6.4.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75314b00825d70e1e34b07396e23f47ed1d4feedc0122748f9f6bd31a544840"},
{file = "coverage-6.4.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52f8b9fcf3c5e427d51bbab1fb92b575a9a9235d516f175b24712bcd4b5be917"},
{file = "coverage-6.4.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5a559aab40c716de80c7212295d0dc96bc1b6c719371c20dd18c5187c3155518"},
{file = "coverage-6.4.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:306788fd019bb90e9cbb83d3f3c6becad1c048dd432af24f8320cf38ac085684"},
{file = "coverage-6.4.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:920a734fe3d311ca01883b4a19aa386c97b82b69fbc023458899cff0a0d621b9"},
{file = "coverage-6.4.3-cp37-cp37m-win32.whl", hash = "sha256:ab9ef0187d6c62b09dec83a84a3b94f71f9690784c84fd762fb3cf2d2b44c914"},
{file = "coverage-6.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:39ebd8e120cb77a06ee3d5fc26f9732670d1c397d7cd3acf02f6f62693b89b80"},
{file = "coverage-6.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc698580216050b5f4a34d2cdd2838b429c53314f1c4835fab7338200a8396f2"},
{file = "coverage-6.4.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:877ee5478fd78e100362aed56db47ccc5f23f6e7bb035a8896855f4c3e49bc9b"},
{file = "coverage-6.4.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:555a498999c44f5287cc95500486cd0d4f021af9162982cbe504d4cb388f73b5"},
{file = "coverage-6.4.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eff095a5aac7011fdb51a2c82a8fae9ec5211577f4b764e1e59cfa27ceeb1b59"},
{file = "coverage-6.4.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5de1e9335e2569974e20df0ce31493d315a830d7987e71a24a2a335a8d8459d3"},
{file = "coverage-6.4.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7856ea39059d75f822ff0df3a51ea6d76307c897048bdec3aad1377e4e9dca20"},
{file = "coverage-6.4.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:411fdd9f4203afd93b056c0868c8f9e5e16813e765de962f27e4e5798356a052"},
{file = "coverage-6.4.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cdf7b83f04a313a21afb1f8730fe4dd09577fefc53bbdfececf78b2006f4268e"},
{file = "coverage-6.4.3-cp38-cp38-win32.whl", hash = "sha256:ab2b1a89d2bc7647622e9eaf06128a5b5451dccf7c242deaa31420b055716481"},
{file = "coverage-6.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:0e34247274bde982bbc613894d33f9e36358179db2ed231dd101c48dd298e7b0"},
{file = "coverage-6.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b104b6b1827d6a22483c469e3983a204bcf9c6bf7544bf90362c4654ebc2edf3"},
{file = "coverage-6.4.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:adf1a0d272633b21d645dd6e02e3293429c1141c7d65a58e4cbcd592d53b8e01"},
{file = "coverage-6.4.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff9832434a9193fbd716fbe05f9276484e18d26cc4cf850853594bb322807ac3"},
{file = "coverage-6.4.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:923f9084d7e1d31b5f74c92396b05b18921ed01ee5350402b561a79dce3ea48d"},
{file = "coverage-6.4.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d64304acf79766e650f7acb81d263a3ea6e2d0d04c5172b7189180ff2c023c"},
{file = "coverage-6.4.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fc294de50941d3da66a09dca06e206297709332050973eca17040278cb0918ff"},
{file = "coverage-6.4.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a42eaaae772f14a5194f181740a67bfd48e8806394b8c67aa4399e09d0d6b5db"},
{file = "coverage-6.4.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4822327b35cb032ff16af3bec27f73985448f08e874146b5b101e0e558b613dd"},
{file = "coverage-6.4.3-cp39-cp39-win32.whl", hash = "sha256:f217850ac0e046ede611312703423767ca032a7b952b5257efac963942c055de"},
{file = "coverage-6.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:0a84376e4fd13cebce2c0ef8c2f037929c8307fb94af1e5dbe50272a1c651b5d"},
{file = "coverage-6.4.3-pp36.pp37.pp38-none-any.whl", hash = "sha256:068d6f2a893af838291b8809c876973d885543411ea460f3e6886ac0ee941732"},
{file = "coverage-6.4.3.tar.gz", hash = "sha256:ec2ae1f398e5aca655b7084392d23e80efb31f7a660d2eecf569fb9f79b3fb94"},
]
distlib = [
{file = "distlib-0.3.5-py2.py3-none-any.whl", hash = "sha256:b710088c59f06338ca514800ad795a132da19fda270e3ce4affc74abf955a26c"},
{file = "distlib-0.3.5.tar.gz", hash = "sha256:a7f75737c70be3b25e2bee06288cec4e4c221de18455b2dd037fe2a795cab2fe"},
]
execnet = [
{file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"},
{file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"},
]
filelock = [
{file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"},
{file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"},
]
glob2 = [
{file = "glob2-0.7.tar.gz", hash = "sha256:85c3dbd07c8aa26d63d7aacee34fa86e9a91a3873bc30bf62ec46e531f92ab8c"},
]
importlib-metadata = [
{file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"},
{file = "importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670"},
]
iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
mako = [
{file = "Mako-1.2.1-py3-none-any.whl", hash = "sha256:df3921c3081b013c8a2d5ff03c18375651684921ae83fd12e64800b7da923257"},
{file = "Mako-1.2.1.tar.gz", hash = "sha256:f054a5ff4743492f1aa9ecc47172cb33b42b9d993cffcc146c9de17e717b0307"},
]
markupsafe = [
{file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"},
{file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"},
{file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"},
{file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"},
{file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"},
{file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"},
{file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"},
{file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"},
{file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"},
{file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"},
{file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"},
{file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"},
{file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"},
{file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"},
{file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"},
{file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"},
{file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"},
{file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"},
{file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"},
{file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"},
{file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"},
{file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"},
{file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"},
{file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"},
{file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"},
{file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"},
{file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"},
{file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"},
{file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"},
{file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"},
{file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"},
]
mypy = [
{file = "mypy-0.971-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2899a3cbd394da157194f913a931edfd4be5f274a88041c9dc2d9cdcb1c315c"},
{file = "mypy-0.971-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:98e02d56ebe93981c41211c05adb630d1d26c14195d04d95e49cd97dbc046dc5"},
{file = "mypy-0.971-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:19830b7dba7d5356d3e26e2427a2ec91c994cd92d983142cbd025ebe81d69cf3"},
{file = "mypy-0.971-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:02ef476f6dcb86e6f502ae39a16b93285fef97e7f1ff22932b657d1ef1f28655"},
{file = "mypy-0.971-cp310-cp310-win_amd64.whl", hash = "sha256:25c5750ba5609a0c7550b73a33deb314ecfb559c350bb050b655505e8aed4103"},
{file = "mypy-0.971-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d3348e7eb2eea2472db611486846742d5d52d1290576de99d59edeb7cd4a42ca"},
{file = "mypy-0.971-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3fa7a477b9900be9b7dd4bab30a12759e5abe9586574ceb944bc29cddf8f0417"},
{file = "mypy-0.971-cp36-cp36m-win_amd64.whl", hash = "sha256:2ad53cf9c3adc43cf3bea0a7d01a2f2e86db9fe7596dfecb4496a5dda63cbb09"},
{file = "mypy-0.971-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:855048b6feb6dfe09d3353466004490b1872887150c5bb5caad7838b57328cc8"},
{file = "mypy-0.971-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:23488a14a83bca6e54402c2e6435467a4138785df93ec85aeff64c6170077fb0"},
{file = "mypy-0.971-cp37-cp37m-win_amd64.whl", hash = "sha256:4b21e5b1a70dfb972490035128f305c39bc4bc253f34e96a4adf9127cf943eb2"},
{file = "mypy-0.971-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9796a2ba7b4b538649caa5cecd398d873f4022ed2333ffde58eaf604c4d2cb27"},
{file = "mypy-0.971-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a361d92635ad4ada1b1b2d3630fc2f53f2127d51cf2def9db83cba32e47c856"},
{file = "mypy-0.971-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b793b899f7cf563b1e7044a5c97361196b938e92f0a4343a5d27966a53d2ec71"},
{file = "mypy-0.971-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d1ea5d12c8e2d266b5fb8c7a5d2e9c0219fedfeb493b7ed60cd350322384ac27"},
{file = "mypy-0.971-cp38-cp38-win_amd64.whl", hash = "sha256:23c7ff43fff4b0df93a186581885c8512bc50fc4d4910e0f838e35d6bb6b5e58"},
{file = "mypy-0.971-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1f7656b69974a6933e987ee8ffb951d836272d6c0f81d727f1d0e2696074d9e6"},
{file = "mypy-0.971-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2022bfadb7a5c2ef410d6a7c9763188afdb7f3533f22a0a32be10d571ee4bbe"},
{file = "mypy-0.971-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef943c72a786b0f8d90fd76e9b39ce81fb7171172daf84bf43eaf937e9f220a9"},
{file = "mypy-0.971-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d744f72eb39f69312bc6c2abf8ff6656973120e2eb3f3ec4f758ed47e414a4bf"},
{file = "mypy-0.971-cp39-cp39-win_amd64.whl", hash = "sha256:77a514ea15d3007d33a9e2157b0ba9c267496acf12a7f2b9b9f8446337aac5b0"},
{file = "mypy-0.971-py3-none-any.whl", hash = "sha256:0d054ef16b071149917085f51f89555a576e2618d5d9dd70bd6eea6410af3ac9"},
{file = "mypy-0.971.tar.gz", hash = "sha256:40b0f21484238269ae6a57200c807d80debc6459d444c0489a102d7c6a75fa56"},
]
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
packaging = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
]
parse = [
{file = "parse-1.19.0.tar.gz", hash = "sha256:9ff82852bcb65d139813e2a5197627a94966245c897796760a3a2a8eb66f020b"},
]
parse-type = [
{file = "parse_type-0.6.0-py2.py3-none-any.whl", hash = "sha256:c148e88436bd54dab16484108e882be3367f44952c649c9cd6b82a7370b650cb"},
{file = "parse_type-0.6.0.tar.gz", hash = "sha256:20b43c660e48ed47f433bce5873a2a3d4b9b6a7ba47bd7f7d2a7cec4bec5551f"},
]
platformdirs = [
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
{file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
]
pluggy = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
py = [
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
]
pygments = [
{file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"},
{file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"},
]
pyparsing = [
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
]
pytest = [
{file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"},
{file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"},
]
pytest-forked = [
{file = "pytest-forked-1.4.0.tar.gz", hash = "sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e"},
{file = "pytest_forked-1.4.0-py3-none-any.whl", hash = "sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8"},
]
pytest-xdist = [
{file = "pytest-xdist-2.5.0.tar.gz", hash = "sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf"},
{file = "pytest_xdist-2.5.0-py3-none-any.whl", hash = "sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65"},
]
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
tomli = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
tox = [
{file = "tox-3.25.1-py2.py3-none-any.whl", hash = "sha256:c38e15f4733683a9cc0129fba078633e07eb0961f550a010ada879e95fb32632"},
{file = "tox-3.25.1.tar.gz", hash = "sha256:c138327815f53bc6da4fe56baec5f25f00622ae69ef3fe4e1e385720e22486f9"},
]
typed-ast = [
{file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"},
{file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"},
{file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"},
{file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"},
{file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"},
{file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"},
{file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"},
{file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"},
{file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"},
{file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"},
{file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"},
{file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"},
{file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"},
{file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"},
{file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"},
{file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"},
{file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"},
{file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"},
{file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"},
{file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"},
{file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"},
{file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"},
{file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"},
{file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"},
]
types-setuptools = [
{file = "types-setuptools-64.0.1.tar.gz", hash = "sha256:8290b6bf1d916e6b007784d5cbcd112a1af9a2d76343231fcce0a55185343702"},
{file = "types_setuptools-64.0.1-py3-none-any.whl", hash = "sha256:005ccb8a1a7d0dce61cd63081dad0ddc599af4413bb49ce1333119a78a7bb2e1"},
]
typing-extensions = [
{file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"},
{file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"},
]
virtualenv = [
{file = "virtualenv-20.16.3-py2.py3-none-any.whl", hash = "sha256:4193b7bc8a6cd23e4eb251ac64f29b4398ab2c233531e66e40b19a6b7b0d30c1"},
{file = "virtualenv-20.16.3.tar.gz", hash = "sha256:d86ea0bb50e06252d79e6c241507cb904fcd66090c3271381372d6221a3970f9"},
]
zipp = [
{file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"},
{file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"},
]

View File

@ -1,6 +1,58 @@
[tool.poetry]
name = "pytest-bdd"
version = "6.0.1"
description = "BDD for pytest"
authors = ["Oleg Pidsadnyi <oleg.pidsadnyi@gmail.com>", "Anatoly Bubenkov <bubenkoff@gmail.com>"]
maintainers = ["Alessio Bogon <778703+youtux@users.noreply.github.com>"]
license = "MIT"
readme = "README.rst"
homepage = "https://pytest-bdd.readthedocs.io/"
documentation = "https://pytest-bdd.readthedocs.io/"
repository = "https://github.com/pytest-dev/pytest-bdd"
classifiers = [
"Development Status :: 6 - Mature",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: POSIX",
"Operating System :: Microsoft :: Windows",
"Operating System :: MacOS :: MacOS X",
"Topic :: Software Development :: Testing",
"Topic :: Software Development :: Libraries",
"Topic :: Utilities",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
]
[tool.poetry.plugins."pytest11"]
"pytest-bdd" = "pytest_bdd.plugin"
[tool.poetry.scripts]
"pytest-bdd" = "pytest_bdd.scripts:main"
[tool.poetry.dependencies]
python = "^3.7"
glob2 = "*"
Mako = "*"
parse = "*"
parse-type = "*"
pytest = ">=6.2.0"
typing-extensions = "*"
[tool.poetry.dev-dependencies]
tox = "^3.25.1"
mypy = "^0.971"
types-setuptools = "^64.0.1"
pytest-xdist = "^2.5.0"
coverage = {extras = ["toml"], version = "^6.4.3"}
Pygments = "^2.12.0" # for code-block highlighting
[build-system]
requires = ["setuptools>=58", "wheel", "tatsu"]
build-backend = "setuptools.build_meta"
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.black]
line-length = 120
@ -11,11 +63,24 @@ profile = "black"
line_length = 120
multi_line_output = 3
[tool.coverage.report]
exclude_lines = [
"if TYPE_CHECKING:",
"if typing\\.TYPE_CHECKING:",
]
[tool.coverage.run]
branch = true
include =[
"src/pytest_bdd/*",
"tests/*",
]
[tool.mypy]
python_version = "3.7"
warn_return_any = true
warn_unused_configs = true
files = "pytest_bdd/**/*.py"
files = "src/pytest_bdd/**/*.py"
[[tool.mypy.overrides]]
module = ["parse", "parse_type", "glob2"]

View File

@ -1,2 +0,0 @@
packaging
more-itertools

View File

@ -1,55 +0,0 @@
[metadata]
name = pytest-bdd
description = BDD for pytest
long_description = file: README.rst, AUTHORS.rst
long_description_content_type = text/x-rst
author = Oleg Pidsadnyi, Anatoly Bubenkov and others
license = MIT license
author_email = oleg.pidsadnyi@gmail.com
url = https://github.com/pytest-dev/pytest-bdd
version = attr: pytest_bdd.__version__
classifiers =
Development Status :: 6 - Mature
Intended Audience :: Developers
License :: OSI Approved :: MIT License
Operating System :: POSIX
Operating System :: Microsoft :: Windows
Operating System :: MacOS :: MacOS X
Topic :: Software Development :: Testing
Topic :: Software Development :: Libraries
Topic :: Utilities
Programming Language :: Python :: 3
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
[options]
python_requires = >=3.7
install_requires =
lark
glob2
Mako
parse
parse_type
py
pytest>=5.0
tatsu
package_dir =
= src
packages = pytest_bdd
include_package_data = True
cmdclass =
build_py = build_parser.build_py
[options.extras_require]
testing =
tox
mypy==0.910
[options.entry_points]
pytest11 =
pytest-bdd = pytest_bdd.plugin
console_scripts =
pytest-bdd = pytest_bdd.scripts:main

View File

@ -1,3 +0,0 @@
from setuptools import setup
setup()

View File

@ -2,8 +2,6 @@
from __future__ import annotations
from pytest_bdd.scenario import scenario, scenarios
from pytest_bdd.steps import given, then, when
from pytest_bdd.steps import given, step, then, when
__version__ = "6.0.0"
__all__ = ["given", "when", "then", "scenario", "scenarios"]
__all__ = ["given", "when", "step", "then", "scenario", "scenarios"]

View File

@ -9,7 +9,7 @@ import py
from mako.lookup import TemplateLookup
from .feature import get_features
from .scenario import find_argumented_step_fixture_name, make_python_docstring, make_python_name, make_string_literal
from .scenario import inject_fixturedefs_for_step, make_python_docstring, make_python_name, make_string_literal
from .steps import get_step_fixture_name
from .types import STEP_TYPES
@ -124,18 +124,12 @@ def print_missing_code(scenarios: list[ScenarioTemplate], steps: list[Step]) ->
def _find_step_fixturedef(
fixturemanager: FixtureManager, item: Function, name: str, type_: str
fixturemanager: FixtureManager, item: Function, step: Step
) -> Sequence[FixtureDef[Any]] | None:
"""Find step fixturedef."""
step_fixture_name = get_step_fixture_name(name, type_)
fixturedefs = fixturemanager.getfixturedefs(step_fixture_name, item.nodeid)
if fixturedefs is not None:
return fixturedefs
argumented_step_name = find_argumented_step_fixture_name(name, type_, fixturemanager)
if argumented_step_name is not None:
return fixturemanager.getfixturedefs(argumented_step_name, item.nodeid)
return None
with inject_fixturedefs_for_step(step=step, fixturemanager=fixturemanager, nodeid=item.nodeid):
bdd_name = get_step_fixture_name(step=step)
return fixturemanager.getfixturedefs(bdd_name, item.nodeid)
def parse_feature_files(paths: list[str], **kwargs: Any) -> tuple[list[Feature], list[ScenarioTemplate], list[Step]]:
@ -193,7 +187,7 @@ def _show_missing_code_main(config: Config, session: Session) -> None:
if scenario in scenarios:
scenarios.remove(scenario)
for step in scenario.steps:
fixturedefs = _find_step_fixturedef(fm, item, step.name, step.type)
fixturedefs = _find_step_fixturedef(fm, item, step=step)
if fixturedefs:
try:
steps.remove(step)

View File

@ -9,8 +9,7 @@ from collections import OrderedDict
from dataclasses import dataclass, field
from typing import cast
from . import types
from .exceptions import FeatureError
from . import exceptions, types
if typing.TYPE_CHECKING:
from typing import Any, Iterable, Mapping, Match, Sequence
@ -137,7 +136,7 @@ def parse_feature(basedir: str, filename: str, encoding: str = "utf-8") -> Featu
allowed_prev_mode = (types.BACKGROUND, types.GIVEN, types.WHEN)
if not scenario and prev_mode not in allowed_prev_mode and mode in types.STEP_TYPES:
raise FeatureError(
raise exceptions.FeatureError(
"Step definition outside of a Scenario or a Background", line_number, clean_line, filename
)
@ -149,7 +148,7 @@ def parse_feature(basedir: str, filename: str, encoding: str = "utf-8") -> Featu
elif prev_mode == types.FEATURE:
description.append(clean_line)
else:
raise FeatureError(
raise exceptions.FeatureError(
"Multiple features are not allowed in a single feature file",
line_number,
clean_line,
@ -160,11 +159,17 @@ def parse_feature(basedir: str, filename: str, encoding: str = "utf-8") -> Featu
# Remove Feature, Given, When, Then, And
keyword, parsed_line = parse_line(clean_line)
if mode in [types.SCENARIO, types.SCENARIO_OUTLINE]:
tags = get_tags(prev_line)
feature.scenarios[parsed_line] = scenario = ScenarioTemplate(
feature=feature, name=parsed_line, line_number=line_number, tags=tags
scenario = ScenarioTemplate(
feature=feature,
name=parsed_line,
line_number=line_number,
tags=tags,
templated=mode == types.SCENARIO_OUTLINE,
)
feature.scenarios[parsed_line] = scenario
elif mode == types.BACKGROUND:
feature.background = Background(feature=feature, line_number=line_number)
elif mode == types.EXAMPLES:
@ -222,6 +227,7 @@ class ScenarioTemplate:
feature: Feature
name: str
line_number: int
templated: bool
tags: set[str] = field(default_factory=set)
examples: Examples | None = field(default_factory=lambda: Examples())
_steps: list[Step] = field(init=False, default_factory=list)
@ -246,16 +252,21 @@ class ScenarioTemplate:
return (background.steps if background else []) + self._steps
def render(self, context: Mapping[str, Any]) -> Scenario:
steps = [
Step(
name=templated_step.render(context),
type=templated_step.type,
indent=templated_step.indent,
line_number=templated_step.line_number,
keyword=templated_step.keyword,
)
for templated_step in self.steps
]
background_steps = self.feature.background.steps if self.feature.background else []
if not self.templated:
scenario_steps = self._steps
else:
scenario_steps = [
Step(
name=step.render(context),
type=step.type,
indent=step.indent,
line_number=step.line_number,
keyword=step.keyword,
)
for step in self._steps
]
steps = background_steps + scenario_steps
return Scenario(feature=self.feature, name=self.name, line_number=self.line_number, steps=steps, tags=self.tags)
def validate(self):
@ -311,13 +322,10 @@ class Step:
datatable: list[tuple[str, ...]] | None = None,
):
self.type = type
self.name = name
self.line_number = line_number
self.indent = indent
self.keyword = keyword
self.docstring = docstring
self.datatable = datatable
self.failed = False
self.scenario = None
self.background = None
self.lines = []

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import abc
import re as base_re
from typing import Any, Dict, cast
from typing import Any, Dict, TypeVar, cast, overload
import parse as base_parse
from parse_type import cfparse as base_cfparse
@ -42,14 +42,14 @@ class re(StepParser):
:return: `dict` of step arguments
"""
match = self.regex.match(name)
match = self.regex.fullmatch(name)
if match is None:
return None
return match.groupdict()
def is_matching(self, name: str) -> bool:
"""Match given name with the step name."""
return bool(self.regex.match(name))
return bool(self.regex.fullmatch(name))
class parse(StepParser):
@ -99,7 +99,20 @@ class string(StepParser):
return self.name == name
def get_parser(step_name: Any) -> StepParser:
TStepParser = TypeVar("TStepParser", bound=StepParser)
@overload
def get_parser(step_name: str) -> string:
...
@overload
def get_parser(step_name: TStepParser) -> TStepParser:
...
def get_parser(step_name: str | StepParser) -> StepParser:
"""Get parser by given name."""
if isinstance(step_name, StepParser):

View File

@ -31,7 +31,7 @@ def pytest_addhooks(pluginmanager: PytestPluginManager) -> None:
@given("trace")
@when("trace")
@then("trace")
def trace() -> None:
def _() -> None:
"""Enter pytest's pdb trace."""
pytest.set_trace()
@ -63,7 +63,7 @@ def add_bdd_ini(parser: Parser) -> None:
parser.addini("bdd_parser", "Parser to use.", default="tatsu")
@pytest.mark.trylast
@pytest.hookimpl(trylast=True)
def pytest_configure(config: Config) -> None:
"""Configure all subplugins."""
CONFIG_STACK.append(config)
@ -77,18 +77,18 @@ def pytest_unconfigure(config: Config) -> None:
cucumber_json.unconfigure(config)
@pytest.mark.hookwrapper
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item: Item, call: CallInfo) -> Generator[None, _Result, None]:
outcome = yield
reporting.runtest_makereport(item, call, outcome.get_result())
@pytest.mark.tryfirst
@pytest.hookimpl(tryfirst=True)
def pytest_bdd_before_scenario(request: FixtureRequest, feature: Feature, scenario: Scenario) -> None:
reporting.before_scenario(request, feature, scenario)
@pytest.mark.tryfirst
@pytest.hookimpl(tryfirst=True)
def pytest_bdd_step_error(
request: FixtureRequest,
feature: Feature,
@ -101,14 +101,14 @@ def pytest_bdd_step_error(
reporting.step_error(request, feature, scenario, step, step_func, step_func_args, exception)
@pytest.mark.tryfirst
@pytest.hookimpl(tryfirst=True)
def pytest_bdd_before_step(
request: FixtureRequest, feature: Feature, scenario: Scenario, step: Step, step_func: Callable
) -> None:
reporting.before_step(request, feature, scenario, step, step_func)
@pytest.mark.tryfirst
@pytest.hookimpl(tryfirst=True)
def pytest_bdd_after_step(
request: FixtureRequest,
feature: Feature,

View File

@ -12,17 +12,19 @@ test_publish_article = scenario(
"""
from __future__ import annotations
import collections
import contextlib
import logging
import os
import re
from typing import TYPE_CHECKING, Callable, cast
from typing import TYPE_CHECKING, Callable, Iterator, cast
import pytest
from _pytest.fixtures import FixtureLookupError, FixtureManager, FixtureRequest, call_fixture_func
from _pytest.fixtures import FixtureDef, FixtureManager, FixtureRequest, call_fixture_func
from _pytest.nodes import iterparentnodeids
from . import exceptions
from .feature import get_feature, get_features
from .steps import get_step_fixture_name, inject_fixture
from .steps import StepFunctionContext, get_step_fixture_name, inject_fixture
from .utils import CONFIG_STACK, get_args, get_caller_module_locals, get_caller_module_path
if TYPE_CHECKING:
@ -32,104 +34,136 @@ if TYPE_CHECKING:
from .parser import Feature, Scenario, ScenarioTemplate, Step
logger = logging.getLogger(__name__)
PYTHON_REPLACE_REGEX = re.compile(r"\W")
ALPHA_REGEX = re.compile(r"^\d+_*")
def find_argumented_step_fixture_name(
name: str, type_: str, fixturemanager: FixtureManager, request: FixtureRequest | None = None
) -> str | None:
"""Find argumented step fixture name."""
def find_fixturedefs_for_step(step: Step, fixturemanager: FixtureManager, nodeid: str) -> Iterable[FixtureDef[Any]]:
"""Find the fixture defs that can parse a step."""
# happens to be that _arg2fixturedefs is changed during the iteration so we use a copy
for fixturename, fixturedefs in list(fixturemanager._arg2fixturedefs.items()):
for fixturedef in fixturedefs:
parser = getattr(fixturedef.func, "parser", None)
if parser is None:
fixture_def_by_name = list(fixturemanager._arg2fixturedefs.items())
for i, (fixturename, fixturedefs) in enumerate(fixture_def_by_name):
for pos, fixturedef in enumerate(fixturedefs):
step_func_context = getattr(fixturedef.func, "_pytest_bdd_step_context", None)
if step_func_context is None:
continue
match = parser.is_matching(name)
if step_func_context.type is not None and step_func_context.type != step.type:
continue
match = step_func_context.parser.is_matching(step.name)
if not match:
continue
parser_name = get_step_fixture_name(parser.name, type_)
if request:
try:
request.getfixturevalue(parser_name)
except FixtureLookupError:
continue
return parser_name
return None
if fixturedef not in (fixturemanager.getfixturedefs(fixturename, nodeid) or []):
continue
yield fixturedef
def _find_step_function(request: FixtureRequest, step: Step, scenario: Scenario) -> Any:
"""Match the step defined by the regular expression pattern.
@contextlib.contextmanager
def inject_fixturedefs_for_step(step: Step, fixturemanager: FixtureManager, nodeid: str) -> Iterator[None]:
"""Inject fixture definitions that can parse a step.
:param request: PyTest request object.
:param step: Step.
:param scenario: Scenario.
We fist iterate over all the fixturedefs that can parse the step.
:return: Function of the step.
:rtype: function
Then we sort them by their "path" (list of parent IDs) so that we respect the fixture scoping rules.
Finally, we inject them into the request.
"""
name = step.name
bdd_name = get_step_fixture_name(step=step)
fixturedefs = list(find_fixturedefs_for_step(step=step, fixturemanager=fixturemanager, nodeid=nodeid))
# Sort the fixture definitions by their "path", so that the `bdd_name` fixture will
# respect the fixture scope
def get_fixture_path(fixture_def: FixtureDef) -> list[str]:
return list(iterparentnodeids(fixture_def.baseid))
fixturedefs.sort(key=lambda x: get_fixture_path(x))
if not fixturedefs:
yield
return
logger.debug("Adding providers for fixture %r: %r", bdd_name, fixturedefs)
fixturemanager._arg2fixturedefs[bdd_name] = fixturedefs
try:
# Simple case where no parser is used for the step
return request.getfixturevalue(get_step_fixture_name(name, step.type))
except FixtureLookupError:
try:
# Could not find a fixture with the same name, let's see if there is a parser involved
argumented_name = find_argumented_step_fixture_name(name, step.type, request._fixturemanager, request)
if argumented_name:
return request.getfixturevalue(argumented_name)
raise
except FixtureLookupError:
raise exceptions.StepDefinitionNotFoundError(
f"Step definition is not found: {step}. "
f'Line {step.line_number} in scenario "{scenario.name}" in the feature "{scenario.feature.filename}"'
)
yield
finally:
del fixturemanager._arg2fixturedefs[bdd_name]
def _execute_step_function(request: FixtureRequest, scenario: Scenario, step: Step, step_func: Callable) -> None:
"""Execute step function.
def get_step_function(request, step: Step) -> StepFunctionContext | None:
"""Get the step function (context) for the given step.
:param request: PyTest request.
:param scenario: Scenario.
:param step: Step.
:param function step_func: Step function.
:param example: Example table.
We first figure out what's the step fixture name that we have to inject.
Then we let `patch_argumented_step_functions` find out what step definition fixtures can parse the current step,
and it will inject them for the step fixture name.
Finally we let request.getfixturevalue(...) fetch the step definition fixture.
"""
kw = dict(request=request, feature=scenario.feature, scenario=scenario, step=step, step_func=step_func)
bdd_name = get_step_fixture_name(step=step)
with inject_fixturedefs_for_step(step=step, fixturemanager=request._fixturemanager, nodeid=request.node.nodeid):
try:
return cast(StepFunctionContext, request.getfixturevalue(bdd_name))
except pytest.FixtureLookupError:
return None
def _execute_step_function(
request: FixtureRequest, scenario: Scenario, step: Step, context: StepFunctionContext
) -> None:
"""Execute step function."""
kw = {
"request": request,
"feature": scenario.feature,
"scenario": scenario,
"step": step,
"step_func": context.step_func,
"step_func_args": {},
}
request.config.hook.pytest_bdd_before_step(**kw)
kw["step_func_args"] = {}
# Get the step argument values.
converters = context.converters
kwargs = {}
args = get_args(context.step_func)
try:
# Get the step argument values.
converters = getattr(step_func, "converters", {})
kwargs = {}
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}"
)
for arg, value in parsed_args.items():
if arg in converters:
value = converters[arg](value)
kwargs[arg] = value
parser = getattr(step_func, "parser", None)
if parser is not None:
for arg, value in parser.parse_arguments(step.name).items():
if arg in converters:
value = converters[arg](value)
kwargs[arg] = value
kwargs = {arg: kwargs[arg] if arg in kwargs else request.getfixturevalue(arg) for arg in get_args(step_func)}
kwargs = {arg: kwargs[arg] if arg in kwargs else request.getfixturevalue(arg) for arg in args}
kw["step_func_args"] = kwargs
request.config.hook.pytest_bdd_before_step_call(**kw)
target_fixture = getattr(step_func, "target_fixture", None)
# Execute the step as if it was a pytest fixture, so that we can allow "yield" statements in it
return_value = call_fixture_func(fixturefunc=step_func, request=request, kwargs=kwargs)
if target_fixture:
inject_fixture(request, target_fixture, return_value)
request.config.hook.pytest_bdd_after_step(**kw)
return_value = call_fixture_func(fixturefunc=context.step_func, request=request, kwargs=kwargs)
except Exception as exception:
request.config.hook.pytest_bdd_step_error(exception=exception, **kw)
raise
if context.target_fixture is not None:
inject_fixture(request, context.target_fixture, return_value)
request.config.hook.pytest_bdd_after_step(**kw)
def _execute_scenario(feature: Feature, scenario: Scenario, request: FixtureRequest) -> None:
"""Execute the scenario.
@ -141,22 +175,22 @@ def _execute_scenario(feature: Feature, scenario: Scenario, request: FixtureRequ
"""
request.config.hook.pytest_bdd_before_scenario(request=request, feature=feature, scenario=scenario)
try:
# Execute scenario steps
for step in scenario.steps:
try:
step_func = _find_step_function(request, step, scenario)
except exceptions.StepDefinitionNotFoundError as exception:
request.config.hook.pytest_bdd_step_func_lookup_error(
request=request, feature=feature, scenario=scenario, step=step, exception=exception
)
raise
_execute_step_function(request, scenario, step, step_func)
finally:
request.config.hook.pytest_bdd_after_scenario(request=request, feature=feature, scenario=scenario)
for step in scenario.steps:
step_func_context = get_step_function(request=request, step=step)
if step_func_context is None:
exc = exceptions.StepDefinitionNotFoundError(
f"Step definition is not found: {step}. "
f'Line {step.line_number} in scenario "{scenario.name}" in the feature "{scenario.feature.filename}"'
)
request.config.hook.pytest_bdd_step_func_lookup_error(
request=request, feature=feature, scenario=scenario, step=step, exception=exc
)
raise exc
FakeRequest = collections.namedtuple("FakeRequest", ["module"])
try:
_execute_step_function(request, scenario, step, step_func_context)
finally:
request.config.hook.pytest_bdd_after_scenario(request=request, feature=feature, scenario=scenario)
def _get_scenario_decorator(

View File

@ -3,17 +3,17 @@
Example:
@given("I have an article", target_fixture="article")
def given_article(author):
def _(author):
return create_test_article(author=author)
@when("I go to the article page")
def go_to_the_article_page(browser, article):
def _(browser, article):
browser.visit(urljoin(browser.url, "/articles/{0}/".format(article.id)))
@then("I should not see the error message")
def no_error_message(browser):
def _(browser):
with pytest.raises(ElementDoesNotExist):
browser.find_by_css(".message.error").first
@ -22,7 +22,7 @@ Multiple names for the steps:
@given("I have an article")
@given("there is an article")
def article(author):
def _(author):
return create_test_article(author=author)
@ -30,40 +30,54 @@ Reusing existing fixtures for a different step name:
@given("I have a beautiful article")
def given_beautiful_article(article):
def _(article):
pass
"""
from __future__ import annotations
import typing
import enum
from dataclasses import dataclass, field
from itertools import count
from typing import Any, Callable, Iterable, TypeVar
import pytest
from _pytest.fixtures import FixtureDef, FixtureRequest
from typing_extensions import Literal
from .parsers import get_parser
from .parser import Step
from .parsers import StepParser, get_parser
from .types import GIVEN, THEN, WHEN
from .utils import get_caller_module_locals
if typing.TYPE_CHECKING:
from typing import Any, Callable
TCallable = TypeVar("TCallable", bound=Callable[..., Any])
def get_step_fixture_name(name: str, type_: str) -> str:
"""Get step fixture name.
@enum.unique
class StepNamePrefix(enum.Enum):
step_def = "pytestbdd_stepdef"
step_impl = "pytestbdd_stepimpl"
:param name: string
:param type: step type
:return: step fixture name
:rtype: string
"""
return f"pytestbdd_{type_}_{name}"
@dataclass
class StepFunctionContext:
type: Literal["given", "when", "then"] | None
step_func: Callable[..., Any]
parser: StepParser
converters: dict[str, Callable[..., Any]] = field(default_factory=dict)
target_fixture: str | None = None
def get_step_fixture_name(step: Step) -> str:
"""Get step fixture name"""
return f"{StepNamePrefix.step_impl.value}_{step.type}_{step.name}"
def given(
name: Any,
name: str | StepParser,
converters: dict[str, Callable] | None = None,
target_fixture: str | None = None,
stacklevel: int = 1,
) -> Callable:
"""Given step decorator.
@ -71,86 +85,122 @@ def given(
:param converters: Optional `dict` of the argument or parameter converters in form
{<param_name>: <converter function>}.
:param target_fixture: Target fixture name to replace by steps definition function.
:param stacklevel: Stack level to find the caller frame. This is used when injecting the step definition fixture.
:return: Decorator function for the step.
"""
return _step_decorator(GIVEN, name, converters=converters, target_fixture=target_fixture)
return step(name, GIVEN, converters=converters, target_fixture=target_fixture, stacklevel=stacklevel)
def when(name: Any, converters: dict[str, Callable] | None = None, target_fixture: str | None = None) -> Callable:
def when(
name: str | StepParser,
converters: dict[str, Callable] | None = None,
target_fixture: str | None = None,
stacklevel: int = 1,
) -> Callable:
"""When step decorator.
:param name: Step name or a parser object.
:param converters: Optional `dict` of the argument or parameter converters in form
{<param_name>: <converter function>}.
:param target_fixture: Target fixture name to replace by steps definition function.
:param stacklevel: Stack level to find the caller frame. This is used when injecting the step definition fixture.
:return: Decorator function for the step.
"""
return _step_decorator(WHEN, name, converters=converters, target_fixture=target_fixture)
return step(name, WHEN, converters=converters, target_fixture=target_fixture, stacklevel=stacklevel)
def then(name: Any, converters: dict[str, Callable] | None = None, target_fixture: str | None = None) -> Callable:
def then(
name: str | StepParser,
converters: dict[str, Callable] | None = None,
target_fixture: str | None = None,
stacklevel: int = 1,
) -> Callable:
"""Then step decorator.
:param name: Step name or a parser object.
:param converters: Optional `dict` of the argument or parameter converters in form
{<param_name>: <converter function>}.
:param target_fixture: Target fixture name to replace by steps definition function.
:param stacklevel: Stack level to find the caller frame. This is used when injecting the step definition fixture.
:return: Decorator function for the step.
"""
return _step_decorator(THEN, name, converters=converters, target_fixture=target_fixture)
return step(name, THEN, converters=converters, target_fixture=target_fixture, stacklevel=stacklevel)
def _step_decorator(
step_type: str,
step_name: Any,
def step(
name: str | StepParser,
type_: Literal["given", "when", "then"] | None = None,
converters: dict[str, Callable] | None = None,
target_fixture: str | None = None,
) -> Callable:
"""Step decorator for the type and the name.
stacklevel: int = 1,
) -> Callable[[TCallable], TCallable]:
"""Generic step decorator.
:param str step_type: Step type (GIVEN, WHEN or THEN).
:param str step_name: Step name as in the feature file.
:param dict converters: Optional step arguments converters mapping
:param target_fixture: Optional fixture name to replace by step definition
:param name: Step name as in the feature file.
:param type_: Step type ("given", "when" or "then"). If None, this step will work for all the types.
:param converters: Optional step arguments converters mapping.
:param target_fixture: Optional fixture name to replace by step definition.
:param stacklevel: Stack level to find the caller frame. This is used when injecting the step definition fixture.
:return: Decorator function for the step.
Example:
>>> @step("there is an wallet", target_fixture="wallet")
>>> def _() -> dict[str, int]:
>>> return {"eur": 0, "usd": 0}
"""
if converters is None:
converters = {}
def decorator(func: Callable) -> Callable:
step_func = func
parser_instance = get_parser(step_name)
parsed_step_name = parser_instance.name
def decorator(func: TCallable) -> TCallable:
parser = get_parser(name)
step_func.__name__ = str(parsed_step_name)
context = StepFunctionContext(
type=type_,
step_func=func,
parser=parser,
converters=converters,
target_fixture=target_fixture,
)
def lazy_step_func() -> Callable:
return step_func
def step_function_marker() -> StepFunctionContext:
return context
step_func.step_type = step_type
lazy_step_func.step_type = step_type
step_function_marker._pytest_bdd_step_context = context
# Preserve the docstring
lazy_step_func.__doc__ = func.__doc__
step_func.parser = lazy_step_func.parser = parser_instance
if converters:
step_func.converters = lazy_step_func.converters = converters
step_func.target_fixture = lazy_step_func.target_fixture = target_fixture
lazy_step_func = pytest.fixture()(lazy_step_func)
fixture_step_name = get_step_fixture_name(parsed_step_name, step_type)
caller_locals = get_caller_module_locals()
caller_locals[fixture_step_name] = lazy_step_func
caller_locals = get_caller_module_locals(stacklevel=stacklevel)
fixture_step_name = find_unique_name(
f"{StepNamePrefix.step_def.value}_{type_ or '*'}_{parser.name}", seen=caller_locals.keys()
)
caller_locals[fixture_step_name] = pytest.fixture(name=fixture_step_name)(step_function_marker)
return func
return decorator
def find_unique_name(name: str, seen: Iterable[str]) -> str:
"""Find unique name among a set of strings.
New names are generated by appending an increasing number at the end of the name.
Example:
>>> find_unique_name("foo", ["foo", "foo_1"])
'foo_2'
"""
seen = set(seen)
if name not in seen:
return name
for i in count(1):
new_name = f"{name}_{i}"
if new_name not in seen:
return new_name
def inject_fixture(request: FixtureRequest, arg: str, value: Any) -> None:
"""Inject fixture into pytest fixture request.
@ -174,7 +224,9 @@ def inject_fixture(request: FixtureRequest, arg: str, value: Any) -> None:
def fin() -> None:
request._fixturemanager._arg2fixturedefs[arg].remove(fd)
request._fixture_defs[arg] = old_fd
if old_fd is not None:
request._fixture_defs[arg] = old_fd
if add_fixturename:
request._pyfuncitem._fixtureinfo.names_closure.remove(arg)
@ -182,7 +234,8 @@ def inject_fixture(request: FixtureRequest, arg: str, value: Any) -> None:
request.addfinalizer(fin)
# inject fixture definition
request._fixturemanager._arg2fixturedefs.setdefault(arg, []).insert(0, fd)
request._fixturemanager._arg2fixturedefs.setdefault(arg, []).append(fd)
# inject fixture value in request cache
request._fixture_defs[arg] = fd
if add_fixturename:

View File

@ -19,7 +19,7 @@ def test_${ make_python_name(scenario.name)}():
% endfor
% for step in steps:
@${step.type}(${ make_string_literal(step.name)})
def ${ make_python_name(step.name)}():
def _():
${make_python_docstring(step.name)}
raise NotImplementedError
% if not loop.last:

View File

@ -5,16 +5,18 @@ import base64
import pickle
import pkgutil
import re
import typing
from inspect import getframeinfo, signature
from sys import _getframe
from typing import TYPE_CHECKING, TypeVar
if typing.TYPE_CHECKING:
if TYPE_CHECKING:
from typing import Any, Callable
from _pytest.config import Config
from _pytest.pytester import RunResult
T = TypeVar("T")
CONFIG_STACK: list[Config] = []
@ -27,16 +29,18 @@ def get_args(func: Callable) -> list[str]:
:rtype: list
"""
params = signature(func).parameters.values()
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 and param.default is param.empty
]
def get_caller_module_locals(depth: int = 2) -> dict[str, Any]:
def get_caller_module_locals(stacklevel: int = 1) -> dict[str, Any]:
"""Get the caller module locals dictionary.
We use sys._getframe instead of inspect.stack(0) because the latter is way slower, since it iterates over
all the frames in the stack.
"""
return _getframe(depth).f_locals
return _getframe(stacklevel + 1).f_locals
def get_caller_module_path(depth: int = 2) -> str:
@ -72,6 +76,15 @@ def collect_dumped_objects(result: RunResult) -> list:
return [pickle.loads(base64.b64decode(payload)) for payload in payloads]
def setdefault(obj: object, name: str, default: T) -> T:
"""Just like dict.setdefault, but for objects."""
try:
return getattr(obj, name)
except AttributeError:
setattr(obj, name, default)
return default
# TODO: Remove this dev junk
def load_tatsu_parser():
cache = pkgutil.get_data("pytest_bdd", "parser_data/gherkin.tatsu").decode("utf-8")

View File

@ -37,17 +37,17 @@ def test_every_step_takes_param_with_the_same_name(testdir):
@given(parsers.cfparse("I have {euro:d} Euro"))
def i_have(euro, values):
def _(euro, values):
assert euro == values.pop(0)
@when(parsers.cfparse("I pay {euro:d} Euro"))
def i_pay(euro, values, request):
def _(euro, values, request):
assert euro == values.pop(0)
@then(parsers.cfparse("I should have {euro:d} Euro"))
def i_should_have(euro, values):
def _(euro, values):
assert euro == values.pop(0)
"""
@ -89,17 +89,17 @@ def test_argument_in_when(testdir):
@given(parsers.cfparse("I have an argument {arg:Number}", extra_types=dict(Number=int)))
def argument(arguments, arg):
def _(arguments, arg):
arguments["arg"] = arg
@when(parsers.cfparse("I get argument {arg:d}"))
def get_argument(arguments, arg):
def _(arguments, arg):
arguments["arg"] = arg
@then(parsers.cfparse("My argument should be {arg:d}"))
def assert_that_my_argument_is_arg(arguments, arg):
def _(arguments, arg):
assert arguments["arg"] == arg
"""

View File

@ -37,17 +37,17 @@ def test_every_steps_takes_param_with_the_same_name(testdir):
@given(parsers.parse("I have {euro:d} Euro"))
def i_have(euro, values):
def _(euro, values):
assert euro == values.pop(0)
@when(parsers.parse("I pay {euro:d} Euro"))
def i_pay(euro, values, request):
def _(euro, values, request):
assert euro == values.pop(0)
@then(parsers.parse("I should have {euro:d} Euro"))
def i_should_have(euro, values):
def _(euro, values):
assert euro == values.pop(0)
"""
@ -88,17 +88,17 @@ def test_argument_in_when_step_1(testdir):
@given(parsers.parse("I have an argument {arg:Number}", extra_types=dict(Number=int)))
def argument(arguments, arg):
def _(arguments, arg):
arguments["arg"] = arg
@when(parsers.parse("I get argument {arg:d}"))
def get_argument(arguments, arg):
def _(arguments, arg):
arguments["arg"] = arg
@then(parsers.parse("My argument should be {arg:d}"))
def assert_that_my_argument_is_arg(arguments, arg):
def _(arguments, arg):
assert arguments["arg"] == arg
"""

View File

@ -14,7 +14,7 @@ def test_every_steps_takes_param_with_the_same_name(testdir):
When I pay 2 Euro
And I pay 1 Euro
Then I should have 0 Euro
And I should have 999999 Euro # In my dream...
And I should have 999999 Euro
"""
),
@ -35,17 +35,17 @@ def test_every_steps_takes_param_with_the_same_name(testdir):
return [1, 2, 1, 0, 999999]
@given(parsers.re(r"I have (?P<euro>\d+) Euro"), converters=dict(euro=int))
def i_have(euro, values):
def _(euro, values):
assert euro == values.pop(0)
@when(parsers.re(r"I pay (?P<euro>\d+) Euro"), converters=dict(euro=int))
def i_pay(euro, values, request):
def _(euro, values, request):
assert euro == values.pop(0)
@then(parsers.re(r"I should have (?P<euro>\d+) Euro"), converters=dict(euro=int))
def i_should_have(euro, values):
def _(euro, values):
assert euro == values.pop(0)
"""
@ -55,6 +55,59 @@ def test_every_steps_takes_param_with_the_same_name(testdir):
result.assert_outcomes(passed=1)
def test_exact_match(testdir):
"""Test that parsers.re does an exact match (fullmatch) of the whole string.
This tests exists because in the past we only used re.match, which only finds a match at the beginning
of the string, so if there were any more characters not matching at the end, they were ignored"""
testdir.makefile(
".feature",
arguments=textwrap.dedent(
"""\
Feature: Step arguments
Scenario: Every step takes a parameter with the same name
Given I have 2 Euro
# Step that should not be found:
When I pay 1 Euro by mistake
Then I should have 1 Euro left
"""
),
)
testdir.makepyfile(
textwrap.dedent(
r"""
import pytest
from pytest_bdd import parsers, given, when, then, scenarios
scenarios("arguments.feature")
@given(parsers.re(r"I have (?P<amount>\d+) Euro"), converters={"amount": int}, target_fixture="wallet")
def _(amount):
return {"EUR": amount}
# Purposefully using a re that will not match the step "When I pay 1 Euro and 50 cents"
@when(parsers.re(r"I pay (?P<amount>\d+) Euro"), converters={"amount": int})
def _(amount, wallet):
wallet["EUR"] -= amount
@then(parsers.re(r"I should have (?P<amount>\d+) Euro left"), converters={"amount": int})
def _(amount, wallet):
assert wallet["EUR"] == amount
"""
)
)
result = testdir.runpytest()
result.assert_outcomes(failed=1)
result.stdout.fnmatch_lines(
'*StepDefinitionNotFoundError: Step definition is not found: When "I pay 1 Euro by mistake"*'
)
def test_argument_in_when(testdir):
testdir.makefile(
".feature",
@ -86,17 +139,17 @@ def test_argument_in_when(testdir):
pass
@given(parsers.re(r"I have an argument (?P<arg>\d+)"))
def argument(arguments, arg):
def _(arguments, arg):
arguments["arg"] = arg
@when(parsers.re(r"I get argument (?P<arg>\d+)"))
def get_argument(arguments, arg):
def _(arguments, arg):
arguments["arg"] = arg
@then(parsers.re(r"My argument should be (?P<arg>\d+)"))
def assert_that_my_argument_is_arg(arguments, arg):
def _(arguments, arg):
assert arguments["arg"] == arg
"""

116
tests/args/test_common.py Normal file
View File

@ -0,0 +1,116 @@
import textwrap
from pytest_bdd.utils import collect_dumped_objects
def test_reuse_same_step_different_converters(testdir):
testdir.makefile(
".feature",
arguments=textwrap.dedent(
"""\
Feature: Reuse same step with different converters
Scenario: Step function should be able to be decorated multiple times with different converters
Given I have a foo with int value 42
And I have a foo with str value 42
And I have a foo with float value 42
When pass
Then pass
"""
),
)
testdir.makepyfile(
textwrap.dedent(
r"""
import pytest
from pytest_bdd import parsers, given, when, then, scenarios
from pytest_bdd.utils import dump_obj
scenarios("arguments.feature")
@given(parsers.re(r"^I have a foo with int value (?P<value>.*?)$"), converters={"value": int})
@given(parsers.re(r"^I have a foo with str value (?P<value>.*?)$"), converters={"value": str})
@given(parsers.re(r"^I have a foo with float value (?P<value>.*?)$"), converters={"value": float})
def _(value):
dump_obj(value)
return value
@then("pass")
@when("pass")
def _():
pass
"""
)
)
result = testdir.runpytest("-s")
result.assert_outcomes(passed=1)
[int_value, str_value, float_value] = collect_dumped_objects(result)
assert type(int_value) is int
assert int_value == 42
assert type(str_value) is str
assert str_value == "42"
assert type(float_value) is float
assert float_value == 42.0
def test_string_steps_dont_take_precedence(testdir):
"""Test that normal steps don't take precedence over the other steps."""
testdir.makefile(
".feature",
arguments=textwrap.dedent(
"""\
Feature: Step precedence
Scenario: String steps don't take precedence over other steps
Given I have a foo with value 42
When pass
Then pass
"""
),
)
testdir.makeconftest(
textwrap.dedent(
"""
from pytest_bdd import given, when, then, parsers
from pytest_bdd.utils import dump_obj
@given("I have a foo with value 42")
def _():
dump_obj("str")
return 42
@then("pass")
@when("pass")
def _():
pass
"""
)
)
testdir.makepyfile(
textwrap.dedent(
r"""
import pytest
from pytest_bdd import parsers, given, when, then, scenarios
from pytest_bdd.utils import dump_obj
scenarios("arguments.feature")
@given(parsers.re(r"^I have a foo with value (?P<value>.*?)$"))
def _(value):
dump_obj("re")
return 42
"""
)
)
result = testdir.runpytest("-s")
result.assert_outcomes(passed=1)
[which] = collect_dumped_objects(result)
assert which == "re"

View File

@ -1,24 +1,17 @@
import pytest
from tests.utils import PYTEST_6
pytest_plugins = "pytester"
def pytest_generate_tests(metafunc):
if "pytest_params" in metafunc.fixturenames:
if PYTEST_6:
parametrizations = [
pytest.param([], id="no-import-mode"),
pytest.param(["--import-mode=prepend"], id="--import-mode=prepend"),
pytest.param(["--import-mode=append"], id="--import-mode=append"),
pytest.param(["--import-mode=importlib"], id="--import-mode=importlib"),
]
else:
parametrizations = [[]]
metafunc.parametrize(
"pytest_params",
parametrizations,
)
if "bdd_parser" in metafunc.fixturenames:
metafunc.parametrize("bdd_parser", ["legacy", "2020"])
parametrizations = [
pytest.param([], id="no-import-mode"),
pytest.param(["--import-mode=prepend"], id="--import-mode=prepend"),
pytest.param(["--import-mode=append"], id="--import-mode=append"),
pytest.param(["--import-mode=importlib"], id="--import-mode=importlib"),
]
metafunc.parametrize("pytest_params", parametrizations)
if "bdd_parser" in metafunc.fixturenames:
metafunc.parametrize("bdd_parser", ["legacy", "2020"])

View File

@ -34,24 +34,24 @@ def test_step_alias(testdir):
@given("I have an empty list", target_fixture="results")
def results():
def _():
return []
@given("I have foo (which is 1) in my list")
@given("I have bar (alias of foo) in my list")
def foo(results):
def _(results):
results.append(1)
@when("I do crash (which is 2)")
@when("I do boom (alias of crash)")
def crash(results):
def _(results):
results.append(2)
@then("my list should be [1, 1, 2, 2]")
def check_results(results):
def _(results):
assert results == [1, 1, 2, 2]
"""
)

View File

@ -34,40 +34,40 @@ def foo():
@given(parsers.re(r"a background step with multiple lines:\n(?P<data>.+)", flags=re.DOTALL))
def multi_line(foo, data):
def _(foo, data):
assert data == "one\ntwo"
@given('foo has a value "bar"')
def bar(foo):
def _(foo):
foo["bar"] = "bar"
return foo["bar"]
@given('foo has a value "dummy"')
def dummy(foo):
def _(foo):
foo["dummy"] = "dummy"
return foo["dummy"]
@given('foo has no value "bar"')
def no_bar(foo):
def _(foo):
assert foo["bar"]
del foo["bar"]
@then('foo should have value "bar"')
def foo_has_bar(foo):
def _(foo):
assert foo["bar"] == "bar"
@then('foo should have value "dummy"')
def foo_has_dummy(foo):
def _(foo):
assert foo["dummy"] == "dummy"
@then('foo should not have value "bar"')
def foo_has_no_bar(foo):
def _(foo):
assert "bar" not in foo
"""

View File

@ -80,19 +80,19 @@ def test_step_trace(testdir):
from pytest_bdd import given, when, scenario, parsers
@given('a passing step')
def a_passing_step():
def _():
return 'pass'
@given('some other passing step')
def some_other_passing_step():
def _():
return 'pass'
@given('a failing step')
def a_failing_step():
def _():
raise Exception('Error')
@given(parsers.parse('type {type} and value {value}'))
def type_type_and_value_value():
def _():
return 'pass'
@scenario('test.feature', 'Passing')

View File

@ -36,7 +36,7 @@ def test_description(testdir):
@given("I have a bar")
def bar():
def _():
return "bar"
def test_scenario_description():

View File

@ -17,16 +17,16 @@ from pytest_bdd import given, when, then, scenario
@given('there is a bar')
def a_bar():
def _():
return 'bar'
@when('the bar is accessed')
def the_bar_is_accessed():
def _():
pass
@then('world explodes')
def world_explodes():
def _():
pass
@ -123,16 +123,16 @@ def test_error_message_should_be_displayed(testdir, verbosity):
@given('there is a bar')
def a_bar():
def _():
return 'bar'
@when('the bar is accessed')
def the_bar_is_accessed():
def _():
pass
@then('world explodes')
def world_explodes():
def _():
raise Exception("BIGBADABOOM")
@ -157,16 +157,16 @@ def test_local_variables_should_be_displayed_when_showlocals_option_is_used(test
@given('there is a bar')
def a_bar():
def _():
return 'bar'
@when('the bar is accessed')
def the_bar_is_accessed():
def _():
pass
@then('world explodes')
def world_explodes():
def _():
local_var = "MULTIPASS"
raise Exception("BIGBADABOOM")
@ -209,15 +209,15 @@ def test_step_parameters_should_be_replaced_by_their_values(testdir):
from pytest_bdd import given, when, scenario, then, parsers
@given(parsers.parse('there are {start} cucumbers'), target_fixture="start_cucumbers")
def start_cucumbers(start):
def _(start):
return start
@when(parsers.parse('I eat {eat} cucumbers'))
def eat_cucumbers(start_cucumbers, eat):
def _(start_cucumbers, eat):
pass
@then(parsers.parse('I should have {left} cucumbers'))
def should_have_left_cucumbers(start_cucumbers, left):
def _(start_cucumbers, left):
pass
@scenario('test.feature', 'Scenario example 2')

View File

@ -88,12 +88,12 @@ def test_multiline(testdir, feature_text, expected_text):
@given(parsers.parse("I have a step with:\\n{{text}}"), target_fixture="text")
def i_have_text(text):
def _(text):
return text
@then("the text should be parsed with correct indentation")
def text_should_be_correct(text):
def _(text):
assert text == expected_text
""".format(
@ -138,12 +138,12 @@ def test_multiline_wrong_indent(testdir):
@given(parsers.parse("I have a step with:\\n{{text}}"), target_fixture="text")
def i_have_text(text):
def _(text):
return text
@then("the text should be parsed with correct indentation")
def text_should_be_correct(text):
def _(text):
assert text == expected_text
"""

View File

@ -22,18 +22,18 @@ def test_background_no_strict_gherkin(testdir):
return {}
@when('foo has a value "bar"')
def bar(foo):
def _(foo):
foo["bar"] = "bar"
return foo["bar"]
@when('foo is not boolean')
def not_boolean(foo):
def _(foo):
assert foo is not bool
@when('foo has not a value "baz"')
def has_not_baz(foo):
def _(foo):
assert "baz" not in foo
"""
)
@ -78,18 +78,18 @@ def test_scenario_no_strict_gherkin(testdir):
return {}
@when('foo has a value "bar"')
def bar(foo):
def _(foo):
foo["bar"] = "bar"
return foo["bar"]
@when('foo is not boolean')
def not_boolean(foo):
def _(foo):
assert foo is not bool
@when('foo has not a value "baz"')
def has_not_baz(foo):
def _(foo):
assert "baz" not in foo
"""
)

View File

@ -2,7 +2,6 @@
import textwrap
from pytest_bdd.utils import collect_dumped_objects
from tests.utils import assert_outcomes
STEPS = """\
from pytest_bdd import parsers, given, when, then
@ -10,21 +9,21 @@ from pytest_bdd.utils import dump_obj
@given(parsers.parse("there are {start:d} cucumbers"), target_fixture="cucumbers")
def given_cucumbers(start):
def _(start):
assert isinstance(start, int)
dump_obj(start)
return {"start": start}
@when(parsers.parse("I eat {eat:g} cucumbers"))
def eat_cucumbers(cucumbers, eat):
def _(cucumbers, eat):
assert isinstance(eat, float)
dump_obj(eat)
cucumbers["eat"] = eat
@then(parsers.parse("I should have {left} cucumbers"))
def should_have_left_cucumbers(cucumbers, left):
def _(cucumbers, left):
assert isinstance(left, str)
dump_obj(left)
assert cucumbers["start"] - cucumbers["eat"] == int(left)
@ -115,7 +114,7 @@ def test_unused_params(testdir):
)
)
result = testdir.runpytest()
assert_outcomes(result, passed=1)
result.assert_outcomes(passed=1)
def test_outlined_with_other_fixtures(testdir):
@ -205,7 +204,7 @@ def test_outline_with_escaped_pipes(testdir):
@given(parsers.parse("I print the {string}"))
def i_print_the_string(string):
def _(string):
dump_obj(string)
"""
)

View File

@ -10,17 +10,17 @@ from pytest_bdd.utils import dump_obj
# Using `parsers.re` so that we can match empty values
@given(parsers.re("there are (?P<start>.*?) cucumbers"))
def start_cucumbers(start):
def _(start):
dump_obj(start)
@when(parsers.re("I eat (?P<eat>.*?) cucumbers"))
def eat_cucumbers(eat):
def _(eat):
dump_obj(eat)
@then(parsers.re("I should have (?P<left>.*?) cucumbers"))
def should_have_left_cucumbers(left):
def _(left):
dump_obj(left)
"""

View File

@ -65,31 +65,31 @@ def test_step_trace(testdir):
from pytest_bdd import given, when, then, scenarios, parsers
@given('a passing step')
def a_passing_step():
def _():
return 'pass'
@given('some other passing step')
def some_other_passing_step():
def _():
return 'pass'
@given('a failing step')
def a_failing_step():
def _():
raise Exception('Error')
@given(parsers.parse('there are {start:d} cucumbers'), target_fixture="cucumbers")
def given_cucumbers(start):
def _(start):
assert isinstance(start, int)
return {"start": start}
@when(parsers.parse('I eat {eat:g} cucumbers'))
def eat_cucumbers(cucumbers, eat):
def _(cucumbers, eat):
assert isinstance(eat, float)
cucumbers['eat'] = eat
@then(parsers.parse('I should have {left} cucumbers'))
def should_have_left_cucumbers(cucumbers, left):
def _(cucumbers, left):
assert isinstance(left, str)
assert cucumbers['start'] - cucumbers['eat'] == int(left)
@ -267,7 +267,7 @@ def test_complex_types(testdir, pytestconfig):
"""
Feature: Report serialization containing parameters of complex types
Scenario: Complex
Scenario Outline: Complex
Given there is a coordinate <point>
Examples:

View File

@ -24,7 +24,7 @@ def test_when_function_name_same_as_step_name(testdir):
pass
@when("something")
def something():
def _():
return "something"
"""
)

View File

@ -2,8 +2,6 @@
import textwrap
from tests.utils import assert_outcomes
def test_scenario_not_found(testdir, pytest_params):
"""Test the situation when scenario is not found."""
@ -32,7 +30,7 @@ def test_scenario_not_found(testdir, pytest_params):
)
result = testdir.runpytest_subprocess(*pytest_params)
assert_outcomes(result, errors=1)
result.assert_outcomes(errors=1)
result.stdout.fnmatch_lines('*Scenario "NOT FOUND" in feature "Scenario is not found" in*')
@ -73,17 +71,17 @@ def test_scenario_comments(testdir):
@given("I have a bar")
def bar():
def _():
return "bar"
@given("comments should be at the start of words")
def comments():
def _():
pass
@then(parsers.parse("this is not {acomment}"))
def a_comment(acomment):
def _(acomment):
assert re.search("a.*comment", acomment)
"""
@ -138,13 +136,59 @@ def test_simple(testdir, pytest_params):
pass
@given("I have a bar")
def bar():
def _():
return "bar"
@then("pass")
def bar():
def _():
pass
"""
)
result = testdir.runpytest_subprocess(*pytest_params)
result.assert_outcomes(passed=1)
def test_angular_brakets_are_not_parsed(testdir):
"""Test that angular brackets are not parsed for "Scenario"s.
(They should be parsed only when used in "Scenario Outline")
"""
testdir.makefile(
".feature",
simple="""
Feature: Simple feature
Scenario: Simple scenario
Given I have a <tag>
Then pass
Scenario Outline: Outlined scenario
Given I have a templated <foo>
Then pass
Examples:
| foo |
| bar |
""",
)
testdir.makepyfile(
"""
from pytest_bdd import scenarios, given, then, parsers
scenarios("simple.feature")
@given("I have a <tag>")
def _():
return "tag"
@given(parsers.parse("I have a templated {foo}"))
def _(foo):
return "foo"
@then("pass")
def _():
pass
"""
)
result = testdir.runpytest()
result.assert_outcomes(passed=2)

View File

@ -1,8 +1,6 @@
"""Test scenarios shortcut."""
import textwrap
from tests.utils import assert_outcomes
def test_scenarios(testdir, pytest_params, bdd_parser):
"""Test scenarios shortcut (used together with @scenario for individual test override)."""
@ -21,7 +19,7 @@ def test_scenarios(testdir, pytest_params, bdd_parser):
from pytest_bdd import given
@given('I have a bar')
def i_have_bar():
def _():
print('bar!')
return 'bar'
"""
@ -71,7 +69,7 @@ def test_scenarios(testdir, pytest_params, bdd_parser):
"""
)
result = testdir.runpytest_subprocess("-v", "-s", *pytest_params)
assert_outcomes(result, passed=4, failed=1)
result.assert_outcomes(passed=4, failed=1)
result.stdout.fnmatch_lines(["*collected 5 items"])
result.stdout.fnmatch_lines(["*test_test_subfolder_scenario *bar!", "PASSED"])
result.stdout.fnmatch_lines(["*test_test_scenario *bar!", "PASSED"])
@ -91,5 +89,5 @@ def test_scenarios_none_found(testdir, pytest_params):
"""
)
result = testdir.runpytest_subprocess(testpath, *pytest_params)
assert_outcomes(result, errors=1)
result.assert_outcomes(errors=1)
result.stdout.fnmatch_lines(["*NoScenariosFound*"])

View File

@ -32,37 +32,37 @@ def test_steps(testdir):
pass
@given('I have a foo fixture with value "foo"', target_fixture="foo")
def foo():
def _():
return "foo"
@given("there is a list", target_fixture="results")
def results():
def _():
return []
@when("I append 1 to the list")
def append_1(results):
def _(results):
results.append(1)
@when("I append 2 to the list")
def append_2(results):
def _(results):
results.append(2)
@when("I append 3 to the list")
def append_3(results):
def _(results):
results.append(3)
@then('foo should have value "foo"')
def foo_is_foo(foo):
def _(foo):
assert foo == "foo"
@then("the list should be [1, 2, 3]")
def check_results(results):
def _(results):
assert results == [1, 2, 3]
"""
@ -72,6 +72,58 @@ def test_steps(testdir):
result.assert_outcomes(passed=1, failed=0)
def test_step_function_can_be_decorated_multiple_times(testdir):
testdir.makefile(
".feature",
steps=textwrap.dedent(
"""\
Feature: Steps decoration
Scenario: Step function can be decorated multiple times
Given there is a foo with value 42
And there is a second foo with value 43
When I do nothing
And I do nothing again
Then I make no mistakes
And I make no mistakes again
"""
),
)
testdir.makepyfile(
textwrap.dedent(
"""\
from pytest_bdd import given, when, then, scenario, parsers
@scenario("steps.feature", "Step function can be decorated multiple times")
def test_steps():
pass
@given(parsers.parse("there is a foo with value {value}"), target_fixture="foo")
@given(parsers.parse("there is a second foo with value {value}"), target_fixture="second_foo")
def _(value):
return value
@when("I do nothing")
@when("I do nothing again")
def _():
pass
@then("I make no mistakes")
@then("I make no mistakes again")
def _():
assert True
"""
)
)
result = testdir.runpytest()
result.assert_outcomes(passed=1, failed=0)
def test_all_steps_can_provide_fixtures(testdir):
"""Test that given/when/then can all provide fixtures."""
testdir.makefile(
@ -100,22 +152,22 @@ def test_all_steps_can_provide_fixtures(testdir):
scenarios("steps.feature")
@given(parsers.parse('Foo is "{value}"'), target_fixture="foo")
def given_foo_is_value(value):
def _(value):
return value
@when(parsers.parse('Foo is "{value}"'), target_fixture="foo")
def when_foo_is_value(value):
def _(value):
return value
@then(parsers.parse('Foo is "{value}"'), target_fixture="foo")
def then_foo_is_value(value):
def _(value):
return value
@then(parsers.parse('foo should be "{value}"'))
def foo_is_foo(foo, value):
def _(foo, value):
assert foo == value
"""
@ -150,12 +202,12 @@ def test_when_first(testdir):
pass
@when("I do nothing")
def do_nothing():
def _():
pass
@then("I make no mistakes")
def no_errors():
def _():
assert True
"""
@ -191,11 +243,11 @@ def test_then_after_given(testdir):
pass
@given('I have a foo fixture with value "foo"', target_fixture="foo")
def foo():
def _():
return "foo"
@then('foo should have value "foo"')
def foo_is_foo(foo):
def _(foo):
assert foo == "foo"
"""
@ -228,12 +280,12 @@ def test_conftest(testdir):
@given("I have a bar", target_fixture="bar")
def bar():
def _():
return "bar"
@then('bar should have value "bar"')
def bar_is_bar(bar):
def _(bar):
assert bar == "bar"
"""
@ -277,12 +329,12 @@ def test_multiple_given(testdir):
@given(parsers.parse("foo is {value}"), target_fixture="foo")
def foo(value):
def _(value):
return value
@then(parsers.parse("foo should be {value}"))
def foo_should_be(foo, value):
def _(foo, value):
assert foo == value
@ -325,15 +377,15 @@ def test_step_hooks(testdir):
from pytest_bdd import given, when, scenario
@given('I have a bar')
def i_have_bar():
def _():
return 'bar'
@when('it fails')
def when_it_fails():
def _():
raise Exception('when fails')
@given('I have a bar')
def i_have_bar():
def _():
return 'bar'
@pytest.fixture
@ -341,7 +393,7 @@ def test_step_hooks(testdir):
raise Exception('dependency fails')
@when("it's dependency fails")
def when_dependency_fails(dependency):
def _(dependency):
pass
@scenario('test.feature', "When step's dependency a has failure")
@ -357,7 +409,7 @@ def test_step_hooks(testdir):
pass
@when('foo')
def foo():
def _():
return 'foo'
@scenario('test.feature', 'When step validation error happens')
@ -439,11 +491,11 @@ def test_step_trace(testdir):
from pytest_bdd import given, when, scenario
@given('I have a bar')
def i_have_bar():
def _():
return 'bar'
@when('it fails')
def when_it_fails():
def _():
raise Exception('when fails')
@scenario('test.feature', 'When step has failure')
@ -459,7 +511,7 @@ def test_step_trace(testdir):
pass
@when('foo')
def foo():
def _():
return 'foo'
@scenario('test.feature', 'When step validation error happens')
@ -511,14 +563,14 @@ Feature: A feature
scenarios("a.feature")
@when("I setup stuff", target_fixture="stuff")
def stuff():
def _():
print("Setting up...")
yield 42
print("Tearing down...")
@then("stuff should be 42")
def check_stuff(stuff):
def _(stuff):
assert stuff == 42
print("Asserted stuff is 42")

View File

@ -46,7 +46,7 @@ def test_tags_selector(testdir):
from pytest_bdd import given, scenarios
@given('I have a bar')
def i_have_bar():
def _():
return 'bar'
scenarios('test.feature')
@ -101,11 +101,11 @@ def test_tags_after_background_issue_160(testdir):
from pytest_bdd import given, scenarios
@given('I have a bar')
def i_have_bar():
def _():
return 'bar'
@given('I have a baz')
def i_have_baz():
def _():
return 'baz'
scenarios('test.feature')
@ -151,7 +151,7 @@ def test_apply_tag_hook(testdir):
from pytest_bdd import given, scenarios
@given('I have a bar')
def i_have_bar():
def _():
return 'bar'
scenarios('test.feature')
@ -180,11 +180,11 @@ def test_at_in_scenario(testdir):
from pytest_bdd import given, scenarios
@given('I have a foo@bar')
def i_have_at():
def _():
return 'foo@bar'
@given('I have a baz')
def i_have_baz():
def _():
return 'baz'
scenarios('test.feature')

View File

@ -2,8 +2,6 @@
import textwrap
from tests.utils import assert_outcomes
def test_multiple_features_single_file(testdir):
"""Test validation error when multiple features are placed in a single file."""
@ -51,5 +49,5 @@ def test_multiple_features_single_file(testdir):
)
)
result = testdir.runpytest()
assert_outcomes(result, errors=1)
result.assert_outcomes(errors=1)
result.stdout.fnmatch_lines("*FeatureError: Multiple features are not allowed in a single feature file.*")

View File

@ -3,7 +3,6 @@ import itertools
import textwrap
from pytest_bdd.scenario import get_python_name_generator
from tests.utils import assert_outcomes
def test_python_name_generator():
@ -50,7 +49,7 @@ def test_generate_missing(testdir):
scenario = functools.partial(scenario, "generation.feature")
@given("I have a bar")
def i_have_a_bar():
def _():
return "bar"
@scenario("Scenario tests which are already bound to the tests stay as is")
@ -65,7 +64,7 @@ def test_generate_missing(testdir):
)
result = testdir.runpytest("--generate-missing", "--feature", "generation.feature")
assert_outcomes(result, passed=0, failed=0, errors=0)
result.assert_outcomes(passed=0, failed=0, errors=0)
assert not result.stderr.str()
assert result.ret == 0
@ -114,26 +113,26 @@ def test_generate_missing_with_step_parsers(testdir):
scenarios("generation.feature")
@given("I use the string parser without parameter")
def i_have_a_bar():
def _():
return None
@given(parsers.parse("I use parsers.parse with parameter {param}"))
def i_have_n_baz(param):
def _(param):
return param
@given(parsers.re(r"^I use parsers.re with parameter (?P<param>.*?)$"))
def i_have_n_baz(param):
def _(param):
return param
@given(parsers.cfparse("I use parsers.cfparse with parameter {param:d}"))
def i_have_n_baz(param):
def _(param):
return param
"""
)
)
result = testdir.runpytest("--generate-missing", "--feature", "generation.feature")
assert_outcomes(result, passed=0, failed=0, errors=0)
result.assert_outcomes(passed=0, failed=0, errors=0)
assert not result.stderr.str()
assert result.ret == 0

View File

@ -4,6 +4,8 @@ Check the parent givens are collected and overridden in the local conftest.
"""
import textwrap
from pytest_bdd.utils import collect_dumped_objects
def test_parent(testdir):
"""Test parent given is collected.
@ -29,12 +31,12 @@ def test_parent(testdir):
@given("I have a parent fixture", target_fixture="parent")
def parent():
def _():
return "parent"
@given("I have an overridable fixture", target_fixture="overridable")
def overridable():
def _():
return "parent"
"""
@ -58,42 +60,49 @@ def test_parent(testdir):
result.assert_outcomes(passed=1)
def test_global_when_step(testdir, request):
def test_global_when_step(testdir):
"""Test when step defined in the parent conftest."""
testdir.makefile(
".feature",
global_when=textwrap.dedent(
"""\
Feature: Global when
Scenario: Global when step defined in parent conftest
When I use a when step from the parent conftest
"""
),
)
testdir.makeconftest(
textwrap.dedent(
"""\
from pytest_bdd import when
from pytest_bdd.utils import dump_obj
@when("I use a when step from the parent conftest")
def global_when():
pass
def _():
dump_obj("global when step")
"""
)
)
subdir = testdir.mkpydir("subdir")
subdir.join("test_library.py").write(
testdir.mkpydir("subdir").join("test_global_when.py").write(
textwrap.dedent(
"""\
from pytest_bdd.steps import get_step_fixture_name, WHEN
from pytest_bdd import scenarios
def test_global_when_step(request):
assert request.getfixturevalue(
get_step_fixture_name("I use a when step from the parent conftest",
WHEN,
)
)
"""
scenarios("../global_when.feature")
"""
)
)
result = testdir.runpytest()
result = testdir.runpytest("-s")
result.assert_outcomes(passed=1)
[collected_object] = collect_dumped_objects(result)
assert collected_object == "global when step"
def test_child(testdir):
"""Test the child conftest overriding the fixture."""
@ -104,12 +113,12 @@ def test_child(testdir):
@given("I have a parent fixture", target_fixture="parent")
def parent():
def _():
return "parent"
@given("I have an overridable fixture", target_fixture="overridable")
def overridable():
def main_conftest():
return "parent"
"""
@ -124,7 +133,7 @@ def test_child(testdir):
from pytest_bdd import given
@given("I have an overridable fixture", target_fixture="overridable")
def overridable():
def subdir_conftest():
return "child"
"""
@ -169,12 +178,12 @@ def test_local(testdir):
@given("I have a parent fixture", target_fixture="parent")
def parent():
def _():
return "parent"
@given("I have an overridable fixture", target_fixture="overridable")
def overridable():
def _():
return "parent"
"""
@ -198,16 +207,15 @@ def test_local(testdir):
textwrap.dedent(
"""\
from pytest_bdd import given, scenario
from pytest_bdd.steps import get_step_fixture_name, GIVEN
@given("I have an overridable fixture", target_fixture="overridable")
def overridable():
def _():
return "local"
@given("I have a parent fixture", target_fixture="parent")
def parent():
def _():
return "local"
@ -215,21 +223,184 @@ def test_local(testdir):
def test_local(request):
assert request.getfixturevalue("parent") == "local"
assert request.getfixturevalue("overridable") == "local"
fixture = request.getfixturevalue(
get_step_fixture_name("I have a parent fixture", GIVEN)
)
assert fixture() == "local"
fixture = request.getfixturevalue(
get_step_fixture_name("I have an overridable fixture", GIVEN)
)
assert fixture() == "local"
"""
)
)
result = testdir.runpytest()
result.assert_outcomes(passed=1)
def test_uses_correct_step_in_the_hierarchy(testdir):
"""
Test regression found in issue #524, where we couldn't find the correct step implemntation 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).
"""
testdir.makefile(
".feature",
specific=textwrap.dedent(
"""\
Feature: Specificity of steps
Scenario: Overlapping steps
Given I have a specific thing
Then pass
"""
),
)
testdir.makeconftest(
textwrap.dedent(
"""\
from pytest_bdd import parsers, given, then
from pytest_bdd.utils import dump_obj
import pytest
@given(parsers.re("(?P<thing>.*)"))
def root_conftest_catchall(thing):
dump_obj(thing + " (catchall) root_conftest")
@given(parsers.parse("I have a {thing} thing"))
def root_conftest(thing):
dump_obj(thing + " root_conftest")
@given("I have a specific thing")
def root_conftest_specific():
dump_obj("specific" + "(specific) root_conftest")
@then("pass")
def _():
pass
"""
)
)
# Adding deceiving @when steps around the real test, so that we can check if the right one is used
# the right one is the one in test_b/test_b.py
# We purposefully use test_a and test_c as decoys (while test_b/test_b is "good one"), so that we can test that
# we pick the right one.
testdir.makepyfile(
test_a="""\
from pytest_bdd import given, parsers
from pytest_bdd.utils import dump_obj
@given(parsers.re("(?P<thing>.*)"))
def in_root_test_a_catch_all(thing):
dump_obj(thing + " (catchall) test_a")
@given(parsers.parse("I have a specific thing"))
def in_root_test_a_specific():
dump_obj("specific" + " (specific) test_a")
@given(parsers.parse("I have a {thing} thing"))
def in_root_test_a(thing):
dump_obj(thing + " root_test_a")
"""
)
testdir.makepyfile(
test_c="""\
from pytest_bdd import given, parsers
from pytest_bdd.utils import dump_obj
@given(parsers.re("(?P<thing>.*)"))
def in_root_test_c_catch_all(thing):
dump_obj(thing + " (catchall) test_c")
@given(parsers.parse("I have a specific thing"))
def in_root_test_c_specific():
dump_obj("specific" + " (specific) test_c")
@given(parsers.parse("I have a {thing} thing"))
def in_root_test_c(thing):
dump_obj(thing + " root_test_b")
"""
)
test_b_folder = testdir.mkpydir("test_b")
# More decoys: test_b/test_a.py and test_b/test_c.py
test_b_folder.join("test_a.py").write(
textwrap.dedent(
"""\
from pytest_bdd import given, parsers
from pytest_bdd.utils import dump_obj
@given(parsers.re("(?P<thing>.*)"))
def in_root_test_b_test_a_catch_all(thing):
dump_obj(thing + " (catchall) test_b_test_a")
@given(parsers.parse("I have a specific thing"))
def in_test_b_test_a_specific():
dump_obj("specific" + " (specific) test_b_test_a")
@given(parsers.parse("I have a {thing} thing"))
def in_test_b_test_a(thing):
dump_obj(thing + " test_b_test_a")
"""
)
)
test_b_folder.join("test_c.py").write(
textwrap.dedent(
"""\
from pytest_bdd import given, parsers
from pytest_bdd.utils import dump_obj
@given(parsers.re("(?P<thing>.*)"))
def in_root_test_b_test_c_catch_all(thing):
dump_obj(thing + " (catchall) test_b_test_c")
@given(parsers.parse("I have a specific thing"))
def in_test_b_test_c_specific():
dump_obj("specific" + " (specific) test_a_test_c")
@given(parsers.parse("I have a {thing} thing"))
def in_test_b_test_c(thing):
dump_obj(thing + " test_c_test_a")
"""
)
)
# Finally, the file with the actual step definition that should be used
test_b_folder.join("test_b.py").write(
textwrap.dedent(
"""\
from pytest_bdd import scenarios, given, parsers
from pytest_bdd.utils import dump_obj
scenarios("../specific.feature")
@given(parsers.parse("I have a {thing} thing"))
def in_test_b_test_b(thing):
dump_obj(f"{thing} test_b_test_b")
"""
)
)
test_b_folder.join("test_b_alternative.py").write(
textwrap.dedent(
"""\
from pytest_bdd import scenarios, given, parsers
from pytest_bdd.utils import dump_obj
scenarios("../specific.feature")
# Here we try to use an argument different from the others,
# to make sure it doesn't matter if a new step parser string is encountered.
@given(parsers.parse("I have a {t} thing"))
def in_test_b_test_b(t):
dump_obj(f"{t} test_b_test_b")
"""
)
)
result = testdir.runpytest("-s")
result.assert_outcomes(passed=2)
[thing1, thing2] = collect_dumped_objects(result)
assert thing1 == thing2 == "specific test_b_test_b"

View File

@ -53,25 +53,25 @@ def test_generate(testdir, monkeypatch, capsys):
@given('1 have a fixture (appends 1 to a list) in reuse syntax')
def have_a_fixture_appends_1_to_a_list_in_reuse_syntax():
def _():
"""1 have a fixture (appends 1 to a list) in reuse syntax."""
raise NotImplementedError
@given('I have an empty list')
def i_have_an_empty_list():
def _():
"""I have an empty list."""
raise NotImplementedError
@when('I use this fixture')
def i_use_this_fixture():
def _():
"""I use this fixture."""
raise NotImplementedError
@then('my list should be [1]')
def my_list_should_be_1():
def _():
"""my list should be [1]."""
raise NotImplementedError
@ -119,37 +119,37 @@ def test_generate_with_quotes(testdir):
@given('I have a fixture with "double" quotes')
def i_have_a_fixture_with_double_quotes():
def _():
"""I have a fixture with "double" quotes."""
raise NotImplementedError
@given('I have a fixture with \\'single\\' quotes')
def i_have_a_fixture_with_single_quotes():
def _():
"""I have a fixture with 'single' quotes."""
raise NotImplementedError
@given('I have a fixture with double-quote """triple""" quotes')
def i_have_a_fixture_with_doublequote_triple_quotes():
def _():
"""I have a fixture with double-quote \\"\\"\\"triple\\"\\"\\" quotes."""
raise NotImplementedError
@given('I have a fixture with single-quote \\'\\'\\'triple\\'\\'\\' quotes')
def i_have_a_fixture_with_singlequote_triple_quotes():
def _():
"""I have a fixture with single-quote \'\'\'triple\'\'\' quotes."""
raise NotImplementedError
@when('I generate the code')
def i_generate_the_code():
def _():
"""I generate the code."""
raise NotImplementedError
@then('The generated string should be written')
def the_generated_string_should_be_written():
def _():
"""The generated string should be written."""
raise NotImplementedError
'''
@ -195,19 +195,19 @@ def test_unicode_characters(testdir, monkeypatch):
@given('We have a circle')
def we_have_a_circle():
def _():
"""We have a circle."""
raise NotImplementedError
@when('We want to know its circumference')
def we_want_to_know_its_circumference():
def _():
"""We want to know its circumference."""
raise NotImplementedError
@then('We calculate 2 * ℼ * 𝑟')
def we_calculate_2__ℼ__𝑟():
def _():
"""We calculate 2 * ℼ * 𝑟."""
raise NotImplementedError
'''

318
tests/steps/test_common.py Normal file
View File

@ -0,0 +1,318 @@
import textwrap
from typing import Any, Callable
from unittest import mock
import pytest
from pytest_bdd import given, parsers, then, when
from pytest_bdd.utils import collect_dumped_objects
@pytest.mark.parametrize("step_fn, step_type", [(given, "given"), (when, "when"), (then, "then")])
def test_given_when_then_delegate_to_step(step_fn: Callable[..., Any], step_type: str) -> None:
"""Test that @given, @when, @then just delegate the work to @step(...).
This way we don't have to repeat integration tests for each step decorator.
"""
# Simple usage, just the step name
with mock.patch("pytest_bdd.steps.step", autospec=True) as step_mock:
step_fn("foo")
step_mock.assert_called_once_with("foo", type_=step_type, converters=None, target_fixture=None, stacklevel=1)
# Advanced usage: step parser, converters, target_fixture, ...
with mock.patch("pytest_bdd.steps.step", autospec=True) as step_mock:
parser = parsers.re(r"foo (?P<n>\d+)")
step_fn(parser, converters={"n": int}, target_fixture="foo_n", stacklevel=3)
step_mock.assert_called_once_with(
name=parser, type_=step_type, converters={"n": int}, target_fixture="foo_n", stacklevel=3
)
def test_step_function_multiple_target_fixtures(testdir):
testdir.makefile(
".feature",
target_fixture=textwrap.dedent(
"""\
Feature: Multiple target fixtures for step function
Scenario: A step can be decorated multiple times with different target fixtures
Given there is a foo with value "test foo"
And there is a bar with value "test bar"
Then foo should be "test foo"
And bar should be "test bar"
"""
),
)
testdir.makepyfile(
textwrap.dedent(
"""\
import pytest
from pytest_bdd import given, when, then, scenarios, parsers
from pytest_bdd.utils import dump_obj
scenarios("target_fixture.feature")
@given(parsers.parse('there is a foo with value "{value}"'), target_fixture="foo")
@given(parsers.parse('there is a bar with value "{value}"'), target_fixture="bar")
def _(value):
return value
@then(parsers.parse('foo should be "{expected_value}"'))
def _(foo, expected_value):
dump_obj(foo)
assert foo == expected_value
@then(parsers.parse('bar should be "{expected_value}"'))
def _(bar, expected_value):
dump_obj(bar)
assert bar == expected_value
"""
)
)
result = testdir.runpytest("-s")
result.assert_outcomes(passed=1)
[foo, bar] = collect_dumped_objects(result)
assert foo == "test foo"
assert bar == "test bar"
def test_step_functions_same_parser(testdir):
testdir.makefile(
".feature",
target_fixture=textwrap.dedent(
"""\
Feature: A feature
Scenario: A scenario
Given there is a foo with value "(?P<value>\\w+)"
And there is a foo with value "testfoo"
When pass
Then pass
"""
),
)
testdir.makepyfile(
textwrap.dedent(
"""\
import pytest
from pytest_bdd import given, when, then, scenarios, parsers
from pytest_bdd.utils import dump_obj
scenarios("target_fixture.feature")
STEP = 'there is a foo with value "(?P<value>\\w+)"'
@given(STEP)
def _():
dump_obj(('str',))
@given(parsers.re(STEP))
def _(value):
dump_obj(('re', value))
@when("pass")
@then("pass")
def _():
pass
"""
)
)
result = testdir.runpytest("-s")
result.assert_outcomes(passed=1)
[first_given, second_given] = collect_dumped_objects(result)
assert first_given == ("str",)
assert second_given == ("re", "testfoo")
def test_user_implements_a_step_generator(testdir):
"""Test advanced use cases, like the implementation of custom step generators."""
testdir.makefile(
".feature",
user_step_generator=textwrap.dedent(
"""\
Feature: A feature
Scenario: A scenario
Given I have 10 EUR
And the wallet is verified
And I have a wallet
When I pay 1 EUR
Then I should have 9 EUR in my wallet
"""
),
)
testdir.makepyfile(
textwrap.dedent(
"""\
import re
from dataclasses import dataclass, fields
import pytest
from pytest_bdd import given, when, then, scenarios, parsers
from pytest_bdd.utils import dump_obj
@dataclass
class Wallet:
verified: bool
amount_eur: int
amount_usd: int
amount_gbp: int
amount_jpy: int
def pay(self, amount: int, currency: str) -> None:
if not self.verified:
raise ValueError("Wallet account is not verified")
currency = currency.lower()
field = f"amount_{currency}"
setattr(self, field, getattr(self, field) - amount)
@pytest.fixture
def wallet__verified():
return False
@pytest.fixture
def wallet__amount_eur():
return 0
@pytest.fixture
def wallet__amount_usd():
return 0
@pytest.fixture
def wallet__amount_gbp():
return 0
@pytest.fixture
def wallet__amount_jpy():
return 0
@pytest.fixture()
def wallet(
wallet__verified,
wallet__amount_eur,
wallet__amount_usd,
wallet__amount_gbp,
wallet__amount_jpy,
):
return Wallet(
verified=wallet__verified,
amount_eur=wallet__amount_eur,
amount_usd=wallet__amount_usd,
amount_gbp=wallet__amount_gbp,
amount_jpy=wallet__amount_jpy,
)
def generate_wallet_steps(model_name="wallet", stacklevel=1):
stacklevel += 1
@given("I have a wallet", target_fixture=model_name, stacklevel=stacklevel)
def _(wallet):
return wallet
@given(
parsers.re(r"the wallet is (?P<negation>not)?verified"),
target_fixture=f"{model_name}__verified",
stacklevel=2,
)
def _(negation: str):
if negation:
return False
return True
# Generate steps for currency fields:
for field in fields(Wallet):
match = re.fullmatch(r"amount_(?P<currency>[a-z]{3})", field.name)
if not match:
continue
currency = match["currency"]
@given(
parsers.parse(f"I have {{value:d}} {currency.upper()}"),
target_fixture=f"{model_name}__amount_{currency}",
stacklevel=2,
)
def _(value: int, _currency=currency) -> int:
dump_obj(f"given {value} {_currency.upper()}")
return value
@when(
parsers.parse(f"I pay {{value:d}} {currency.upper()}"),
stacklevel=2,
)
def _(wallet: Wallet, value: int, _currency=currency) -> None:
dump_obj(f"pay {value} {_currency.upper()}")
wallet.pay(value, _currency)
@then(
parsers.parse(f"I should have {{value:d}} {currency.upper()} in my wallet"),
stacklevel=2,
)
def _(wallet: Wallet, value: int, _currency=currency) -> None:
dump_obj(f"assert {value} {_currency.upper()}")
assert getattr(wallet, f"amount_{_currency}") == value
generate_wallet_steps()
scenarios("user_step_generator.feature")
"""
)
)
result = testdir.runpytest("-s")
result.assert_outcomes(passed=1)
[given, pay, assert_] = collect_dumped_objects(result)
assert given == "given 10 EUR"
assert pay == "pay 1 EUR"
assert assert_ == "assert 9 EUR"
def test_step_catches_all(testdir):
"""Test that the @step(...) decorator works for all kind of steps."""
testdir.makefile(
".feature",
step_catches_all=textwrap.dedent(
"""\
Feature: A feature
Scenario: A scenario
Given foo
And foo parametrized 1
When foo
And foo parametrized 2
Then foo
And foo parametrized 3
"""
),
)
testdir.makepyfile(
textwrap.dedent(
"""\
import pytest
from pytest_bdd import step, scenarios, parsers
from pytest_bdd.utils import dump_obj
scenarios("step_catches_all.feature")
@step("foo")
def _():
dump_obj("foo")
@step(parsers.parse("foo parametrized {n:d}"))
def _(n):
dump_obj(("foo parametrized", n))
"""
)
)
result = testdir.runpytest("-s")
result.assert_outcomes(passed=1)
objects = collect_dumped_objects(result)
assert objects == ["foo", ("foo parametrized", 1), "foo", ("foo parametrized", 2), "foo", ("foo parametrized", 3)]

View File

@ -25,12 +25,12 @@ def test_given_injection(testdir):
pass
@given("I have injecting given", target_fixture="foo")
def injecting_given():
def _():
return "injected foo"
@then('foo should be "injected foo"')
def foo_is_injected_foo(foo):
def _(foo):
assert foo == "injected foo"
"""

View File

@ -1,71 +0,0 @@
"""Test when and then steps are callables."""
import textwrap
import pytest
def test_when_then(testdir):
"""Test when and then steps are callable functions.
This test checks that when and then are not evaluated
during fixture collection that might break the scenario.
"""
testdir.makepyfile(
textwrap.dedent(
"""\
import pytest
from pytest_bdd import given, when, then
from pytest_bdd.steps import get_step_fixture_name, WHEN, THEN
@when("I do stuff")
def do_stuff():
pass
@then("I check stuff")
def check_stuff():
pass
def test_when_then(request):
do_stuff_ = request.getfixturevalue(get_step_fixture_name("I do stuff", WHEN))
assert callable(do_stuff_)
check_stuff_ = request.getfixturevalue(get_step_fixture_name("I check stuff", THEN))
assert callable(check_stuff_)
"""
)
)
result = testdir.runpytest()
result.assert_outcomes(passed=1)
@pytest.mark.parametrize(
("step", "keyword"),
[("given", "Given"), ("when", "When"), ("then", "Then")],
)
def test_preserve_decorator(testdir, step, keyword):
"""Check that we preserve original function attributes after decorating it."""
testdir.makepyfile(
textwrap.dedent(
'''\
from pytest_bdd import {step}
from pytest_bdd.steps import get_step_fixture_name
@{step}("{keyword}")
def func():
"""Doc string."""
def test_decorator():
assert globals()[get_step_fixture_name("{keyword}", {step}.__name__)].__doc__ == "Doc string."
'''.format(
step=step, keyword=keyword
)
)
)
result = testdir.runpytest()
result.assert_outcomes(passed=1)

View File

@ -38,7 +38,7 @@ def test_steps_in_feature_file_have_unicode(testdir):
@given(parsers.parse(u"у мене є рядок який містить '{content}'"))
def there_is_a_string_with_content(content, string):
def _(content, string):
string["content"] = content
@ -46,7 +46,7 @@ def test_steps_in_feature_file_have_unicode(testdir):
@then(parsers.parse("I should see that the string equals to content '{content}'"))
def assert_that_the_string_equals_to_content(content, string):
def _(content, string):
assert string["content"] == content
"""
)
@ -85,11 +85,11 @@ def test_steps_in_py_file_have_unicode(testdir):
@given("there is an other string with content 'якийсь контент'")
def there_is_an_other_string_with_content(string):
def _(string):
string["content"] = u"с каким-то контентом"
@then("I should see that the other string equals to content 'якийсь контент'")
def assert_that_the_other_string_equals_to_content(string):
def _(string):
assert string["content"] == u"с каким-то контентом"
"""

View File

@ -1,50 +1,7 @@
from __future__ import annotations
import typing
import pytest
from packaging.utils import Version
if typing.TYPE_CHECKING:
from _pytest.pytester import RunResult
# We leave this here for the future as an easy way to do feature-based testing.
PYTEST_VERSION = Version(pytest.__version__)
PYTEST_6 = PYTEST_VERSION >= Version("6")
if PYTEST_6:
def assert_outcomes(
result: RunResult,
passed: int = 0,
skipped: int = 0,
failed: int = 0,
errors: int = 0,
xpassed: int = 0,
xfailed: int = 0,
) -> None:
"""Compatibility function for result.assert_outcomes"""
result.assert_outcomes(
errors=errors, passed=passed, skipped=skipped, failed=failed, xpassed=xpassed, xfailed=xfailed
)
else:
def assert_outcomes(
result: RunResult,
passed: int = 0,
skipped: int = 0,
failed: int = 0,
errors: int = 0,
xpassed: int = 0,
xfailed: int = 0,
) -> None:
"""Compatibility function for result.assert_outcomes"""
result.assert_outcomes(
error=errors, # Pytest < 6 uses the singular form
passed=passed,
skipped=skipped,
failed=failed,
xpassed=xpassed,
xfailed=xfailed,
)

21
tox.ini
View File

@ -1,12 +1,8 @@
[tox]
isolated_build = true
# needed for PEP 517-style builds
isolated_build = True
distshare = {homedir}/.tox/distshare
envlist = py310-pytestlatest-linters,
; python 3.10 is only supported by pytest >= 6.2.5:
py310-pytest{62,70,latest}-coverage,
; the rest of pytest runs need to use an older python:
py39-pytest{50,51,52,53,54,60,61}-coverage,
py{37,38,39}-pytestlatest-coverage,
envlist = py{37,38,39,310,311}-pytest{62,70,71,latest}-coverage,
py310-pytestlatest-xdist-coverage
skip_missing_interpreters = true
@ -16,6 +12,7 @@ setenv =
xdist: _PYTEST_MORE_ARGS=-n3 -rfsxX
deps =
pytestlatest: pytest
pytest71: pytest~=7.1.0
pytest70: pytest~=7.0.0
pytest62: pytest~=6.2.0
pytest61: pytest~=6.1.0
@ -26,18 +23,13 @@ deps =
pytest51: pytest~=5.1.0
pytest50: pytest~=5.0.0
coverage: coverage
coverage: coverage[toml]
xdist: pytest-xdist
-r{toxinidir}/requirements-testing.txt
commands = {env:_PYTEST_CMD:pytest} {env:_PYTEST_MORE_ARGS:} {posargs:-vvl}
[testenv:py310-pytestlatest-linters]
deps = black==22.1.0
commands = black --check --verbose setup.py docs pytest_bdd tests
[testenv:mypy]
deps =
mypy==0.931
mypy==0.961
types-setuptools
commands = mypy
@ -47,3 +39,4 @@ python =
3.8: py38
3.9: py39
3.10: py310
3.11: py311