Merge pull request #406 from elchupanebrej/actual_python_pytest

- Drop support of python 2.7, 3.5; Add explicit support for python >=3.6
- Remove six dependency

Co-authored-by: Konstantin Goloveshko <konstantin.goloveshko@collabio.team>
This commit is contained in:
Alessio Bogon 2021-03-02 19:24:50 +01:00 committed by GitHub
commit 5d58fe3bbb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 141 additions and 269 deletions

View File

@ -1,11 +1,10 @@
dist: bionic
language: python
python:
- '2.7'
- '3.5'
- '3.6'
- '3.7'
- '3.8'
- '3.9'
install: pip install tox tox-travis coverage codecov
script: tox --recreate
branches:

View File

@ -1,6 +1,10 @@
Changelog
=========
Unreleased
-----------
- Drop compatibility for python 2 and officially support only python >= 3.6.
4.0.2
-----
- Fix a bug that prevents using comments in the ``Examples:`` section. (youtux)

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# Pytest-BDD documentation build configuration file, created by
# sphinx-quickstart on Sun Apr 7 21:07:56 2013.
@ -15,7 +14,8 @@
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
import sys, os
import os
import sys
sys.path.insert(0, os.path.abspath(".."))
@ -43,8 +43,8 @@ source_suffix = ".rst"
master_doc = "index"
# General information about the project.
project = u"Pytest-BDD"
copyright = u"2013, Oleg Pidsadnyi"
project = "Pytest-BDD"
copyright = "2013, Oleg Pidsadnyi"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
@ -183,7 +183,7 @@ latex_elements = {
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [("index", "Pytest-BDD.tex", u"Pytest-BDD Documentation", u"Oleg Pidsadnyi", "manual")]
latex_documents = [("index", "Pytest-BDD.tex", "Pytest-BDD Documentation", "Oleg Pidsadnyi", "manual")]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
@ -210,7 +210,7 @@ latex_documents = [("index", "Pytest-BDD.tex", u"Pytest-BDD Documentation", u"Ol
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [("index", "pytest-bdd", u"Pytest-BDD Documentation", [u"Oleg Pidsadnyi"], 1)]
man_pages = [("index", "pytest-bdd", "Pytest-BDD Documentation", ["Oleg Pidsadnyi"], 1)]
# If true, show URL addresses after external links.
# man_show_urls = False
@ -225,8 +225,8 @@ texinfo_documents = [
(
"index",
"Pytest-BDD",
u"Pytest-BDD Documentation",
u"Oleg Pidsadnyi",
"Pytest-BDD Documentation",
"Oleg Pidsadnyi",
"Pytest-BDD",
"One line description of project.",
"Miscellaneous",

View File

@ -1,3 +1,3 @@
[tool.black]
line-length = 120
target-version = ['py27', 'py35', 'py36', 'py37', 'py38']
target-version = ['py36', 'py37', 'py38']

View File

@ -1,14 +1,10 @@
"""Cucumber json output formatter."""
import codecs
import json
import math
import os
import sys
import time
from .feature import force_unicode
def add_options(parser):
"""Add pytest-bdd options."""
@ -72,7 +68,7 @@ class LogBDDCucumberJSON(object):
if report.passed or not step["failed"]: # ignore setup/teardown
result = {"status": "passed"}
elif report.failed and step["failed"]:
result = {"status": "failed", "error_message": force_unicode(report.longrepr) if error_message else ""}
result = {"status": "failed", "error_message": str(report.longrepr) if error_message else ""}
elif report.skipped:
result = {"status": "skipped"}
result["duration"] = int(math.floor((10 ** 9) * step["duration"])) # nanosec
@ -171,11 +167,7 @@ class LogBDDCucumberJSON(object):
self.suite_start_time = time.time()
def pytest_sessionfinish(self):
if sys.version_info[0] < 3:
logfile_open = codecs.open
else:
logfile_open = open
with logfile_open(self.logfile, "w", encoding="utf-8") as logfile:
with open(self.logfile, "w", encoding="utf-8") as logfile:
logfile.write(json.dumps(list(self.features.values())))
def pytest_terminal_summary(self, terminalreporter):

View File

@ -1,5 +1,4 @@
"""pytest-bdd Exceptions."""
import six
class ScenarioIsDecoratorOnly(Exception):
@ -30,19 +29,14 @@ class StepDefinitionNotFoundError(Exception):
"""Step definition not found."""
class InvalidStepParserError(Exception):
"""Invalid step parser."""
class NoScenariosFound(Exception):
"""No scenarios found."""
@six.python_2_unicode_compatible
class FeatureError(Exception):
"""Feature parse error."""
message = u"{0}.\nLine number: {1}.\nLine: {2}.\nFile: {3}"
message = "{0}.\nLine number: {1}.\nLine: {2}.\nFile: {3}"
def __str__(self):
"""String representation."""

View File

@ -24,47 +24,15 @@ Syntax example:
one line.
"""
import os.path
import sys
import glob2
from .parser import parse_feature
# Global features dictionary
features = {}
def force_unicode(obj, encoding="utf-8"):
"""Get the unicode string out of given object (python 2 and python 3).
:param obj: An `object`, usually a string.
:return: unicode string.
"""
if sys.version_info < (3, 0):
if isinstance(obj, str):
return obj.decode(encoding)
else:
return unicode(obj)
else: # pragma: no cover
return str(obj)
def force_encode(string, encoding="utf-8"):
"""Force string encoding (Python compatibility function).
:param str string: A string value.
:param str encoding: Encoding.
:return: Encoded string.
"""
if sys.version_info < (3, 0):
if isinstance(string, unicode):
string = string.encode(encoding)
return string
def get_feature(base_path, filename, encoding="utf-8"):
"""Get a feature by the filename.

View File

@ -3,15 +3,14 @@
import itertools
import os.path
from mako.lookup import TemplateLookup
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 .steps import get_step_fixture_name
from .feature import get_features
from .types import STEP_TYPES
template_lookup = TemplateLookup(directories=[os.path.join(os.path.dirname(__file__), "templates")])
@ -109,7 +108,7 @@ def print_missing_code(scenarios, steps):
tw.write(code)
def _find_step_fixturedef(fixturemanager, item, name, type_, encoding="utf-8"):
def _find_step_fixturedef(fixturemanager, item, name, type_):
"""Find step fixturedef.
:param request: PyTest Item object.
@ -117,11 +116,11 @@ def _find_step_fixturedef(fixturemanager, item, name, type_, encoding="utf-8"):
:return: Step function.
"""
fixturedefs = fixturemanager.getfixturedefs(get_step_fixture_name(name, type_, encoding), item.nodeid)
fixturedefs = fixturemanager.getfixturedefs(get_step_fixture_name(name, type_), item.nodeid)
if not fixturedefs:
name = find_argumented_step_fixture_name(name, type_, fixturemanager)
if name:
return _find_step_fixturedef(fixturemanager, item, name, encoding)
return _find_step_fixturedef(fixturemanager, item, name, type_)
else:
return fixturedefs

View File

@ -1,7 +1,3 @@
# -*- encoding: utf-8 -*-
from __future__ import unicode_literals
import re
from _pytest.terminal import TerminalReporter

View File

@ -4,8 +4,6 @@ import re
import textwrap
from collections import OrderedDict
import six
from . import types, exceptions
SPLIT_LINE_RE = re.compile(r"(?<!\\)\|")
@ -193,7 +191,7 @@ def parse_feature(basedir, filename, encoding="utf-8"):
target.add_step(step)
prev_line = clean_line
feature.description = u"\n".join(description).strip()
feature.description = "\n".join(description).strip()
return feature
@ -292,7 +290,6 @@ class Scenario(object):
)
@six.python_2_unicode_compatible
class Step(object):
"""Step."""
@ -447,9 +444,6 @@ class Examples(object):
"""Bool comparison."""
return bool(self.vertical_examples or self.examples)
if six.PY2:
__nonzero__ = __bool__
def get_tags(line):
"""Get tags out of the given line.

View File

@ -3,13 +3,11 @@
from __future__ import absolute_import
import re as base_re
from functools import partial
import parse as base_parse
import six
from parse_type import cfparse as base_cfparse
from .exceptions import InvalidStepParserError
class StepParser(object):
"""Parser of the individual step."""
@ -22,11 +20,11 @@ class StepParser(object):
:return: `dict` of step arguments
"""
raise NotImplementedError()
raise NotImplementedError() # pragma: no cover
def is_matching(self, name):
"""Match given name with the step name."""
raise NotImplementedError()
raise NotImplementedError() # pragma: no cover
class re(StepParser):
@ -84,6 +82,11 @@ class cfparse(parse):
class string(StepParser):
"""Exact string step parser."""
def __init__(self, name):
"""Stringify"""
name = str(name, **({"encoding": "utf-8"} if isinstance(name, bytes) else {}))
super().__init__(name)
def parse_arguments(self, name):
"""No parameters are available for simple string step.
@ -104,11 +107,11 @@ def get_parser(step_name):
:return: step parser object
:rtype: StepArgumentParser
"""
if isinstance(step_name, six.string_types):
if isinstance(step_name, six.binary_type): # Python 2 compatibility
step_name = step_name.decode("utf-8")
return string(step_name)
elif not hasattr(step_name, "is_matching") or not hasattr(step_name, "parse_arguments"):
raise InvalidStepParserError(step_name)
else:
def does_support_parser_interface(obj):
return all(map(partial(hasattr, obj), ["is_matching", "parse_arguments"]))
if does_support_parser_interface(step_name):
return step_name
else:
return string(step_name)

View File

@ -2,11 +2,11 @@
import pytest
from . import given, when, then
from . import cucumber_json
from . import generation
from . import reporting
from . import gherkin_terminal_reporter
from . import given, when, then
from . import reporting
from .utils import CONFIG_STACK
@ -84,25 +84,3 @@ def pytest_cmdline_main(config):
def pytest_bdd_apply_tag(tag, function):
mark = getattr(pytest.mark, tag)
return mark(function)
@pytest.mark.tryfirst
def pytest_collection_modifyitems(session, config, items):
"""Re-order items using the creation counter as fallback.
Pytest has troubles to correctly order the test items for python < 3.6.
For this reason, we have to apply some better ordering for pytest_bdd scenario-decorated test functions.
This is not needed for python 3.6+, but this logic is safe to apply in that case as well.
"""
# TODO: Try to only re-sort the items that have __pytest_bdd_counter__, and not the others,
# since there may be other hooks that are executed before this and that want to reorder item as well
def item_key(item):
if isinstance(item, pytest.Function):
declaration_order = getattr(item.function, "__pytest_bdd_counter__", 0)
else:
declaration_order = 0
func, linenum = item.reportinfo()[:2]
return (func, linenum if linenum is not None else -1, declaration_order)
items.sort(key=item_key)

View File

@ -6,7 +6,6 @@ that enriches the pytest test reporting.
import time
from .feature import force_unicode
from .utils import get_parametrize_markers_args
@ -84,8 +83,7 @@ class ScenarioReport(object):
elif tuple(node_param_values) in param_values:
self.param_index = param_values.index(tuple(node_param_values))
self.example_kwargs = {
example_param: force_unicode(node.funcargs[example_param])
for example_param in scenario.get_example_params()
example_param: str(node.funcargs[example_param]) for example_param in scenario.get_example_params()
}
@property

View File

@ -22,7 +22,7 @@ except ImportError:
from _pytest import python as pytest_fixtures
from . import exceptions
from .feature import force_unicode, get_feature, get_features
from .feature import get_feature, get_features
from .steps import get_step_fixture_name, inject_fixture
from .utils import CONFIG_STACK, get_args, get_caller_module_locals, get_caller_module_path
@ -30,11 +30,6 @@ PYTHON_REPLACE_REGEX = re.compile(r"\W")
ALPHA_REGEX = re.compile(r"^\d+_*")
# We have to keep track of the invocation of @scenario() so that we can reorder test item accordingly.
# In python 3.6+ this is no longer necessary, as the order is automatically retained.
_py2_scenario_creation_counter = 0
def find_argumented_step_fixture_name(name, type_, fixturemanager, request=None):
"""Find argumented step fixture name."""
# happens to be that _arg2fixturedefs is changed during the iteration so we use a copy
@ -58,7 +53,7 @@ def find_argumented_step_fixture_name(name, type_, fixturemanager, request=None)
return parser_name
def _find_step_function(request, step, scenario, encoding):
def _find_step_function(request, step, scenario):
"""Match the step defined by the regular expression pattern.
:param request: PyTest request object.
@ -71,7 +66,7 @@ def _find_step_function(request, step, scenario, encoding):
name = step.name
try:
# Simple case where no parser is used for the step
return request.getfixturevalue(get_step_fixture_name(name, step.type, encoding))
return request.getfixturevalue(get_step_fixture_name(name, step.type))
except pytest_fixtures.FixtureLookupError:
try:
# Could not find a fixture with the same name, let's see if there is a parser involved
@ -81,10 +76,8 @@ def _find_step_function(request, step, scenario, encoding):
raise
except pytest_fixtures.FixtureLookupError:
raise exceptions.StepDefinitionNotFoundError(
u"""Step definition is not found: {step}."""
""" Line {step.line_number} in scenario "{scenario.name}" in the feature "{feature.filename}""".format(
step=step, scenario=scenario, feature=scenario.feature
)
f"Step definition is not found: {step}. "
f'Line {step.line_number} in scenario "{scenario.name}" in the feature "{scenario.feature.filename}"'
)
@ -120,7 +113,7 @@ def _execute_step_function(request, scenario, step, step_func):
raise
def _execute_scenario(feature, scenario, request, encoding):
def _execute_scenario(feature, scenario, request):
"""Execute the scenario.
:param feature: Feature.
@ -134,7 +127,7 @@ def _execute_scenario(feature, scenario, request, encoding):
# Execute scenario steps
for step in scenario.steps:
try:
step_func = _find_step_function(request, step, scenario, encoding=encoding)
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
@ -148,12 +141,7 @@ def _execute_scenario(feature, scenario, request, encoding):
FakeRequest = collections.namedtuple("FakeRequest", ["module"])
def _get_scenario_decorator(feature, feature_name, scenario, scenario_name, encoding):
global _py2_scenario_creation_counter
counter = _py2_scenario_creation_counter
_py2_scenario_creation_counter += 1
def _get_scenario_decorator(feature, feature_name, scenario, scenario_name):
# HACK: Ideally we would use `def decorator(fn)`, but we want to return a custom exception
# when the decorator is misused.
# Pytest inspect the signature to determine the required fixtures, and in that case it would look
@ -174,7 +162,7 @@ def _get_scenario_decorator(feature, feature_name, scenario, scenario_name, enco
@pytest.mark.usefixtures(*function_args)
def scenario_wrapper(request):
_execute_scenario(feature, scenario, request, encoding)
_execute_scenario(feature, scenario, request)
return fn(*[request.getfixturevalue(arg) for arg in args])
for param_set in scenario.get_params():
@ -184,11 +172,8 @@ def _get_scenario_decorator(feature, feature_name, scenario, scenario_name, enco
config = CONFIG_STACK[-1]
config.hook.pytest_bdd_apply_tag(tag=tag, function=scenario_wrapper)
scenario_wrapper.__doc__ = u"{feature_name}: {scenario_name}".format(
feature_name=feature_name, scenario_name=scenario_name
)
scenario_wrapper.__doc__ = f"{feature_name}: {scenario_name}"
scenario_wrapper.__scenario__ = scenario
scenario_wrapper.__pytest_bdd_counter__ = counter
scenario.test_function = scenario_wrapper
return scenario_wrapper
@ -205,7 +190,7 @@ def scenario(feature_name, scenario_name, encoding="utf-8", example_converters=N
example parameter, and value is the converter function.
"""
scenario_name = force_unicode(scenario_name, encoding)
scenario_name = str(scenario_name)
caller_module_path = get_caller_module_path()
# Get the feature
@ -217,10 +202,9 @@ def scenario(feature_name, scenario_name, encoding="utf-8", example_converters=N
try:
scenario = feature.scenarios[scenario_name]
except KeyError:
feature_name = feature.name or "[Empty]"
raise exceptions.ScenarioNotFound(
u'Scenario "{scenario_name}" in feature "{feature_name}" in {feature_filename} is not found.'.format(
scenario_name=scenario_name, feature_name=feature.name or "[Empty]", feature_filename=feature.filename
)
f'Scenario "{scenario_name}" in feature "{feature_name}" in {feature.filename} is not found.'
)
scenario.example_converters = example_converters
@ -229,7 +213,7 @@ def scenario(feature_name, scenario_name, encoding="utf-8", example_converters=N
scenario.validate()
return _get_scenario_decorator(
feature=feature, feature_name=feature_name, scenario=scenario, scenario_name=scenario_name, encoding=encoding
feature=feature, feature_name=feature_name, scenario=scenario, scenario_name=scenario_name
)
@ -256,12 +240,12 @@ def make_python_name(string):
def make_python_docstring(string):
"""Make a python docstring literal out of a given string."""
return u'"""{}."""'.format(string.replace(u'"""', u'\\"\\"\\"'))
return '"""{}."""'.format(string.replace('"""', '\\"\\"\\"'))
def make_string_literal(string):
"""Make python string literal out of a given string."""
return u"'{}'".format(string.replace(u"'", u"\\'"))
return "'{}'".format(string.replace("'", "\\'"))
def get_python_name_generator(name):

View File

@ -5,7 +5,6 @@ import os.path
import re
import glob2
import six
from .generation import generate_code, parse_feature_files
@ -49,10 +48,7 @@ def print_generated_code(args):
"""Print generated test code for the given filenames."""
features, scenarios, steps = parse_feature_files(args.files)
code = generate_code(features, scenarios, steps)
if six.PY2:
print(code.encode("utf-8"))
else:
print(code)
print(code)
def main():

View File

@ -36,7 +36,6 @@ def given_beautiful_article(article):
"""
from __future__ import absolute_import
import inspect
import pytest
@ -45,24 +44,20 @@ try:
except ImportError:
from _pytest import python as pytest_fixtures
from .feature import force_encode
from .types import GIVEN, WHEN, THEN
from .parsers import get_parser
from .utils import get_args, get_caller_module_locals
def get_step_fixture_name(name, type_, encoding=None):
def get_step_fixture_name(name, type_):
"""Get step fixture name.
:param name: unicode string
:param name: string
:param type: step type
:param encoding: encoding
:return: step fixture name
:rtype: string
"""
return "pytestbdd_{type}_{name}".format(
type=type_, name=force_encode(name, **(dict(encoding=encoding) if encoding else {}))
)
return f"pytestbdd_{type_}_{name}"
def given(name, converters=None, target_fixture=None):
@ -118,7 +113,7 @@ def _step_decorator(step_type, step_name, converters=None, target_fixture=None):
parser_instance = get_parser(step_name)
parsed_step_name = parser_instance.name
step_func.__name__ = force_encode(parsed_step_name)
step_func.__name__ = str(parsed_step_name)
def lazy_step_func():
return step_func

View File

@ -1,5 +1,4 @@
% if features:
# coding=utf-8
"""${ features[0].name or features[0].rel_filename } feature tests."""
from pytest_bdd import (

View File

@ -1,47 +1,25 @@
"""Various utility functions."""
from sys import _getframe
from inspect import getframeinfo
import six
from inspect import signature as _signature
from sys import _getframe
CONFIG_STACK = []
if six.PY2:
from inspect import getargspec as _getargspec
def get_args(func):
"""Get a list of argument names for a function.
def get_args(func):
"""Get a list of argument names for a function.
:param func: The function to inspect.
:param func: The function to inspect.
:return: A list of argument names.
:rtype: list
"""
return _getargspec(func).args
else:
from inspect import signature as _signature
def get_args(func):
"""Get a list of argument names for a function.
:param func: The function to inspect.
:return: A list of argument names.
:rtype: list
"""
params = _signature(func).parameters.values()
return [param.name for param in params if param.kind == param.POSITIONAL_OR_KEYWORD]
:return: A list of argument names.
:rtype: list
"""
params = _signature(func).parameters.values()
return [param.name for param in params if param.kind == param.POSITIONAL_OR_KEYWORD]
def get_parametrize_markers_args(node):
"""In pytest 3.6 new API to access markers has been introduced and it deprecated
MarkInfo objects.
This function uses that API if it is available otherwise it uses MarkInfo objects.
"""
return tuple(arg for mark in node.iter_markers("parametrize") for arg in mark.args)

View File

@ -7,7 +7,6 @@ import re
from setuptools import setup
dirname = os.path.dirname(__file__)
long_description = (
@ -42,8 +41,9 @@ setup(
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 3",
]
+ [("Programming Language :: Python :: %s" % x) for x in "2.7 3.5 3.6 3.7 3.8".split()],
install_requires=["glob2", "Mako", "parse", "parse_type", "py", "pytest>=4.3", "six>=1.9.0"],
+ [("Programming Language :: Python :: %s" % x) for x in "3.6 3.7 3.8 3.9".split()],
python_requires=">=3.6",
install_requires=["glob2", "Mako", "parse", "parse_type", "py", "pytest>=4.3"],
# the following makes a plugin available to py.test
entry_points={
"pytest11": ["pytest-bdd = pytest_bdd.plugin"],

View File

@ -8,12 +8,13 @@ def runandparse(testdir, *args):
"""Run tests in testdir and parse json output."""
resultpath = testdir.tmpdir.join("cucumber.json")
result = testdir.runpytest("--cucumberjson={0}".format(resultpath), "-s", *args)
jsonobject = json.load(resultpath.open())
with resultpath.open() as f:
jsonobject = json.load(f)
return result, jsonobject
class OfType(object):
"""Helper object comparison to which is always 'equal'."""
"""Helper object to help compare object type to initialization type"""
def __init__(self, type=None):
self.type = type
@ -22,9 +23,6 @@ class OfType(object):
return isinstance(other, self.type) if self.type else True
string = type(u"")
def test_step_trace(testdir):
"""Test step trace."""
testdir.makefile(
@ -155,7 +153,7 @@ def test_step_trace(testdir):
"line": 12,
"match": {"location": ""},
"name": "a failing step",
"result": {"error_message": OfType(string), "status": "failed", "duration": OfType(int)},
"result": {"error_message": OfType(str), "status": "failed", "duration": OfType(int)},
},
],
"tags": [{"name": "scenario-failing-tag", "line": 9}],

View File

@ -8,7 +8,7 @@ def test_no_scenarios(testdir):
features = testdir.mkdir("features")
features.join("test.feature").write_text(
textwrap.dedent(
u"""
"""
Given foo
When bar
Then baz

View File

@ -1,5 +1,6 @@
"""Test scenario reporting."""
import textwrap
import pytest
@ -103,22 +104,22 @@ def test_step_trace(testdir):
report = result.matchreport("test_passing", when="call").scenario
expected = {
"feature": {
"description": u"",
"description": "",
"filename": feature.strpath,
"line_number": 2,
"name": u"One passing scenario, one failing scenario",
"name": "One passing scenario, one failing scenario",
"rel_filename": relpath,
"tags": [u"feature-tag"],
"tags": ["feature-tag"],
},
"line_number": 5,
"name": u"Passing",
"name": "Passing",
"steps": [
{
"duration": OfType(float),
"failed": False,
"keyword": "Given",
"line_number": 6,
"name": u"a passing step",
"name": "a passing step",
"type": "given",
},
{
@ -126,11 +127,11 @@ def test_step_trace(testdir):
"failed": False,
"keyword": "And",
"line_number": 7,
"name": u"some other passing step",
"name": "some other passing step",
"type": "given",
},
],
"tags": [u"scenario-passing-tag"],
"tags": ["scenario-passing-tag"],
"examples": [],
"example_kwargs": {},
}
@ -140,22 +141,22 @@ def test_step_trace(testdir):
report = result.matchreport("test_failing", when="call").scenario
expected = {
"feature": {
"description": u"",
"description": "",
"filename": feature.strpath,
"line_number": 2,
"name": u"One passing scenario, one failing scenario",
"name": "One passing scenario, one failing scenario",
"rel_filename": relpath,
"tags": [u"feature-tag"],
"tags": ["feature-tag"],
},
"line_number": 10,
"name": u"Failing",
"name": "Failing",
"steps": [
{
"duration": OfType(float),
"failed": False,
"keyword": "Given",
"line_number": 11,
"name": u"a passing step",
"name": "a passing step",
"type": "given",
},
{
@ -163,11 +164,11 @@ def test_step_trace(testdir):
"failed": True,
"keyword": "And",
"line_number": 12,
"name": u"a failing step",
"name": "a failing step",
"type": "given",
},
],
"tags": [u"scenario-failing-tag"],
"tags": ["scenario-failing-tag"],
"examples": [],
"example_kwargs": {},
}
@ -176,22 +177,22 @@ def test_step_trace(testdir):
report = result.matchreport("test_outlined[12-5.0-7]", when="call").scenario
expected = {
"feature": {
"description": u"",
"description": "",
"filename": feature.strpath,
"line_number": 2,
"name": u"One passing scenario, one failing scenario",
"name": "One passing scenario, one failing scenario",
"rel_filename": relpath,
"tags": [u"feature-tag"],
"tags": ["feature-tag"],
},
"line_number": 14,
"name": u"Outlined",
"name": "Outlined",
"steps": [
{
"duration": OfType(float),
"failed": False,
"keyword": "Given",
"line_number": 15,
"name": u"there are <start> cucumbers",
"name": "there are <start> cucumbers",
"type": "given",
},
{
@ -199,7 +200,7 @@ def test_step_trace(testdir):
"failed": False,
"keyword": "When",
"line_number": 16,
"name": u"I eat <eat> cucumbers",
"name": "I eat <eat> cucumbers",
"type": "when",
},
{
@ -207,7 +208,7 @@ def test_step_trace(testdir):
"failed": False,
"keyword": "Then",
"line_number": 17,
"name": u"I should have <left> cucumbers",
"name": "I should have <left> cucumbers",
"type": "then",
},
],
@ -227,22 +228,22 @@ def test_step_trace(testdir):
report = result.matchreport("test_outlined[5-4.0-1]", when="call").scenario
expected = {
"feature": {
"description": u"",
"description": "",
"filename": feature.strpath,
"line_number": 2,
"name": u"One passing scenario, one failing scenario",
"name": "One passing scenario, one failing scenario",
"rel_filename": relpath,
"tags": [u"feature-tag"],
"tags": ["feature-tag"],
},
"line_number": 14,
"name": u"Outlined",
"name": "Outlined",
"steps": [
{
"duration": OfType(float),
"failed": False,
"keyword": "Given",
"line_number": 15,
"name": u"there are <start> cucumbers",
"name": "there are <start> cucumbers",
"type": "given",
},
{
@ -250,7 +251,7 @@ def test_step_trace(testdir):
"failed": False,
"keyword": "When",
"line_number": 16,
"name": u"I eat <eat> cucumbers",
"name": "I eat <eat> cucumbers",
"type": "when",
},
{
@ -258,7 +259,7 @@ def test_step_trace(testdir):
"failed": False,
"keyword": "Then",
"line_number": 17,
"name": u"I should have <left> cucumbers",
"name": "I should have <left> cucumbers",
"type": "then",
},
],

View File

@ -26,7 +26,7 @@ def test_scenarios(testdir, pytest_params):
features = testdir.mkdir("features")
features.join("test.feature").write_text(
textwrap.dedent(
u"""
"""
Scenario: Test scenario
Given I have a bar
"""
@ -36,7 +36,7 @@ def test_scenarios(testdir, pytest_params):
)
features.join("subfolder", "test.feature").write_text(
textwrap.dedent(
u"""
"""
Scenario: Test subfolder scenario
Given I have a bar

View File

@ -1,6 +1,7 @@
"""Test tags."""
import textwrap
import pkg_resources
import pytest
from pytest_bdd.parser import get_tags
@ -234,7 +235,15 @@ def test_at_in_scenario(testdir):
scenarios('test.feature')
"""
)
result = testdir.runpytest_subprocess("--strict")
# Deprecate --strict after pytest 6.1
# https://docs.pytest.org/en/stable/deprecations.html#the-strict-command-line-option
pytest_version = pkg_resources.get_distribution("pytest").parsed_version
if pytest_version >= pkg_resources.parse_version("6.2"):
strict_option = "--strict-markers"
else:
strict_option = "--strict"
result = testdir.runpytest_subprocess(strict_option)
result.stdout.fnmatch_lines(["*= 2 passed * =*"])

View File

@ -1,12 +1,9 @@
# coding=utf-8
"""Test code generation command."""
import os
import sys
import textwrap
import six
from pytest_bdd.scripts import main
PATH = os.path.dirname(__file__)
@ -19,7 +16,7 @@ def test_generate(testdir, monkeypatch, capsys):
feature = features.join("generate.feature")
feature.write_text(
textwrap.dedent(
u"""\
"""\
Feature: Code generation
Scenario: Given and when using the same fixture should not evaluate it twice
@ -39,8 +36,7 @@ def test_generate(testdir, monkeypatch, capsys):
main()
out, err = capsys.readouterr()
assert out == textwrap.dedent(
'''
# coding=utf-8
'''\
"""Code generation feature tests."""
from pytest_bdd import (
@ -79,11 +75,7 @@ def test_generate(testdir, monkeypatch, capsys):
"""my list should be [1]."""
raise NotImplementedError
'''[
1:
].replace(
u"'", u"'"
)
'''
)
@ -111,7 +103,6 @@ def test_generate_with_quotes(testdir):
result = testdir.run("pytest-bdd", "generate", "generate_with_quotes.feature")
assert result.stdout.str() == textwrap.dedent(
'''\
# coding=utf-8
"""Handling quotes in code generation feature tests."""
from pytest_bdd import (
@ -165,7 +156,7 @@ def test_generate_with_quotes(testdir):
)
def test_unicode_characters(testdir):
def test_unicode_characters(testdir, monkeypatch):
"""Test generating code with unicode characters.
Primary purpose is to ensure compatibility with Python2.
@ -175,7 +166,6 @@ def test_unicode_characters(testdir):
".feature",
unicode_characters=textwrap.dedent(
"""\
# coding=utf-8
Feature: Generating unicode characters
Scenario: Calculating the circumference of a circle
@ -186,10 +176,12 @@ def test_unicode_characters(testdir):
),
)
if sys.version_info < (3, 7):
monkeypatch.setenv("PYTHONIOENCODING", "utf-8")
result = testdir.run("pytest-bdd", "generate", "unicode_characters.feature")
expected_output = textwrap.dedent(
u'''\
# coding=utf-8
'''\
"""Generating unicode characters feature tests."""
from pytest_bdd import (
@ -218,11 +210,9 @@ def test_unicode_characters(testdir):
@then('We calculate 2 * ℼ * 𝑟')
def {function_with_unicode_chars}():
def we_calculate_2__ℼ__𝑟():
"""We calculate 2 * ℼ * 𝑟."""
raise NotImplementedError
'''.format(
function_with_unicode_chars=u"we_calculate_2__ℼ__𝑟" if not six.PY2 else u"we_calculate_2____"
)
'''
)
assert result.stdout.str() == expected_output

View File

@ -1,4 +1,3 @@
# coding: utf-8
"""Tests for testing cases when we have unicode in feature file."""
import textwrap
@ -8,7 +7,7 @@ def test_steps_in_feature_file_have_unicode(testdir):
testdir.makefile(
".feature",
unicode=textwrap.dedent(
u"""\
"""\
Feature: Юнікодні символи
Scenario: Кроки в .feature файлі містять юнікод
@ -24,8 +23,7 @@ def test_steps_in_feature_file_have_unicode(testdir):
testdir.makepyfile(
textwrap.dedent(
u"""\
# coding: utf-8
"""\
import sys
import pytest
from pytest_bdd import parsers, given, then, scenario
@ -50,8 +48,6 @@ 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):
assert string["content"] == content
if sys.version_info < (3, 0):
assert isinstance(content, unicode)
"""
)
)
@ -63,7 +59,7 @@ def test_steps_in_py_file_have_unicode(testdir):
testdir.makefile(
".feature",
unicode=textwrap.dedent(
u"""\
"""\
Feature: Юнікодні символи
Scenario: Steps in .py file have unicode
@ -75,8 +71,7 @@ def test_steps_in_py_file_have_unicode(testdir):
testdir.makepyfile(
textwrap.dedent(
u"""\
# coding: utf-8
"""\
import pytest
from pytest_bdd import given, then, scenario

10
tox.ini
View File

@ -1,10 +1,9 @@
[tox]
distshare = {homedir}/.tox/distshare
envlist = py38-pytestlatest-linters,
py27-pytest{43,44,45,46}-coverage,
py38-pytest{43,44,45,46,50,51,52,53,54,60, latest}-coverage,
py{35,36,38}-pytestlatest-coverage,
py27-pytestlatest-xdist-coverage
py39-pytest{43,44,45,46,50,51,52,53,54,60,61,62, latest}-coverage,
py{36,37,38}-pytestlatest-coverage,
py39-pytestlatest-xdist-coverage
skip_missing_interpreters = true
[testenv]
@ -13,6 +12,8 @@ setenv =
xdist: _PYTEST_MORE_ARGS=-n3 -rfsxX
deps =
pytestlatest: pytest
pytest62: pytest~=6.2.0
pytest61: pytest~=6.1.0
pytest60: pytest~=6.0.0
pytest54: pytest~=5.4.0
pytest53: pytest~=5.3.0
@ -29,6 +30,7 @@ deps =
-r{toxinidir}/requirements-testing.txt
commands = {env:_PYTEST_CMD:pytest} {env:_PYTEST_MORE_ARGS:} {posargs:-vvl}
; Black doesn't support >py38 now
[testenv:py38-pytestlatest-linters]
deps = black
commands = black --check --verbose setup.py docs pytest_bdd tests