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