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:
commit
b26487aea8
|
@ -1,5 +0,0 @@
|
|||
[run]
|
||||
branch = true
|
||||
include =
|
||||
pytest_bdd/*
|
||||
tests/*
|
|
@ -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
|
||||
|
|
|
@ -48,10 +48,3 @@ nosetests.xml
|
|||
|
||||
#PyCharm
|
||||
/.idea
|
||||
|
||||
# virtualenv
|
||||
/.Python
|
||||
/lib
|
||||
/include
|
||||
/share
|
||||
/local
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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: .
|
34
CHANGES.rst
34
CHANGES.rst
|
@ -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>`_.
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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/*
|
||||
```
|
|
@ -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
|
33
Makefile
33
Makefile
|
@ -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
|
||||
|
|
301
README.rst
301
README.rst
|
@ -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
|
||||
|
|
15
RELEASING.md
15
RELEASING.md
|
@ -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/*
|
||||
```
|
|
@ -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.
|
||||
|
|
|
@ -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"},
|
||||
]
|
|
@ -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"]
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
packaging
|
||||
more-itertools
|
55
setup.cfg
55
setup.cfg
|
@ -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
|
|
@ -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"]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
||||
"""
|
||||
|
|
|
@ -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"
|
|
@ -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"])
|
||||
|
|
|
@ -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]
|
||||
"""
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
||||
"""
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -36,7 +36,7 @@ def test_description(testdir):
|
|||
|
||||
|
||||
@given("I have a bar")
|
||||
def bar():
|
||||
def _():
|
||||
return "bar"
|
||||
|
||||
def test_scenario_description():
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
"""
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
||||
"""
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -24,7 +24,7 @@ def test_when_function_name_same_as_step_name(testdir):
|
|||
pass
|
||||
|
||||
@when("something")
|
||||
def something():
|
||||
def _():
|
||||
return "something"
|
||||
"""
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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*"])
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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.*")
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
'''
|
||||
|
|
|
@ -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)]
|
|
@ -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"
|
||||
|
||||
"""
|
||||
|
|
|
@ -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)
|
|
@ -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"с каким-то контентом"
|
||||
|
||||
"""
|
||||
|
|
|
@ -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
21
tox.ini
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue