diff --git a/.travis.yml b/.travis.yml index 8728d6e..db0a38b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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: diff --git a/CHANGES.rst b/CHANGES.rst index f2745ec..b6c981e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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) diff --git a/docs/conf.py b/docs/conf.py index 46ae470..b1a5a43 100644 --- a/docs/conf.py +++ b/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", diff --git a/pyproject.toml b/pyproject.toml index 632eb80..f383bfd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,3 @@ [tool.black] line-length = 120 -target-version = ['py27', 'py35', 'py36', 'py37', 'py38'] +target-version = ['py36', 'py37', 'py38'] diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index fa0b3a1..e2d550c 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -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): diff --git a/pytest_bdd/exceptions.py b/pytest_bdd/exceptions.py index 17ef5c3..2570674 100644 --- a/pytest_bdd/exceptions.py +++ b/pytest_bdd/exceptions.py @@ -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.""" diff --git a/pytest_bdd/feature.py b/pytest_bdd/feature.py index 7051eb7..7ab3862 100644 --- a/pytest_bdd/feature.py +++ b/pytest_bdd/feature.py @@ -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. diff --git a/pytest_bdd/generation.py b/pytest_bdd/generation.py index 6f7e637..5267304 100644 --- a/pytest_bdd/generation.py +++ b/pytest_bdd/generation.py @@ -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 diff --git a/pytest_bdd/gherkin_terminal_reporter.py b/pytest_bdd/gherkin_terminal_reporter.py index b4c9d4b..97f7ae7 100644 --- a/pytest_bdd/gherkin_terminal_reporter.py +++ b/pytest_bdd/gherkin_terminal_reporter.py @@ -1,7 +1,3 @@ -# -*- encoding: utf-8 -*- - -from __future__ import unicode_literals - import re from _pytest.terminal import TerminalReporter diff --git a/pytest_bdd/parser.py b/pytest_bdd/parser.py index 702b7fb..c912b80 100644 --- a/pytest_bdd/parser.py +++ b/pytest_bdd/parser.py @@ -4,8 +4,6 @@ import re import textwrap from collections import OrderedDict -import six - from . import types, exceptions SPLIT_LINE_RE = re.compile(r"(?=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"], diff --git a/tests/feature/test_cucumber_json.py b/tests/feature/test_cucumber_json.py index 0c35c59..82f76a1 100644 --- a/tests/feature/test_cucumber_json.py +++ b/tests/feature/test_cucumber_json.py @@ -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}], diff --git a/tests/feature/test_no_scenario.py b/tests/feature/test_no_scenario.py index 44c8ede..d9a8754 100644 --- a/tests/feature/test_no_scenario.py +++ b/tests/feature/test_no_scenario.py @@ -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 diff --git a/tests/feature/test_report.py b/tests/feature/test_report.py index 70a3b84..56eac0f 100644 --- a/tests/feature/test_report.py +++ b/tests/feature/test_report.py @@ -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 cucumbers", + "name": "there are cucumbers", "type": "given", }, { @@ -199,7 +200,7 @@ def test_step_trace(testdir): "failed": False, "keyword": "When", "line_number": 16, - "name": u"I eat cucumbers", + "name": "I 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 cucumbers", + "name": "I should have 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 cucumbers", + "name": "there are cucumbers", "type": "given", }, { @@ -250,7 +251,7 @@ def test_step_trace(testdir): "failed": False, "keyword": "When", "line_number": 16, - "name": u"I eat cucumbers", + "name": "I 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 cucumbers", + "name": "I should have cucumbers", "type": "then", }, ], diff --git a/tests/feature/test_scenarios.py b/tests/feature/test_scenarios.py index 4e4ab59..ba60ee2 100644 --- a/tests/feature/test_scenarios.py +++ b/tests/feature/test_scenarios.py @@ -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 diff --git a/tests/feature/test_tags.py b/tests/feature/test_tags.py index 67650b2..0eb24bc 100644 --- a/tests/feature/test_tags.py +++ b/tests/feature/test_tags.py @@ -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 * =*"]) diff --git a/tests/scripts/test_generate.py b/tests/scripts/test_generate.py index 7d76793..fc8eeec 100644 --- a/tests/scripts/test_generate.py +++ b/tests/scripts/test_generate.py @@ -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 diff --git a/tests/steps/test_unicode.py b/tests/steps/test_unicode.py index 69160c2..4749779 100644 --- a/tests/steps/test_unicode.py +++ b/tests/steps/test_unicode.py @@ -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 diff --git a/tox.ini b/tox.ini index 4725667..52138d9 100644 --- a/tox.ini +++ b/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