Skip to content

Commit

Permalink
Merge pull request #894 from jenisys/fix/python3.9
Browse files Browse the repository at this point in the history
FIX: Some regressions in test suite for python 3.9
  • Loading branch information
jenisys committed Jan 9, 2021
2 parents d6d43f3 + 2f84ea7 commit e37f162
Show file tree
Hide file tree
Showing 31 changed files with 867 additions and 191 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ tools/virtualenvs
.ropeproject
nosetests.xml
rerun.txt
testrun*.json
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ CLARIFICATION:

FIXED:

* FIXED: Some tests related to python3.9
* FIXED: active-tag logic if multiple tags with same category exists.
* issue #772: ScenarioOutline.Examples without table (submitted by: The-QA-Geek)
* issue #755: Failures with Python 3.8 (submitted by: hroncok)
* issue #725: Scenario Outline description lines seem to be ignored (submitted by: nizwiz)
Expand Down
12 changes: 8 additions & 4 deletions behave/api/runtime_constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"""

from __future__ import absolute_import
import six
import sys
from behave.exception import ConstraintError


Expand All @@ -19,11 +21,10 @@ def require_min_python_version(minimal_version):
:param minimal_version: Minimum version (as string, tuple)
:raises: behave.exception.ConstraintError
"""
import six
import sys
python_version = sys.version_info
if isinstance(minimal_version, six.string_types):
python_version = "%s.%s" % sys.version_info[:2]
python_version = float("%s.%s" % sys.version_info[:2])
minimal_version = float(minimal_version)
elif not isinstance(minimal_version, tuple):
raise TypeError("string or tuple (was: %s)" % type(minimal_version))

Expand All @@ -40,6 +41,9 @@ def require_min_behave_version(minimal_version):
"""
# -- SIMPLISTIC IMPLEMENTATION:
from behave.version import VERSION as behave_version
if behave_version < minimal_version:
behave_version2 = behave_version.split(".")
minimal_version2 = minimal_version.split(".")
if behave_version2 < minimal_version2:
# -- USE: Tuple comparison as version comparison.
raise ConstraintError("behave >= %s expected (was: %s)" % \
(minimal_version, behave_version))
81 changes: 81 additions & 0 deletions behave/python_feature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# -*- coding: UTF-8 -*-
"""
Provides a knowledge database if some Python features are supported
in the current python version.
"""

from __future__ import absolute_import
import sys
import six
from behave.tag_matcher import bool_to_string


# -----------------------------------------------------------------------------
# CONSTANTS:
# -----------------------------------------------------------------------------
PYTHON_VERSION = sys.version_info[:2]


# -----------------------------------------------------------------------------
# CLASSES:
# -----------------------------------------------------------------------------
class PythonFeature(object):

@staticmethod
def has_asyncio_coroutine_decorator():
"""Indicates if python supports ``@asyncio.coroutine`` decorator.
EXAMPLE::
import asyncio
@asyncio.coroutine
def async_waits_seconds(duration):
yield from asyncio.sleep(duration)
:returns: True, if this python version supports this feature.
.. since:: Python >= 3.4
.. deprecated:: Since Python 3.8 (use async-function instead)
"""
# -- NOTE: @asyncio.coroutine is deprecated in py3.8, removed in py3.10
return (3, 4) <= PYTHON_VERSION < (3, 10)

@staticmethod
def has_async_function():
"""Indicates if python supports async-functions / async-keyword.
EXAMPLE::
import asyncio
async def async_waits_seconds(duration):
yield from asyncio.sleep(duration)
:returns: True, if this python version supports this feature.
.. since:: Python >= 3.5
"""
return (3, 5) <= PYTHON_VERSION

@classmethod
def has_coroutine(cls):
return cls.has_async_function() or cls.has_asyncio_coroutine_decorator()


# -----------------------------------------------------------------------------
# SUPPORTED: ACTIVE-TAGS
# -----------------------------------------------------------------------------
PYTHON_HAS_ASYNCIO_COROUTINE_DECORATOR = PythonFeature.has_asyncio_coroutine_decorator()
PYTHON_HAS_ASYNC_FUNCTION = PythonFeature.has_async_function()
PYTHON_HAS_COROUTINE = PythonFeature.has_coroutine()
ACTIVE_TAG_VALUE_PROVIDER = {
"python2": bool_to_string(six.PY2),
"python3": bool_to_string(six.PY3),
"python.version": "%s.%s" % PYTHON_VERSION,
"os": sys.platform.lower(),

# -- PYTHON FEATURE, like: @use.with_py.feature_asyncio.coroutine
"python_has_coroutine": bool_to_string(PYTHON_HAS_COROUTINE),
"python_has_asyncio.coroutine_decorator":
bool_to_string(PYTHON_HAS_ASYNCIO_COROUTINE_DECORATOR),
"python_has_async_function": bool_to_string(PYTHON_HAS_ASYNC_FUNCTION),
"python_has_async_keyword": bool_to_string(PYTHON_HAS_ASYNC_FUNCTION),
}
82 changes: 62 additions & 20 deletions behave/tag_matcher.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: UTF-8 -*-
"""
Contains classes and functionality to provide a skip-if logic based on tags
in feature files.
Contains classes and functionality to provide the active-tag mechanism.
Active-tags provide a skip-if logic based on tags in feature files.
"""

from __future__ import absolute_import
Expand All @@ -10,6 +10,11 @@
import six


def bool_to_string(value):
"""Converts a Boolean value into its normalized string representation."""
return str(bool(value)).lower()


class TagMatcher(object):
"""Abstract base class that defines the TagMatcher protocol."""

Expand All @@ -36,12 +41,13 @@ def should_exclude_with(self, tags):
class ActiveTagMatcher(TagMatcher):
"""Provides an active tag matcher for many categories.
TAG SCHEMA:
TAG SCHEMA 1 (preferred):
* use.with_{category}={value}
* not.with_{category}={value}
TAG SCHEMA 2:
* active.with_{category}={value}
* not_active.with_{category}={value}
* only.with_{category}={value} (NOTE: For backward compatibility)
TAG LOGIC
----------
Expand All @@ -52,7 +58,7 @@ class ActiveTagMatcher(TagMatcher):
active_group.enabled := enabled(group.tag1) or enabled(group.tag2) or ...
active_tags.enabled := enabled(group1) and enabled(group2) and ...
All active-tag groups must be turned "on".
All active-tag groups must be turned "on" (enabled).
Otherwise, the model element should be excluded.
CONCEPT: ValueProvider
Expand Down Expand Up @@ -81,12 +87,12 @@ def get(self, category_name, default=None):
# -- FILE: features/alice.feature
Feature:
@active.with_os=win32
@use.with_os=win32
Scenario: Alice (Run only on Windows)
Given I do something
...
@not_active.with_browser=chrome
@not.with_browser=chrome
Scenario: Bob (Excluded with Web-Browser Chrome)
Given I do something else
...
Expand Down Expand Up @@ -116,7 +122,7 @@ def before_scenario(context, scenario):
scenario.skip(exclude_reason) #< LATE-EXCLUDE from run-set.
"""
value_separator = "="
tag_prefixes = ["active", "not_active", "use", "not", "only"]
tag_prefixes = ["use", "not", "active", "not_active", "only"]
tag_schema = r"^(?P<prefix>%s)\.with_(?P<category>\w+(\.\w+)*)%s(?P<value>.*)$"
ignore_unknown_categories = True
use_exclude_reason = False
Expand Down Expand Up @@ -163,21 +169,49 @@ def is_tag_negated(self, tag): # pylint: disable=no-self-use

def is_tag_group_enabled(self, group_category, group_tag_pairs):
"""Provides boolean logic to determine if all active-tags
which use the same category result in a enabled value.
Use LOGICAL-OR expression for active-tags with same category::
category_tag_group.enabled := enabled(tag1) or enabled(tag2) or ...
which use the same category result in an enabled value.
.. code-block:: gherkin
@use.with_xxx=alice
@use.with_xxx=bob
@not.with_xxx=charly
@not.with_xxx=doro
Scenario:
Given a step passes
...
Use LOGICAL expression for active-tags with same category::
category_tag_group.enabled := positive-tag-expression and not negative-tag-expression
positive-tag-expression := enabled(tag1) or enabled(tag2) or ...
negative-tag-expression := enabled(tag3) or enabled(tag4) or ...
tag1, tag2 are positive-tags, like @use.with_category=value
tag3, tag4 are negative-tags, like @not.with_category=value
xxx | Only use parts: (xxx == "alice") or (xxx == "bob")
-------+-------------------
alice | true
bob | true
other | false
xxx | Only not parts:
| (not xxx == "charly") and (not xxx == "doro")
| = not((xxx == "charly") or (xxx == "doro"))
-------+-------------------
charly | false
doro | false
other | true
xxx | Use and not parts:
| ((xxx == "alice") or (xxx == "bob")) and not((xxx == "charly") or (xxx == "doro"))
-------+-------------------
alice | true
bob | true
charly | false
doro | false
other | false
:param group_category: Category for this tag-group (as string).
:param category_tag_group: List of active-tag match-pairs.
:return: True, if tag-group is enabled.
Expand All @@ -191,20 +225,28 @@ def is_tag_group_enabled(self, group_category, group_tag_pairs):
# -- CASE: Unknown category, ignore it.
return True

tags_enabled = []
positive_tags_matched = []
negative_tags_matched = []
for category_tag, tag_match in group_tag_pairs:
tag_prefix = tag_match.group("prefix")
category = tag_match.group("category")
tag_value = tag_match.group("value")
assert category == group_category

is_category_tag_switched_on = operator.eq # equal_to
if self.is_tag_negated(tag_prefix):
is_category_tag_switched_on = operator.ne # not_equal_to

tag_enabled = is_category_tag_switched_on(tag_value, current_value)
tags_enabled.append(tag_enabled)
return any(tags_enabled) # -- PROVIDES: LOGICAL-OR expression
# -- CASE: @not.with_CATEGORY=VALUE
tag_matched = (tag_value == current_value)
negative_tags_matched.append(tag_matched)
else:
# -- CASE: @use.with_CATEGORY=VALUE
tag_matched = (tag_value == current_value)
positive_tags_matched.append(tag_matched)
tag_expression1 = any(positive_tags_matched) #< LOGICAL-OR expression
tag_expression2 = any(negative_tags_matched) #< LOGICAL-OR expression
if not positive_tags_matched:
tag_expression1 = True
tag_group_enabled = bool(tag_expression1 and not tag_expression2)
return tag_group_enabled

def should_exclude_with(self, tags):
group_categories = self.group_active_tags_by_category(tags)
Expand Down
1 change: 1 addition & 0 deletions bin/behave
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from __future__ import absolute_import
Expand Down
8 changes: 0 additions & 8 deletions bin/invoke

This file was deleted.

9 changes: 0 additions & 9 deletions bin/invoke.cmd

This file was deleted.

15 changes: 10 additions & 5 deletions bin/json.format.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
from __future__ import absolute_import

__author__ = "Jens Engel"
__copyright__ = "(c) 2011-2013 by Jens Engel"
VERSION = "0.2.2"
__copyright__ = "(c) 2011-2021 by Jens Engel"
VERSION = "0.3.0"

# -- IMPORTS:
import os.path
Expand All @@ -29,6 +29,7 @@
# CONSTANTS:
# ----------------------------------------------------------------------------
DEFAULT_INDENT_SIZE = 2
PYTHON_VERSION = sys.version_info[:2]

# ----------------------------------------------------------------------------
# FUNCTIONS:
Expand Down Expand Up @@ -58,7 +59,11 @@ def json_format(filename, indent=DEFAULT_INDENT_SIZE, **kwargs):
# return 0

contents = open(filename, "r").read()
data = json.loads(contents, encoding=encoding)
if PYTHON_VERSION >= (3, 1):
# -- NOTE: encoding keyword is deprecated since python 3.1
data = json.loads(contents)
else:
data = json.loads(contents, encoding=encoding)
contents2 = json.dumps(data, indent=indent, sort_keys=sort_keys)
contents2 = contents2.strip()
contents2 = "%s\n" % contents2
Expand All @@ -69,7 +74,7 @@ def json_format(filename, indent=DEFAULT_INDENT_SIZE, **kwargs):
outfile = open(filename, "w")
outfile.write(contents2)
outfile.close()
console.warn("%s OK", message)
console.warning("%s OK", message)
return 1 #< OK

def json_formatall(filenames, indent=DEFAULT_INDENT_SIZE, dry_run=False):
Expand Down Expand Up @@ -143,7 +148,7 @@ def main(args=None):
console.info("SKIP %s, no JSON files found in dir.", filename)
skipped += 1
elif not os.path.exists(filename):
console.warn("SKIP %s, file not found.", filename)
console.warning("SKIP %s, file not found.", filename)
skipped += 1
continue
else:
Expand Down

0 comments on commit e37f162

Please sign in to comment.