Update python and pytest
- Drop support of python 2.7, 3.5; Add explicit support for python >=3.6 - Remove six dependency - Simplify getting step parser strategy; If step could be stringified it could be used - Respect pytest --strict option in tests
This commit is contained in:
parent
7cb344ef64
commit
e1dc0cad9a
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
16
docs/conf.py
16
docs/conf.py
|
@ -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",
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
[tool.black]
|
||||
line-length = 120
|
||||
target-version = ['py27', 'py35', 'py36', 'py37', 'py38']
|
||||
target-version = ['py36', 'py37', 'py38']
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
from _pytest.terminal import TerminalReporter
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
% if features:
|
||||
# coding=utf-8
|
||||
"""${ features[0].name or features[0].rel_filename } feature tests."""
|
||||
|
||||
from pytest_bdd import (
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
6
setup.py
6
setup.py
|
@ -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"],
|
||||
|
|
|
@ -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}],
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
],
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 * =*"])
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
10
tox.ini
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue