Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-pytest-bdd for 
openSUSE:Factory checked in at 2021-03-02 12:32:12
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-pytest-bdd (Old)
 and      /work/SRC/openSUSE:Factory/.python-pytest-bdd.new.2378 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-pytest-bdd"

Tue Mar  2 12:32:12 2021 rev:9 rq:875535 version:4.0.2

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-pytest-bdd/python-pytest-bdd.changes      
2020-09-23 18:48:19.373758629 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-pytest-bdd.new.2378/python-pytest-bdd.changes
    2021-03-02 12:44:38.948313631 +0100
@@ -1,0 +2,9 @@
+Fri Feb 26 20:50:41 UTC 2021 - Ben Greiner <c...@bnavigator.de>
+
+- update to 4.0.2
+  * Fix a bug that prevents using comments in the Examples:
+    section. (youtux) 
+- provide the correct u-a conrolled command to the tests
+- Skip failing tests: test_at_scenario and test_step_trace
+
+-------------------------------------------------------------------

Old:
----
  pytest-bdd-4.0.1.tar.gz

New:
----
  pytest-bdd-4.0.2.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-pytest-bdd.spec ++++++
--- /var/tmp/diff_new_pack.Jog5zD/_old  2021-03-02 12:44:39.376314001 +0100
+++ /var/tmp/diff_new_pack.Jog5zD/_new  2021-03-02 12:44:39.380314005 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python-pytest-bdd
 #
-# Copyright (c) 2020 SUSE LLC
+# Copyright (c) 2021 SUSE LLC
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -19,7 +19,7 @@
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 %bcond_without python2
 Name:           python-pytest-bdd
-Version:        4.0.1
+Version:        4.0.2
 Release:        0
 Summary:        BDD for pytest
 License:        MIT
@@ -80,9 +80,14 @@
 
 %check
 export LANG=en_US.UTF-8
-export PYTHONDONTWRITEBYTECODE=1
-# test_generate_with_quotes and test_unicode_characters require ptyest-bdd 
binary which we handle with u-a
-%pytest -k 'not test_generate_with_quotes and not test_unicode_characters'
+%{python_expand #  provide the u-a controlled command in PATH
+mkdir -p build/testbin
+ln -s %{buildroot}%{_bindir}/pytest-bdd-%{$python_bin_suffix} 
build/testbin/pytest-bdd
+}
+export PATH=$PWD/build/testbin:$PATH
+# test_at_in_scenario: the result footer looks slightly different
+# test_step_trace: unraisable exception in the obs environment
+%pytest -k "not (test_at_in_scenario or test_step_trace)" -ra
 
 %post
 %python_install_alternative pytest-bdd

++++++ pytest-bdd-4.0.1.tar.gz -> pytest-bdd-4.0.2.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-bdd-4.0.1/CHANGES.rst 
new/pytest-bdd-4.0.2/CHANGES.rst
--- old/pytest-bdd-4.0.1/CHANGES.rst    2020-09-08 12:03:15.000000000 +0200
+++ new/pytest-bdd-4.0.2/CHANGES.rst    2020-12-07 13:38:07.000000000 +0100
@@ -1,6 +1,11 @@
 Changelog
 =========
 
+4.0.2
+-----
+- Fix a bug that prevents using comments in the ``Examples:`` section. (youtux)
+
+
 4.0.1
 -----
 - Fixed performance regression introduced in 4.0.0 where collection time of 
tests would take way longer than before. (youtux)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-bdd-4.0.1/pytest_bdd/__init__.py 
new/pytest-bdd-4.0.2/pytest_bdd/__init__.py
--- old/pytest-bdd-4.0.1/pytest_bdd/__init__.py 2020-09-08 12:03:15.000000000 
+0200
+++ new/pytest-bdd-4.0.2/pytest_bdd/__init__.py 2020-12-07 13:38:07.000000000 
+0100
@@ -3,6 +3,6 @@
 from pytest_bdd.steps import given, when, then
 from pytest_bdd.scenario import scenario, scenarios
 
-__version__ = "4.0.1"
+__version__ = "4.0.2"
 
 __all__ = [given.__name__, when.__name__, then.__name__, scenario.__name__, 
scenarios.__name__]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-bdd-4.0.1/pytest_bdd/feature.py 
new/pytest-bdd-4.0.2/pytest_bdd/feature.py
--- old/pytest-bdd-4.0.1/pytest_bdd/feature.py  2020-09-08 12:03:15.000000000 
+0200
+++ new/pytest-bdd-4.0.2/pytest_bdd/feature.py  2020-12-07 13:38:07.000000000 
+0100
@@ -23,84 +23,18 @@
 :note: There're no multiline steps, the description of the step must fit in
 one line.
 """
-
-from collections import OrderedDict
-from os import path as op
-import codecs
-import re
+import os.path
 import sys
-import textwrap
 
 import glob2
-import six
 
-from . import types
-from . import exceptions
+from .parser import parse_feature
 
 
 # Global features dictionary
 features = {}
 
 
-STEP_PREFIXES = [
-    ("Feature: ", types.FEATURE),
-    ("Scenario Outline: ", types.SCENARIO_OUTLINE),
-    ("Examples: Vertical", types.EXAMPLES_VERTICAL),
-    ("Examples:", types.EXAMPLES),
-    ("Scenario: ", types.SCENARIO),
-    ("Background:", types.BACKGROUND),
-    ("Given ", types.GIVEN),
-    ("When ", types.WHEN),
-    ("Then ", types.THEN),
-    ("@", types.TAG),
-    # Continuation of the previously mentioned step type
-    ("And ", None),
-    ("But ", None),
-]
-
-STEP_PARAM_RE = re.compile(r"\<(.+?)\>")
-COMMENT_RE = re.compile(r"(^|(?<=\s))#")
-SPLIT_LINE_RE = re.compile(r"(?<!\\)\|")
-
-
-def get_step_type(line):
-    """Detect step type by the beginning of the line.
-
-    :param str line: Line of the Feature file.
-
-    :return: SCENARIO, GIVEN, WHEN, THEN, or `None` if can't be detected.
-    """
-    for prefix, _type in STEP_PREFIXES:
-        if line.startswith(prefix):
-            return _type
-
-
-def strip_comments(line):
-    """Remove comments.
-
-    :param str line: Line of the Feature file.
-
-    :return: Stripped line.
-    """
-    res = COMMENT_RE.search(line)
-    if res:
-        line = line[: res.start()]
-    return line.strip()
-
-
-def parse_line(line):
-    """Parse step line to get the step prefix (Scenario, Given, When, Then or 
And) and the actual step name.
-
-    :param line: Line of the Feature file.
-
-    :return: `tuple` in form ("<prefix>", "<Line without the prefix>").
-    """
-    for prefix, _ in STEP_PREFIXES:
-        if line.startswith(prefix):
-            return prefix.strip(), line[len(prefix) :].strip()
-    return "", line
-
-
 def force_unicode(obj, encoding="utf-8"):
     """Get the unicode string out of given object (python 2 and python 3).
 
@@ -131,26 +65,26 @@
     return string
 
 
-def get_tags(line):
-    """Get tags out of the given line.
-
-    :param str line: Feature file text line.
-
-    :return: List of tags.
-    """
-    if not line or not line.strip().startswith("@"):
-        return set()
-    return set((tag.lstrip("@") for tag in line.strip().split(" @") if 
len(tag) > 1))
-
+def get_feature(base_path, filename, encoding="utf-8"):
+    """Get a feature by the filename.
 
-def split_line(line):
-    """Split the given Examples line.
-
-    :param str|unicode line: Feature file Examples line.
-
-    :return: List of strings.
-    """
-    return [cell.replace("\\|", "|").strip() for cell in 
SPLIT_LINE_RE.split(line[1:-1])]
+    :param str base_path: Base feature directory.
+    :param str filename: Filename of the feature file.
+    :param str encoding: Feature file encoding.
+
+    :return: `Feature` instance from the parsed feature cache.
+
+    :note: The features are parsed on the execution of the test and
+           stored in the global variable cache to improve the performance
+           when multiple scenarios are referencing the same file.
+    """
+
+    full_name = os.path.abspath(os.path.join(base_path, filename))
+    feature = features.get(full_name)
+    if not feature:
+        feature = parse_feature(base_path, filename, encoding=encoding)
+        features[full_name] = feature
+    return feature
 
 
 def get_features(paths, **kwargs):
@@ -165,388 +99,11 @@
     for path in paths:
         if path not in seen_names:
             seen_names.add(path)
-            if op.isdir(path):
-                features.extend(get_features(glob2.iglob(op.join(path, "**", 
"*.feature")), **kwargs))
+            if os.path.isdir(path):
+                features.extend(get_features(glob2.iglob(os.path.join(path, 
"**", "*.feature")), **kwargs))
             else:
-                base, name = op.split(path)
-                feature = Feature.get_feature(base, name, **kwargs)
+                base, name = os.path.split(path)
+                feature = get_feature(base, name, **kwargs)
                 features.append(feature)
     features.sort(key=lambda feature: feature.name or feature.filename)
     return features
-
-
-class Examples(object):
-
-    """Example table."""
-
-    def __init__(self):
-        """Initialize examples instance."""
-        self.example_params = []
-        self.examples = []
-        self.vertical_examples = []
-        self.line_number = None
-        self.name = None
-
-    def set_param_names(self, keys):
-        """Set parameter names.
-
-        :param names: `list` of `string` parameter names.
-        """
-        self.example_params = [str(key) for key in keys]
-
-    def add_example(self, values):
-        """Add example.
-
-        :param values: `list` of `string` parameter values.
-        """
-        self.examples.append(values)
-
-    def add_example_row(self, param, values):
-        """Add example row.
-
-        :param param: `str` parameter name
-        :param values: `list` of `string` parameter values
-        """
-        if param in self.example_params:
-            raise exceptions.ExamplesNotValidError(
-                """Example rows should contain unique parameters. "{0}" 
appeared more than once""".format(param)
-            )
-        self.example_params.append(param)
-        self.vertical_examples.append(values)
-
-    def get_params(self, converters, builtin=False):
-        """Get scenario pytest parametrization table.
-
-        :param converters: `dict` of converter functions to convert parameter 
values
-        """
-        param_count = len(self.example_params)
-        if self.vertical_examples and not self.examples:
-            for value_index in range(len(self.vertical_examples[0])):
-                example = []
-                for param_index in range(param_count):
-                    
example.append(self.vertical_examples[param_index][value_index])
-                self.examples.append(example)
-
-        if self.examples:
-            params = []
-            for example in self.examples:
-                example = list(example)
-                for index, param in enumerate(self.example_params):
-                    raw_value = example[index]
-                    if converters and param in converters:
-                        value = converters[param](raw_value)
-                        if not builtin or value.__class__.__module__ in 
{"__builtin__", "builtins"}:
-                            example[index] = value
-                params.append(example)
-            return [self.example_params, params]
-        else:
-            return []
-
-    def __bool__(self):
-        """Bool comparison."""
-        return bool(self.vertical_examples or self.examples)
-
-    if six.PY2:
-        __nonzero__ = __bool__
-
-
-class Feature(object):
-    """Feature."""
-
-    def __init__(self, basedir, filename, encoding="utf-8"):
-        """Parse the feature file.
-
-        :param str basedir: Feature files base directory.
-        :param str filename: Relative path to the feature file.
-        :param str encoding: Feature file encoding (utf-8 by default).
-        """
-        self.scenarios = OrderedDict()
-        self.rel_filename = op.join(op.basename(basedir), filename)
-        self.filename = filename = op.abspath(op.join(basedir, filename))
-        self.line_number = 1
-        self.name = None
-        self.tags = set()
-        self.examples = Examples()
-        scenario = None
-        mode = None
-        prev_mode = None
-        description = []
-        step = None
-        multiline_step = False
-        prev_line = None
-        self.background = None
-
-        with codecs.open(filename, encoding=encoding) as f:
-            content = force_unicode(f.read(), encoding)
-            for line_number, line in enumerate(content.splitlines(), start=1):
-                unindented_line = line.lstrip()
-                line_indent = len(line) - len(unindented_line)
-                if step and (step.indent < line_indent or ((not 
unindented_line) and multiline_step)):
-                    multiline_step = True
-                    # multiline step, so just add line and continue
-                    step.add_line(line)
-                    continue
-                else:
-                    step = None
-                    multiline_step = False
-                stripped_line = line.strip()
-                clean_line = strip_comments(line)
-                if not clean_line and (not prev_mode or prev_mode not in 
types.FEATURE):
-                    continue
-                mode = get_step_type(clean_line) or mode
-
-                allowed_prev_mode = (types.BACKGROUND, types.GIVEN, types.WHEN)
-
-                if not scenario and prev_mode not in allowed_prev_mode and 
mode in types.STEP_TYPES:
-                    raise exceptions.FeatureError(
-                        "Step definition outside of a Scenario or a 
Background", line_number, clean_line, filename
-                    )
-
-                if mode == types.FEATURE:
-                    if prev_mode is None or prev_mode == types.TAG:
-                        _, self.name = parse_line(clean_line)
-                        self.line_number = line_number
-                        self.tags = get_tags(prev_line)
-                    elif prev_mode == types.FEATURE:
-                        description.append(clean_line)
-                    else:
-                        raise exceptions.FeatureError(
-                            "Multiple features are not allowed in a single 
feature file",
-                            line_number,
-                            clean_line,
-                            filename,
-                        )
-
-                prev_mode = mode
-
-                # Remove Feature, Given, When, Then, And
-                keyword, parsed_line = parse_line(clean_line)
-                if mode in [types.SCENARIO, types.SCENARIO_OUTLINE]:
-                    tags = get_tags(prev_line)
-                    self.scenarios[parsed_line] = scenario = Scenario(self, 
parsed_line, line_number, tags=tags)
-                elif mode == types.BACKGROUND:
-                    self.background = Background(feature=self, 
line_number=line_number)
-                elif mode == types.EXAMPLES:
-                    mode = types.EXAMPLES_HEADERS
-                    (scenario or self).examples.line_number = line_number
-                elif mode == types.EXAMPLES_VERTICAL:
-                    mode = types.EXAMPLE_LINE_VERTICAL
-                    (scenario or self).examples.line_number = line_number
-                elif mode == types.EXAMPLES_HEADERS:
-                    (scenario or self).examples.set_param_names([l for l in 
split_line(parsed_line) if l])
-                    mode = types.EXAMPLE_LINE
-                elif mode == types.EXAMPLE_LINE:
-                    (scenario or self).examples.add_example([l for l in 
split_line(stripped_line)])
-                elif mode == types.EXAMPLE_LINE_VERTICAL:
-                    param_line_parts = [l for l in split_line(stripped_line)]
-                    try:
-                        (scenario or 
self).examples.add_example_row(param_line_parts[0], param_line_parts[1:])
-                    except exceptions.ExamplesNotValidError as exc:
-                        if scenario:
-                            raise exceptions.FeatureError(
-                                """Scenario has not valid examples. 
{0}""".format(exc.args[0]),
-                                line_number,
-                                clean_line,
-                                filename,
-                            )
-                        else:
-                            raise exceptions.FeatureError(
-                                """Feature has not valid examples. 
{0}""".format(exc.args[0]),
-                                line_number,
-                                clean_line,
-                                filename,
-                            )
-                elif mode and mode not in (types.FEATURE, types.TAG):
-                    step = Step(
-                        name=parsed_line, type=mode, indent=line_indent, 
line_number=line_number, keyword=keyword
-                    )
-                    if self.background and not scenario:
-                        target = self.background
-                    else:
-                        target = scenario
-                    target.add_step(step)
-                prev_line = clean_line
-
-        self.description = u"\n".join(description).strip()
-
-    @classmethod
-    def get_feature(cls, base_path, filename, encoding="utf-8"):
-        """Get a feature by the filename.
-
-        :param str base_path: Base feature directory.
-        :param str filename: Filename of the feature file.
-        :param str encoding: Feature file encoding.
-
-        :return: `Feature` instance from the parsed feature cache.
-
-        :note: The features are parsed on the execution of the test and
-               stored in the global variable cache to improve the performance
-               when multiple scenarios are referencing the same file.
-        """
-        full_name = op.abspath(op.join(base_path, filename))
-        feature = features.get(full_name)
-        if not feature:
-            feature = Feature(base_path, filename, encoding=encoding)
-            features[full_name] = feature
-        return feature
-
-
-class Scenario(object):
-
-    """Scenario."""
-
-    def __init__(self, feature, name, line_number, example_converters=None, 
tags=None):
-        """Scenario constructor.
-
-        :param pytest_bdd.feature.Feature feature: Feature.
-        :param str name: Scenario name.
-        :param int line_number: Scenario line number.
-        :param dict example_converters: Example table parameter converters.
-        :param set tags: Set of tags.
-        """
-        self.feature = feature
-        self.name = name
-        self._steps = []
-        self.examples = Examples()
-        self.line_number = line_number
-        self.example_converters = example_converters
-        self.tags = tags or set()
-        self.failed = False
-        self.test_function = None
-
-    def add_step(self, step):
-        """Add step to the scenario.
-
-        :param pytest_bdd.feature.Step step: Step.
-        """
-        step.scenario = self
-        self._steps.append(step)
-
-    @property
-    def steps(self):
-        """Get scenario steps including background steps.
-
-        :return: List of steps.
-        """
-        result = []
-        if self.feature.background:
-            result.extend(self.feature.background.steps)
-        result.extend(self._steps)
-        return result
-
-    @property
-    def params(self):
-        """Get parameter names.
-
-        :return: Parameter names.
-        :rtype: frozenset
-        """
-        return frozenset(sum((list(step.params) for step in self.steps), []))
-
-    def get_example_params(self):
-        """Get example parameter names."""
-        return set(self.examples.example_params + 
self.feature.examples.example_params)
-
-    def get_params(self, builtin=False):
-        """Get converted example params."""
-        for examples in [self.feature.examples, self.examples]:
-            yield examples.get_params(self.example_converters, builtin=builtin)
-
-    def validate(self):
-        """Validate the scenario.
-
-        :raises ScenarioValidationError: when scenario is not valid
-        """
-        params = self.params
-        example_params = self.get_example_params()
-        if params and example_params and params != example_params:
-            raise exceptions.ScenarioExamplesNotValidError(
-                """Scenario "{0}" in the feature "{1}" has not valid examples. 
"""
-                """Set of step parameters {2} should match set of example 
values {3}.""".format(
-                    self.name, self.feature.filename, sorted(params), 
sorted(example_params)
-                )
-            )
-
-
-@six.python_2_unicode_compatible
-class Step(object):
-
-    """Step."""
-
-    def __init__(self, name, type, indent, line_number, keyword):
-        """Step constructor.
-
-        :param str name: step name.
-        :param str type: step type.
-        :param int indent: step text indent.
-        :param int line_number: line number.
-        :param str keyword: step keyword.
-        """
-        self.name = name
-        self.keyword = keyword
-        self.lines = []
-        self.indent = indent
-        self.type = type
-        self.line_number = line_number
-        self.failed = False
-        self.start = 0
-        self.stop = 0
-        self.scenario = None
-        self.background = None
-
-    def add_line(self, line):
-        """Add line to the multiple step.
-
-        :param str line: Line of text - the continuation of the step name.
-        """
-        self.lines.append(line)
-
-    @property
-    def name(self):
-        """Get step name."""
-        multilines_content = textwrap.dedent("\n".join(self.lines)) if 
self.lines else ""
-
-        # Remove the multiline quotes, if present.
-        multilines_content = re.sub(
-            pattern=r'^"""\n(?P<content>.*)\n"""$',
-            repl=r"\g<content>",
-            string=multilines_content,
-            flags=re.DOTALL,  # Needed to make the "." match also new lines
-        )
-
-        lines = [self._name] + [multilines_content]
-        return "\n".join(lines).strip()
-
-    @name.setter
-    def name(self, value):
-        """Set step name."""
-        self._name = value
-
-    def __str__(self):
-        """Full step name including the type."""
-        return '{type} "{name}"'.format(type=self.type.capitalize(), 
name=self.name)
-
-    @property
-    def params(self):
-        """Get step params."""
-        return tuple(frozenset(STEP_PARAM_RE.findall(self.name)))
-
-
-class Background(object):
-
-    """Background."""
-
-    def __init__(self, feature, line_number):
-        """Background constructor.
-
-        :param pytest_bdd.feature.Feature feature: Feature.
-        :param int line_number: Line number.
-        """
-        self.feature = feature
-        self.line_number = line_number
-        self.steps = []
-
-    def add_step(self, step):
-        """Add step to the background."""
-        step.background = self
-        self.steps.append(step)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/pytest-bdd-4.0.1/pytest_bdd/gherkin_terminal_reporter.py 
new/pytest-bdd-4.0.2/pytest_bdd/gherkin_terminal_reporter.py
--- old/pytest-bdd-4.0.1/pytest_bdd/gherkin_terminal_reporter.py        
2020-09-08 12:03:15.000000000 +0200
+++ new/pytest-bdd-4.0.2/pytest_bdd/gherkin_terminal_reporter.py        
2020-12-07 13:38:07.000000000 +0100
@@ -6,7 +6,7 @@
 
 from _pytest.terminal import TerminalReporter
 
-from .feature import STEP_PARAM_RE
+from .parser import STEP_PARAM_RE
 
 
 def add_options(parser):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-bdd-4.0.1/pytest_bdd/parser.py 
new/pytest-bdd-4.0.2/pytest_bdd/parser.py
--- old/pytest-bdd-4.0.1/pytest_bdd/parser.py   1970-01-01 01:00:00.000000000 
+0100
+++ new/pytest-bdd-4.0.2/pytest_bdd/parser.py   2020-12-07 13:38:07.000000000 
+0100
@@ -0,0 +1,466 @@
+import io
+import os.path
+import re
+import textwrap
+from collections import OrderedDict
+
+import six
+
+from . import types, exceptions
+
+SPLIT_LINE_RE = re.compile(r"(?<!\\)\|")
+COMMENT_RE = re.compile(r"(^|(?<=\s))#")
+STEP_PREFIXES = [
+    ("Feature: ", types.FEATURE),
+    ("Scenario Outline: ", types.SCENARIO_OUTLINE),
+    ("Examples: Vertical", types.EXAMPLES_VERTICAL),
+    ("Examples:", types.EXAMPLES),
+    ("Scenario: ", types.SCENARIO),
+    ("Background:", types.BACKGROUND),
+    ("Given ", types.GIVEN),
+    ("When ", types.WHEN),
+    ("Then ", types.THEN),
+    ("@", types.TAG),
+    # Continuation of the previously mentioned step type
+    ("And ", None),
+    ("But ", None),
+]
+
+
+def split_line(line):
+    """Split the given Examples line.
+
+    :param str|unicode line: Feature file Examples line.
+
+    :return: List of strings.
+    """
+    return [cell.replace("\\|", "|").strip() for cell in 
SPLIT_LINE_RE.split(line)[1:-1]]
+
+
+def parse_line(line):
+    """Parse step line to get the step prefix (Scenario, Given, When, Then or 
And) and the actual step name.
+
+    :param line: Line of the Feature file.
+
+    :return: `tuple` in form ("<prefix>", "<Line without the prefix>").
+    """
+    for prefix, _ in STEP_PREFIXES:
+        if line.startswith(prefix):
+            return prefix.strip(), line[len(prefix) :].strip()
+    return "", line
+
+
+def strip_comments(line):
+    """Remove comments.
+
+    :param str line: Line of the Feature file.
+
+    :return: Stripped line.
+    """
+    res = COMMENT_RE.search(line)
+    if res:
+        line = line[: res.start()]
+    return line.strip()
+
+
+def get_step_type(line):
+    """Detect step type by the beginning of the line.
+
+    :param str line: Line of the Feature file.
+
+    :return: SCENARIO, GIVEN, WHEN, THEN, or `None` if can't be detected.
+    """
+    for prefix, _type in STEP_PREFIXES:
+        if line.startswith(prefix):
+            return _type
+
+
+def parse_feature(basedir, filename, encoding="utf-8"):
+    """Parse the feature file.
+
+    :param str basedir: Feature files base directory.
+    :param str filename: Relative path to the feature file.
+    :param str encoding: Feature file encoding (utf-8 by default).
+    """
+    abs_filename = os.path.abspath(os.path.join(basedir, filename))
+    rel_filename = os.path.join(os.path.basename(basedir), filename)
+    feature = Feature(
+        scenarios=OrderedDict(),
+        filename=abs_filename,
+        rel_filename=rel_filename,
+        line_number=1,
+        name=None,
+        tags=set(),
+        examples=Examples(),
+        background=None,
+        description="",
+    )
+    scenario = None
+    mode = None
+    prev_mode = None
+    description = []
+    step = None
+    multiline_step = False
+    prev_line = None
+
+    with io.open(abs_filename, "rt", encoding=encoding) as f:
+        content = f.read()
+
+    for line_number, line in enumerate(content.splitlines(), start=1):
+        unindented_line = line.lstrip()
+        line_indent = len(line) - len(unindented_line)
+        if step and (step.indent < line_indent or ((not unindented_line) and 
multiline_step)):
+            multiline_step = True
+            # multiline step, so just add line and continue
+            step.add_line(line)
+            continue
+        else:
+            step = None
+            multiline_step = False
+        stripped_line = line.strip()
+        clean_line = strip_comments(line)
+        if not clean_line and (not prev_mode or prev_mode not in 
types.FEATURE):
+            continue
+        mode = get_step_type(clean_line) or mode
+
+        allowed_prev_mode = (types.BACKGROUND, types.GIVEN, types.WHEN)
+
+        if not scenario and prev_mode not in allowed_prev_mode and mode in 
types.STEP_TYPES:
+            raise exceptions.FeatureError(
+                "Step definition outside of a Scenario or a Background", 
line_number, clean_line, filename
+            )
+
+        if mode == types.FEATURE:
+            if prev_mode is None or prev_mode == types.TAG:
+                _, feature.name = parse_line(clean_line)
+                feature.line_number = line_number
+                feature.tags = get_tags(prev_line)
+            elif prev_mode == types.FEATURE:
+                description.append(clean_line)
+            else:
+                raise exceptions.FeatureError(
+                    "Multiple features are not allowed in a single feature 
file",
+                    line_number,
+                    clean_line,
+                    filename,
+                )
+
+        prev_mode = mode
+
+        # Remove Feature, Given, When, Then, And
+        keyword, parsed_line = parse_line(clean_line)
+        if mode in [types.SCENARIO, types.SCENARIO_OUTLINE]:
+            tags = get_tags(prev_line)
+            feature.scenarios[parsed_line] = scenario = Scenario(feature, 
parsed_line, line_number, tags=tags)
+        elif mode == types.BACKGROUND:
+            feature.background = Background(feature=feature, 
line_number=line_number)
+        elif mode == types.EXAMPLES:
+            mode = types.EXAMPLES_HEADERS
+            (scenario or feature).examples.line_number = line_number
+        elif mode == types.EXAMPLES_VERTICAL:
+            mode = types.EXAMPLE_LINE_VERTICAL
+            (scenario or feature).examples.line_number = line_number
+        elif mode == types.EXAMPLES_HEADERS:
+            (scenario or feature).examples.set_param_names([l for l in 
split_line(parsed_line) if l])
+            mode = types.EXAMPLE_LINE
+        elif mode == types.EXAMPLE_LINE:
+            (scenario or feature).examples.add_example([l for l in 
split_line(stripped_line)])
+        elif mode == types.EXAMPLE_LINE_VERTICAL:
+            param_line_parts = [l for l in split_line(stripped_line)]
+            try:
+                (scenario or 
feature).examples.add_example_row(param_line_parts[0], param_line_parts[1:])
+            except exceptions.ExamplesNotValidError as exc:
+                if scenario:
+                    raise exceptions.FeatureError(
+                        """Scenario has not valid examples. 
{0}""".format(exc.args[0]),
+                        line_number,
+                        clean_line,
+                        filename,
+                    )
+                else:
+                    raise exceptions.FeatureError(
+                        """Feature has not valid examples. 
{0}""".format(exc.args[0]),
+                        line_number,
+                        clean_line,
+                        filename,
+                    )
+        elif mode and mode not in (types.FEATURE, types.TAG):
+            step = Step(name=parsed_line, type=mode, indent=line_indent, 
line_number=line_number, keyword=keyword)
+            if feature.background and not scenario:
+                target = feature.background
+            else:
+                target = scenario
+            target.add_step(step)
+        prev_line = clean_line
+
+    feature.description = u"\n".join(description).strip()
+    return feature
+
+
+class Feature(object):
+    """Feature."""
+
+    def __init__(self, scenarios, filename, rel_filename, name, tags, 
examples, background, line_number, description):
+        self.scenarios = scenarios
+        self.rel_filename = rel_filename
+        self.filename = filename
+        self.name = name
+        self.tags = tags
+        self.examples = examples
+        self.name = name
+        self.line_number = line_number
+        self.tags = tags
+        self.scenarios = scenarios
+        self.description = description
+        self.background = background
+
+
+class Scenario(object):
+
+    """Scenario."""
+
+    def __init__(self, feature, name, line_number, example_converters=None, 
tags=None):
+        """Scenario constructor.
+
+        :param pytest_bdd.parser.Feature feature: Feature.
+        :param str name: Scenario name.
+        :param int line_number: Scenario line number.
+        :param dict example_converters: Example table parameter converters.
+        :param set tags: Set of tags.
+        """
+        self.feature = feature
+        self.name = name
+        self._steps = []
+        self.examples = Examples()
+        self.line_number = line_number
+        self.example_converters = example_converters
+        self.tags = tags or set()
+        self.failed = False
+        self.test_function = None
+
+    def add_step(self, step):
+        """Add step to the scenario.
+
+        :param pytest_bdd.parser.Step step: Step.
+        """
+        step.scenario = self
+        self._steps.append(step)
+
+    @property
+    def steps(self):
+        """Get scenario steps including background steps.
+
+        :return: List of steps.
+        """
+        result = []
+        if self.feature.background:
+            result.extend(self.feature.background.steps)
+        result.extend(self._steps)
+        return result
+
+    @property
+    def params(self):
+        """Get parameter names.
+
+        :return: Parameter names.
+        :rtype: frozenset
+        """
+        return frozenset(sum((list(step.params) for step in self.steps), []))
+
+    def get_example_params(self):
+        """Get example parameter names."""
+        return set(self.examples.example_params + 
self.feature.examples.example_params)
+
+    def get_params(self, builtin=False):
+        """Get converted example params."""
+        for examples in [self.feature.examples, self.examples]:
+            yield examples.get_params(self.example_converters, builtin=builtin)
+
+    def validate(self):
+        """Validate the scenario.
+
+        :raises ScenarioValidationError: when scenario is not valid
+        """
+        params = self.params
+        example_params = self.get_example_params()
+        if params and example_params and params != example_params:
+            raise exceptions.ScenarioExamplesNotValidError(
+                """Scenario "{0}" in the feature "{1}" has not valid examples. 
"""
+                """Set of step parameters {2} should match set of example 
values {3}.""".format(
+                    self.name, self.feature.filename, sorted(params), 
sorted(example_params)
+                )
+            )
+
+
+@six.python_2_unicode_compatible
+class Step(object):
+
+    """Step."""
+
+    def __init__(self, name, type, indent, line_number, keyword):
+        """Step constructor.
+
+        :param str name: step name.
+        :param str type: step type.
+        :param int indent: step text indent.
+        :param int line_number: line number.
+        :param str keyword: step keyword.
+        """
+        self.name = name
+        self.keyword = keyword
+        self.lines = []
+        self.indent = indent
+        self.type = type
+        self.line_number = line_number
+        self.failed = False
+        self.start = 0
+        self.stop = 0
+        self.scenario = None
+        self.background = None
+
+    def add_line(self, line):
+        """Add line to the multiple step.
+
+        :param str line: Line of text - the continuation of the step name.
+        """
+        self.lines.append(line)
+
+    @property
+    def name(self):
+        """Get step name."""
+        multilines_content = textwrap.dedent("\n".join(self.lines)) if 
self.lines else ""
+
+        # Remove the multiline quotes, if present.
+        multilines_content = re.sub(
+            pattern=r'^"""\n(?P<content>.*)\n"""$',
+            repl=r"\g<content>",
+            string=multilines_content,
+            flags=re.DOTALL,  # Needed to make the "." match also new lines
+        )
+
+        lines = [self._name] + [multilines_content]
+        return "\n".join(lines).strip()
+
+    @name.setter
+    def name(self, value):
+        """Set step name."""
+        self._name = value
+
+    def __str__(self):
+        """Full step name including the type."""
+        return '{type} "{name}"'.format(type=self.type.capitalize(), 
name=self.name)
+
+    @property
+    def params(self):
+        """Get step params."""
+        return tuple(frozenset(STEP_PARAM_RE.findall(self.name)))
+
+
+class Background(object):
+
+    """Background."""
+
+    def __init__(self, feature, line_number):
+        """Background constructor.
+
+        :param pytest_bdd.parser.Feature feature: Feature.
+        :param int line_number: Line number.
+        """
+        self.feature = feature
+        self.line_number = line_number
+        self.steps = []
+
+    def add_step(self, step):
+        """Add step to the background."""
+        step.background = self
+        self.steps.append(step)
+
+
+class Examples(object):
+
+    """Example table."""
+
+    def __init__(self):
+        """Initialize examples instance."""
+        self.example_params = []
+        self.examples = []
+        self.vertical_examples = []
+        self.line_number = None
+        self.name = None
+
+    def set_param_names(self, keys):
+        """Set parameter names.
+
+        :param names: `list` of `string` parameter names.
+        """
+        self.example_params = [str(key) for key in keys]
+
+    def add_example(self, values):
+        """Add example.
+
+        :param values: `list` of `string` parameter values.
+        """
+        self.examples.append(values)
+
+    def add_example_row(self, param, values):
+        """Add example row.
+
+        :param param: `str` parameter name
+        :param values: `list` of `string` parameter values
+        """
+        if param in self.example_params:
+            raise exceptions.ExamplesNotValidError(
+                """Example rows should contain unique parameters. "{0}" 
appeared more than once""".format(param)
+            )
+        self.example_params.append(param)
+        self.vertical_examples.append(values)
+
+    def get_params(self, converters, builtin=False):
+        """Get scenario pytest parametrization table.
+
+        :param converters: `dict` of converter functions to convert parameter 
values
+        """
+        param_count = len(self.example_params)
+        if self.vertical_examples and not self.examples:
+            for value_index in range(len(self.vertical_examples[0])):
+                example = []
+                for param_index in range(param_count):
+                    
example.append(self.vertical_examples[param_index][value_index])
+                self.examples.append(example)
+
+        if self.examples:
+            params = []
+            for example in self.examples:
+                example = list(example)
+                for index, param in enumerate(self.example_params):
+                    raw_value = example[index]
+                    if converters and param in converters:
+                        value = converters[param](raw_value)
+                        if not builtin or value.__class__.__module__ in 
{"__builtin__", "builtins"}:
+                            example[index] = value
+                params.append(example)
+            return [self.example_params, params]
+        else:
+            return []
+
+    def __bool__(self):
+        """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.
+
+    :param str line: Feature file text line.
+
+    :return: List of tags.
+    """
+    if not line or not line.strip().startswith("@"):
+        return set()
+    return set((tag.lstrip("@") for tag in line.strip().split(" @") if 
len(tag) > 1))
+
+
+STEP_PARAM_RE = re.compile(r"\<(.+?)\>")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-bdd-4.0.1/pytest_bdd/reporting.py 
new/pytest-bdd-4.0.2/pytest_bdd/reporting.py
--- old/pytest-bdd-4.0.1/pytest_bdd/reporting.py        2020-09-08 
12:03:15.000000000 +0200
+++ new/pytest-bdd-4.0.2/pytest_bdd/reporting.py        2020-12-07 
13:38:07.000000000 +0100
@@ -19,7 +19,7 @@
     def __init__(self, step):
         """Step report constructor.
 
-        :param pytest_bdd.feature.Step step: Step.
+        :param pytest_bdd.parser.Step step: Step.
         """
         self.step = step
         self.started = time.time()
@@ -66,7 +66,7 @@
     def __init__(self, scenario, node):
         """Scenario report constructor.
 
-        :param pytest_bdd.feature.Scenario scenario: Scenario.
+        :param pytest_bdd.parser.Scenario scenario: Scenario.
         :param node: pytest test node object
         """
         self.scenario = scenario
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-bdd-4.0.1/pytest_bdd/scenario.py 
new/pytest-bdd-4.0.2/pytest_bdd/scenario.py
--- old/pytest-bdd-4.0.1/pytest_bdd/scenario.py 2020-09-08 12:03:15.000000000 
+0200
+++ new/pytest-bdd-4.0.2/pytest_bdd/scenario.py 2020-12-07 13:38:07.000000000 
+0100
@@ -11,10 +11,8 @@
 )
 """
 import collections
-import inspect
 import os
 import re
-import sys
 
 import pytest
 
@@ -24,7 +22,7 @@
     from _pytest import python as pytest_fixtures
 
 from . import exceptions
-from .feature import Feature, force_unicode, get_features
+from .feature import force_unicode, 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
 
@@ -213,7 +211,7 @@
     # Get the feature
     if features_base_dir is None:
         features_base_dir = get_features_base_dir(caller_module_path)
-    feature = Feature.get_feature(features_base_dir, feature_name, 
encoding=encoding)
+    feature = get_feature(features_base_dir, feature_name, encoding=encoding)
 
     # Get the scenario
     try:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-bdd-4.0.1/tests/feature/test_outline.py 
new/pytest-bdd-4.0.2/tests/feature/test_outline.py
--- old/pytest-bdd-4.0.1/tests/feature/test_outline.py  2020-09-08 
12:03:15.000000000 +0200
+++ new/pytest-bdd-4.0.2/tests/feature/test_outline.py  2020-12-07 
13:38:07.000000000 +0100
@@ -42,7 +42,7 @@
 
                     Examples:
                     | start | eat | left |
-                    |  12   |  5  |  7   |
+                    |  12   |  5  |  7   | # a comment
                     |  5    |  4  |  1   |
 
             """
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-bdd-4.0.1/tests/feature/test_tags.py 
new/pytest-bdd-4.0.2/tests/feature/test_tags.py
--- old/pytest-bdd-4.0.1/tests/feature/test_tags.py     2020-09-08 
12:03:15.000000000 +0200
+++ new/pytest-bdd-4.0.2/tests/feature/test_tags.py     2020-12-07 
13:38:07.000000000 +0100
@@ -3,7 +3,7 @@
 
 import pytest
 
-from pytest_bdd import feature
+from pytest_bdd.parser import get_tags
 
 
 def test_tags_selector(testdir):
@@ -251,4 +251,4 @@
     ],
 )
 def test_get_tags(line, expected):
-    assert feature.get_tags(line) == expected
+    assert get_tags(line) == expected

Reply via email to