Hello, this email is a notification from the Auto Upgrade Helper that the automatic attempt to upgrade the recipe *python3-behave* to *6* has Failed(do_compile). Detailed error information: do_compile failed Next steps: - apply the patch: git am 0001-python3-behave-upgrade-1.2.6-git9520119376046aeff738.patch - check the changes to upstream patches and summarize them in the commit message, - compile an image that contains the package - perform some basic sanity tests - amend the patch and sign it off: git commit -s --reset-author --amend - send it to the appropriate mailing list Alternatively, if you believe the recipe should not be upgraded at this time, you can fill RECIPE_NO_UPDATE_REASON in respective recipe file so that automatic upgrades would no longer be attempted. Please review the attached files for further information and build/update failures. Any problem please file a bug at https://bugzilla.yoctoproject.org/enter_bug.cgi?product=Automated%20Update%20Handler Regards, The Upgrade Helper -- >8 -- From 815d53cd9a095b0b67dc6f98ad9a7fa251edefbf Mon Sep 17 00:00:00 2001 From: Upgrade Helper Date: Tue, 11 Jul 2023 17:12:25 -0500 Subject: [PATCH] python3-behave: upgrade 1.2.6+git9520119376046aeff73804b5f1ea05d87a63f370 -> 6 --- ...ioOutlineBuilder-was-not-copying-des.patch | 22 + .../0002-UPDATE-FIXED-725.patch | 21 + ...ST-to-verify-that-issue-725-is-fixed.patch | 60 + ...ter-counts-computation-when-Rules-ar.patch | 342 + ...print_summary-Simplify-if-Rules-are-.patch | 60 + ...r-Add-basic-support-output-for-Rules.patch | 395 + ...MP-VERSION-1.2.7.dev1-was-1.2.7.dev0.patch | 67 + .../0008-Correct-examples-and-docstring.patch | 41 + ...ure.run_items-processing-with-Rule-s.patch | 58 + ...-sphinx-intl-support-for-READTHEDOCS.patch | 58 + ...ent-package-versions-in-requirements.patch | 81 + .../0012-docs-conf.py-tweaks.patch | 30 + ...lled-after_rule-hook-was-after_after.patch | 30 + ...d-hints-on-Gherkin-v6-grammar-issues.patch | 45 + .../0015-README-ReST-tweaks.patch | 18 + ...016-Example-using-Gherkin-v6-grammar.patch | 228 + .../0017-PREPARE-Python-3.8-support.patch | 39 + ...x-logging.Formatter-validate-problem.patch | 22 + ...arily-move-py38-dev-to-front-build-f.patch | 28 + ...Tweaks-for-faster-builds-temporarily.patch | 35 + ...8-logging.Formatter.validate-problem.patch | 47 + ...on-3.8-asyncio.coroutine-is-deprecat.patch | 42 + .../0023-UPDATE-Add-755-info.patch | 24 + ...ted-to-docstring-example-and-weird-b.patch | 25 + ...pe-sequence-warnings-w-regex-pattern.patch | 29 + ...NUP-Move-deprecated-tag-matcher-clas.patch | 1020 +++ .../python3-behave/0027-Comment-tweaks.patch | 30 + ...-related-to-invalid-escapes-in-regex.patch | 79 + ...ould-not-break-configured-rerun-sett.patch | 73 + ...les-and-failing-scenarios-enable-via.patch | 36 + ..._v6-Tweak-ScenarioOutline-Examples-t.patch | 27 + .../0032-Add-info-on-merged-pull-588.patch | 21 + ...3-Tweak-tests-required-by-pytest-5.0.patch | 97 + ...st-instead-of-nose-to-remove-nose.im.patch | 180 + ...Y-nose-to-avoid-nose.importer-warnin.patch | 1815 ++++ ...0036-FIX-Remove-test-from-pytest-run.patch | 22 + ...on-Add-support-for-Scenario-containe.patch | 652 ++ ...tion-for-Select-by-location-for-Scen.patch | 58 + .../0039-tests-Fix-warnings-for-python3.patch | 50 + ...ag-expressions-1.1.2-to-fix-warnings.patch | 55 + ...ENT-Support-emojis-in-feature-files-.patch | 91 + ...valid-escape-char-in-regex-w-python3.patch | 250 + ...3-Example-related-to-question-in-756.patch | 335 + ...X-python3.8-regressions-on-CI-server.patch | 489 + .../0045-UPDATE-Mark-issue-755-as-fixed.patch | 46 + ...DATE-Cucumber-gherkin-languages.json.patch | 57 + ...ule-keyword-translation-in-portugues.patch | 202 + ...-generate-from-gherkin-languages.jso.patch | 141 + ...ming-to-fixture.behave.no_background.patch | 322 + ...50-EXAMPLE-Cleanup-Gherkin-v6-README.patch | 64 + ...for-feature.background-inheritance-f.patch | 1510 +++ ...-Add-support-for-runtime-constraints.patch | 269 + .../0053-Use-runtime-constraints.patch | 196 + ...0054-CLEANUP-Remove-deprecated-parts.patch | 3937 ++++++++ ...0055-CLEANUP-Remove-deprecated-parts.patch | 736 ++ ...rform-more-Gherkin-v6-checks-and-run.patch | 155 + ...-and-python-module-old-was-broken-no.patch | 72 + .../0058-UTIL-Formatting-tweaks.patch | 22 + ...e-use_fixture_by_tag-didn-t-return-t.patch | 23 + .../0060-Added-issue-unit-test.patch | 62 + ...e-pull-request-767-with-minor-tweaks.patch | 60 + ...sue-766-PrettyFormatter-UnicodeError.patch | 83 + ...enarioOutline.Examples-without-table.patch | 74 + ...enarioOutline.Examples-without-table.patch | 21 + .../0065-Nibble-TravisCI-to-wake-up.patch | 21 + .../0066-Tweak-pytest-version-selection.patch | 37 + ...figuration-to-silence-JUnit-XML-dial.patch | 37 + ...e-test-for-wildcard-pattern-matching.patch | 56 + ...ATE-dependencies-path.py-path-pytest.patch | 141 + ...eprecatedWarning-from-distutils-pack.patch | 25 + ...-Add-ContextMode-enum-related-to-797.patch | 216 + .../0072-Cleanup-comments.patch | 22 + ...phinx-build-problem-async_steps3x.py.patch | 29 + ...-parse_expressions-was-parse_builtin.patch | 185 + ...ssion-add-links-to-parse_type-module.patch | 40 + ...MP-VERSION-1.2.7.dev2-was-1.2.7.dev1.patch | 65 + ...leanups-related-to-question-in-800-P.patch | 223 + ...y-name-uses-regex-pattern-related-to.patch | 82 + ...nce-problem-copy-paste-in-Rule-class.patch | 34 + ...API-description-for-Runner-Operation.patch | 195 + ...0081-FIX-DOCS-Runner-operations-typo.patch | 22 + ...ement-Context.add_cleanup-with-layer.patch | 295 + ...8.0-parse-versions-1.16.0-.-1.17.x-h.patch | 37 + ...Duplicated-steps-AmbiguousStepErrors.patch | 34 + .../0085-Add-renovate.json.patch | 21 + ...086-PRPEPARE-RENOVATE-With-adaptions.patch | 175 + .../0087-Pin-dependencies.patch | 36 + ...te-Extend-pip-requirements-file-list.patch | 31 + ...IN-REQUIREMENTS-Extend-to-all-places.patch | 92 + ..._cleanup-replaces-_tasklet_cleanup-r.patch | 8116 +++++++++++++++++ ...nge-code-blocks-from-bash-to-console.patch | 36 + .../0092-Fix-typo-in-tutorial.patch | 24 + ...nts-Use-PyHamcrest-2.0-for-python2.7.patch | 80 + .../0094-UPDATE-PR-877-was-merged.patch | 21 + .../0095-capitalizing-steps.patch | 28 + ...develop.update-gherkin-that-aborted-.patch | 56 + ...ainst-PowerPC-CPU-support-Travis-867.patch | 22 + ...098-Add-Context.attach-docs-and-test.patch | 132 + .../0099-Add-Contributing-chapter.patch | 125 + ...apt-Tox-target-for-building-the-docs.patch | 34 + ...tion-HTML-formatter-in-documentation.patch | 83 + ...le-highlighting-for-pip-install-docs.patch | 22 + ...ocs-fix-simple-typo-tuorial-tutorial.patch | 52 + ...t-for-python3.9-by-using-active-tags.patch | 227 + .../0105-PREFER-python3-from-now-on.patch | 19 + .../0106-REMOVE-invoke-scripts.patch | 41 + ...X-Deprecated-warnings-for-Python-3.x.patch | 124 + ...lems-in-virtualenvs-w-behave4cmd0-st.patch | 18 + .../0109-FIX-Active-tag-logic.patch | 875 ++ .../0110-FIX-Tests-w-more.features.patch | 56 + ...FIX-Some-regressions-with-Python-3.9.patch | 741 ++ .../0112-docs-Update-new-and-noteworthy.patch | 84 + ...elper-function-to-print-the-current-.patch | 278 + ...kin-languages.json-from-cucumber-rep.patch | 541 ++ ...TE-CHANGES-Related-to-PR-895-and-827.patch | 22 + ...r-python-2.7-builds-mock-4.0-only-fo.patch | 39 + ...-to-use-a-custom-runner-in-the-behav.patch | 126 + ...llow-forcing-color-with-color-always.patch | 59 + ...lor-with-no-value-followed-by-posarg.patch | 43 + .../0120-Add-BEHAVE_COLOR-env-var.patch | 31 + ...121-fix-malformed-table-rows-warning.patch | 33 + ...-955-setup-Remove-attribute-use_2to3.patch | 42 + .../0123-Add-info-for-fixed-issue-955.patch | 21 + .../python/python3-behave_1.2.6.bb | 129 +- 124 files changed, 29808 insertions(+), 2 deletions(-) create mode 100644 meta-python/recipes-devtools/python/python3-behave/0001-FIXES-725-ScenarioOutlineBuilder-was-not-copying-des.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0002-UPDATE-FIXED-725.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0003-ADD-TEST-to-verify-that-issue-725-is-fixed.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0004-FIX-SummaryReporter-counts-computation-when-Rules-ar.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0005-SummaryReporter.print_summary-Simplify-if-Rules-are-.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0006-Formatter-Add-basic-support-output-for-Rules.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0007-BUMP-VERSION-1.2.7.dev1-was-1.2.7.dev0.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0008-Correct-examples-and-docstring.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0009-FIX-feature.run_items-processing-with-Rule-s.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0010-docs-Disable-sphinx-intl-support-for-READTHEDOCS.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0011-Cleanup-Dependent-package-versions-in-requirements.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0012-docs-conf.py-tweaks.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0013-FIX-Misspelled-after_rule-hook-was-after_after.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0014-Add-hints-on-Gherkin-v6-grammar-issues.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0015-README-ReST-tweaks.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0016-Example-using-Gherkin-v6-grammar.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0017-PREPARE-Python-3.8-support.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0018-py3.8-Try-to-fix-logging.Formatter-validate-problem.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0019-travis.ci-Temporarily-move-py38-dev-to-front-build-f.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0020-travis.ci-Tweaks-for-faster-builds-temporarily.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0021-FIX-py3.8-logging.Formatter.validate-problem.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0022-PREPARE-FOR-Python-3.8-asyncio.coroutine-is-deprecat.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0023-UPDATE-Add-755-info.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0024-FIX-WARNING-Related-to-docstring-example-and-weird-b.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0025-FIX-invalid-escape-sequence-warnings-w-regex-pattern.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0026-DEPRECATING-CLEANUP-Move-deprecated-tag-matcher-clas.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0027-Comment-tweaks.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0028-FIX-warnings-related-to-invalid-escapes-in-regex.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0029-Steps-catalog-should-not-break-configured-rerun-sett.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0030-Add-feature-w-rules-and-failing-scenarios-enable-via.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0031-examples-gherkin_v6-Tweak-ScenarioOutline-Examples-t.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0032-Add-info-on-merged-pull-588.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0033-Tweak-tests-required-by-pytest-5.0.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0034-CLEANUP-Use-pytest-instead-of-nose-to-remove-nose.im.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0035-REMOVE-DEPENDENCY-nose-to-avoid-nose.importer-warnin.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0036-FIX-Remove-test-from-pytest-run.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0037-Select-by-location-Add-support-for-Scenario-containe.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0038-docs-Add-description-for-Select-by-location-for-Scen.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0039-tests-Fix-warnings-for-python3.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0040-Use-cucumber-tag-expressions-1.1.2-to-fix-warnings.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0041-MENTION-ENHANCEMENT-Support-emojis-in-feature-files-.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0042-FIX-Invalid-escape-char-in-regex-w-python3.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0043-Example-related-to-question-in-756.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0044-FIX-python3.8-regressions-on-CI-server.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0045-UPDATE-Mark-issue-755-as-fixed.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0046-UPDATE-Cucumber-gherkin-languages.json.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0047-gherkin-Adding-Rule-keyword-translation-in-portugues.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0048-Tweaks-to-update-generate-from-gherkin-languages.jso.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0049-EXAMPLE-Tweak-naming-to-fixture.behave.no_background.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0050-EXAMPLE-Cleanup-Gherkin-v6-README.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0051-Improve-support-for-feature.background-inheritance-f.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0052-Add-support-for-runtime-constraints.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0053-Use-runtime-constraints.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0054-CLEANUP-Remove-deprecated-parts.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0055-CLEANUP-Remove-deprecated-parts.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0056-more.features-Perform-more-Gherkin-v6-checks-and-run.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0057-UTIL-Correct-URL-and-python-module-old-was-broken-no.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0058-UTIL-Formatting-tweaks.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0059-Fixed-a-bug-where-use_fixture_by_tag-didn-t-return-t.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0060-Added-issue-unit-test.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0061-Merge-pull-request-767-with-minor-tweaks.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0062-CHECK-Issue-766-PrettyFormatter-UnicodeError.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0063-FIX-issue-772-ScenarioOutline.Examples-without-table.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0064-FIX-issue-772-ScenarioOutline.Examples-without-table.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0065-Nibble-TravisCI-to-wake-up.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0066-Tweak-pytest-version-selection.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0067-Tweak-pytest-configuration-to-silence-JUnit-XML-dial.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0068-Add-basic-feature-test-for-wildcard-pattern-matching.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0069-UPDATE-dependencies-path.py-path-pytest.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0070-pytest-Disable-DeprecatedWarning-from-distutils-pack.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0071-CLEANUP-Add-ContextMode-enum-related-to-797.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0072-Cleanup-comments.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0073-FIX-sphinx-build-problem-async_steps3x.py.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0074-docs-Rename-page-parse_expressions-was-parse_builtin.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0075-docs-parse_expression-add-links-to-parse_type-module.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0076-BUMP-VERSION-1.2.7.dev2-was-1.2.7.dev1.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0077-Gherkin-parser-Cleanups-related-to-question-in-800-P.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0078-Clarify-select-by-name-uses-regex-pattern-related-to.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0079-FIX-Cross-reference-problem-copy-paste-in-Rule-class.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0080-DOCS-Update-API-description-for-Runner-Operation.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0081-FIX-DOCS-Runner-operations-typo.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0082-issue-740-Enhancement-Context.add_cleanup-with-layer.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0083-UPDATE-parse-1.18.0-parse-versions-1.16.0-.-1.17.x-h.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0084-RELATED-TO-Duplicated-steps-AmbiguousStepErrors.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0085-Add-renovate.json.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0086-PRPEPARE-RENOVATE-With-adaptions.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0087-Pin-dependencies.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0088-renovate-Extend-pip-requirements-file-list.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0089-PIN-REQUIREMENTS-Extend-to-all-places.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0090-tasks-Add-invoke_cleanup-replaces-_tasklet_cleanup-r.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0091-Docs-change-code-blocks-from-bash-to-console.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0092-Fix-typo-in-tutorial.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0093-py.requirements-Use-PyHamcrest-2.0-for-python2.7.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0094-UPDATE-PR-877-was-merged.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0095-capitalizing-steps.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0096-FIX-invoke-task-develop.update-gherkin-that-aborted-.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0097-Test-against-PowerPC-CPU-support-Travis-867.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0098-Add-Context.attach-docs-and-test.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0099-Add-Contributing-chapter.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0100-Adapt-Tox-target-for-building-the-docs.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0101-Mention-HTML-formatter-in-documentation.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0102-Use-console-highlighting-for-pip-install-docs.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0103-docs-fix-simple-typo-tuorial-tutorial.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0104-Add-support-for-python3.9-by-using-active-tags.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0105-PREFER-python3-from-now-on.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0106-REMOVE-invoke-scripts.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0107-FIX-Deprecated-warnings-for-Python-3.x.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0108-FIX-Python2-problems-in-virtualenvs-w-behave4cmd0-st.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0109-FIX-Active-tag-logic.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0110-FIX-Tests-w-more.features.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0111-FIX-Some-regressions-with-Python-3.9.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0112-docs-Update-new-and-noteworthy.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0113-Add-diagnostic-helper-function-to-print-the-current-.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0114-UPDATE-i18n-gherkin-languages.json-from-cucumber-rep.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0115-UPDATE-CHANGES-Related-to-PR-895-and-827.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0116-FIX-CI-TRAVIS-For-python-2.7-builds-mock-4.0-only-fo.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0117-Adds-the-ability-to-use-a-custom-runner-in-the-behav.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0118-Allow-forcing-color-with-color-always.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0119-Allow-color-with-no-value-followed-by-posarg.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0120-Add-BEHAVE_COLOR-env-var.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0121-fix-malformed-table-rows-warning.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0122-FIX-955-setup-Remove-attribute-use_2to3.patch create mode 100644 meta-python/recipes-devtools/python/python3-behave/0123-Add-info-for-fixed-issue-955.patch diff --git a/meta-python/recipes-devtools/python/python3-behave/0001-FIXES-725-ScenarioOutlineBuilder-was-not-copying-des.patch b/meta-python/recipes-devtools/python/python3-behave/0001-FIXES-725-ScenarioOutlineBuilder-was-not-copying-des.patch new file mode 100644 index 000000000..5c49cc00c --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0001-FIXES-725-ScenarioOutlineBuilder-was-not-copying-des.patch @@ -0,0 +1,22 @@ +From b941f353c129f73934853082f3f3a01cebe6f944 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Mon, 11 Mar 2019 22:37:04 +0100 +Subject: [PATCH] FIXES #725: ScenarioOutlineBuilder was not copying + description to created Scenario. + +--- + behave/model.py | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/behave/model.py b/behave/model.py +index 4ad4b9d..9dd68fd 100644 +--- a/behave/model.py ++++ b/behave/model.py +@@ -1196,6 +1196,7 @@ class ScenarioOutlineBuilder(object): + scenario.feature = scenario_outline.feature + scenario.parent = scenario_outline + scenario.background = scenario_outline.background ++ scenario.description = scenario_outline.description + scenario._row = row # pylint: disable=protected-access + scenarios.append(scenario) + return scenarios diff --git a/meta-python/recipes-devtools/python/python3-behave/0002-UPDATE-FIXED-725.patch b/meta-python/recipes-devtools/python/python3-behave/0002-UPDATE-FIXED-725.patch new file mode 100644 index 000000000..2be6811fb --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0002-UPDATE-FIXED-725.patch @@ -0,0 +1,21 @@ +From 0e26bbae1f9f8d60c3ab9470b3685af1dde5b6d8 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Mon, 11 Mar 2019 22:40:13 +0100 +Subject: [PATCH] UPDATE: FIXED #725 + +--- + CHANGES.rst | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/CHANGES.rst b/CHANGES.rst +index c11840f..d6e96af 100644 +--- a/CHANGES.rst ++++ b/CHANGES.rst +@@ -32,6 +32,7 @@ ENHANCEMENTS: + + FIXED: + ++* issue #725: Scenario Outline description lines seem to be ignored (submitted by: nizwiz) + * issue #713: Background section doesn't support description (provided by: dgou) + * pull #657: Allow async steps with timeouts to fail when they raise exceptions (provided by: ALSchwalm) + * issue #631: ScenarioOutline variables not possible in table headings (provided by: mschnelle, pull #642) diff --git a/meta-python/recipes-devtools/python/python3-behave/0003-ADD-TEST-to-verify-that-issue-725-is-fixed.patch b/meta-python/recipes-devtools/python/python3-behave/0003-ADD-TEST-to-verify-that-issue-725-is-fixed.patch new file mode 100644 index 000000000..3e4f6fdf2 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0003-ADD-TEST-to-verify-that-issue-725-is-fixed.patch @@ -0,0 +1,60 @@ +From 66324f8dc74715a5018d1eced225557c40bd7acd Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Mon, 11 Mar 2019 23:08:00 +0100 +Subject: [PATCH] ADD TEST to verify that issue #725 is fixed. + +--- + tests/issues/test_issue0725.py | 44 ++++++++++++++++++++++++++++++++++ + 1 file changed, 44 insertions(+) + create mode 100644 tests/issues/test_issue0725.py + +diff --git a/tests/issues/test_issue0725.py b/tests/issues/test_issue0725.py +new file mode 100644 +index 0000000..7479f59 +--- /dev/null ++++ b/tests/issues/test_issue0725.py +@@ -0,0 +1,44 @@ ++# -*- coding: UTF-8 -*- ++""" ++https://github.com/behave/behave/issues/725 ++ ++ANALYSIS: ++---------- ++ ++ScenarioOutlineBuilder did not copy ScenarioOutline.description ++to the Scenarios that were created from the ScenarioOutline. ++""" ++ ++from __future__ import absolute_import, print_function ++from behave.parser import parse_feature ++ ++ ++def test_issue(): ++ """Verifies that issue #725 is fixed.""" ++ text = u''' ++Feature: ScenarioOutline with description ++ ++ Scenario Outline: SO_1 ++ Description line 1 for ScenarioOutline. ++ Description line 2 for ScenarioOutline. ++ ++ Given a step with "" ++ ++ Examples: ++ | name | ++ | Alice | ++ | Bob | ++'''.lstrip() ++ feature = parse_feature(text) ++ assert len(feature.scenarios) == 1 ++ scenario_outline_1 = feature.scenarios[0] ++ assert len(scenario_outline_1.scenarios) == 2 ++ # -- HINT: Last line triggers building of the Scenarios from ScenarioOutline. ++ ++ expected_description = [ ++ "Description line 1 for ScenarioOutline.", ++ "Description line 2 for ScenarioOutline.", ++ ] ++ assert scenario_outline_1.description == expected_description ++ assert scenario_outline_1.scenarios[0].description == expected_description ++ assert scenario_outline_1.scenarios[1].description == expected_description diff --git a/meta-python/recipes-devtools/python/python3-behave/0004-FIX-SummaryReporter-counts-computation-when-Rules-ar.patch b/meta-python/recipes-devtools/python/python3-behave/0004-FIX-SummaryReporter-counts-computation-when-Rules-ar.patch new file mode 100644 index 000000000..ec55efb50 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0004-FIX-SummaryReporter-counts-computation-when-Rules-ar.patch @@ -0,0 +1,342 @@ +From 74d539b86ca52e83255183d96b93ff7492751b6f Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Wed, 13 Mar 2019 08:29:02 +0100 +Subject: [PATCH] FIX: SummaryReporter counts computation when Rules are used. + +--- + behave/model.py | 39 +++--- + behave/reporter/summary.py | 114 ++++++++++++++---- + .../unit/{reporters => reporter}/__init__.py | 0 + .../{reporters => reporter}/test_summary.py | 4 + + 4 files changed, 116 insertions(+), 41 deletions(-) + rename tests/unit/{reporters => reporter}/__init__.py (100%) + rename tests/unit/{reporters => reporter}/test_summary.py (99%) + +diff --git a/behave/model.py b/behave/model.py +index 9dd68fd..6238313 100644 +--- a/behave/model.py ++++ b/behave/model.py +@@ -144,18 +144,18 @@ class ScenarioContainer(TagAndStatusStatement, Replayable): + self.hook_failed = False + self.run_starttime = 0 + self.run_endtime = 0 +- for scenario in self.scenarios: +- scenario.reset() ++ for run_item in self.run_items: ++ run_item.reset() + + def __iter__(self): +- return iter(self.scenarios) ++ return iter(self.run_items) + + def add_scenario(self, scenario): + feature = getattr(self, "feature", None) + if isinstance(self, Feature): + feature = self + +- scenario.parent = self # XXX-NEW ++ scenario.parent = self + scenario.feature = feature + scenario.background = self.background + self.scenarios.append(scenario) +@@ -174,17 +174,17 @@ class ScenarioContainer(TagAndStatusStatement, Replayable): + + skipped = True + passed_count = 0 +- for scenario in self.scenarios: +- scenario_status = scenario.status +- if scenario_status == Status.failed: ++ for run_item in self.run_items: ++ run_item_status = run_item.status ++ if run_item_status == Status.failed: + return Status.failed +- elif scenario_status == Status.untested: ++ elif run_item_status == Status.untested: + if passed_count > 0: + return Status.failed # ABORTED: Some passed, now untested. + return Status.untested +- if scenario_status != Status.skipped: ++ if run_item_status != Status.skipped: + skipped = False +- if scenario_status == Status.passed: ++ if run_item_status == Status.passed: + passed_count += 1 + + if skipped: +@@ -217,14 +217,19 @@ class ScenarioContainer(TagAndStatusStatement, Replayable): + """ + # TODO: Better use self.run_items + all_scenarios = [] +- for scenario in self.scenarios: +- if isinstance(scenario, ScenarioOutline): +- scenario_outline = scenario ++ # for scenario in self.scenarios: ++ for run_item in self.run_items: ++ if isinstance(run_item, Rule): ++ rule = run_item ++ all_scenarios.extend(rule.walk_scenarios(with_outlines=with_outlines)) ++ if isinstance(run_item, ScenarioOutline): ++ scenario_outline = run_item + if with_outlines: + all_scenarios.append(scenario_outline) + all_scenarios.extend(scenario_outline.scenarios) + else: +- all_scenarios.append(scenario) ++ assert isinstance(run_item, Scenario) ++ all_scenarios.append(run_item) + return all_scenarios + + def should_run(self, config=None): +@@ -285,9 +290,9 @@ class ScenarioContainer(TagAndStatusStatement, Replayable): + self.clear_status() + self.should_skip = True + self.skip_reason = reason +- for scenario in self.scenarios: +- scenario.skip(reason, require_not_executed) +- if not self.scenarios: ++ for run_item in self.run_items: ++ run_item.skip(reason, require_not_executed) ++ if not self.run_items: + # -- SPECIAL CASE: Feature without scenarios + self.set_status(Status.skipped) + assert self.status in self.final_status #< skipped, failed or passed. +diff --git a/behave/reporter/summary.py b/behave/reporter/summary.py +index c82daa1..2ccdc8f 100644 +--- a/behave/reporter/summary.py ++++ b/behave/reporter/summary.py +@@ -6,25 +6,52 @@ Provides a summary after each test run. + from __future__ import absolute_import, division, print_function + import sys + from time import time as time_now +-from behave.model import ScenarioOutline ++from behave.model import Rule, ScenarioOutline # MAYBE: Scenario + from behave.model_core import Status + from behave.reporter.base import Reporter + from behave.formatter.base import StreamOpener + + +-# -- DISABLED: optional_steps = ('untested', 'undefined') +-optional_steps = (Status.untested,) # MAYBE: Status.undefined +-status_order = (Status.passed, Status.failed, Status.skipped, ++# --------------------------------------------------------------------------- ++# CONSTANTS: ++# --------------------------------------------------------------------------- ++# -- DISABLED: OPTIONAL_STEPS = ('untested', 'undefined') ++OPTIONAL_STEPS = (Status.untested,) # MAYBE: Status.undefined ++STATUS_ORDER = (Status.passed, Status.failed, Status.skipped, + Status.undefined, Status.untested) + + +-def format_summary(statement_type, summary): ++# --------------------------------------------------------------------------- ++# UTILITY FUNCTIONS: ++# --------------------------------------------------------------------------- ++def pluralize(word, count=1, suffix="s"): ++ if count == 1: ++ return word ++ # -- OTHERWISE: ++ return "{0}{1}".format(word, suffix) ++ ++ ++def compute_summary_sum(summary): ++ """Compute sum of all summary counts (except: all) ++ ++ :param summary: Summary counts (as dict). ++ :return: Sum of all counts (as integer). ++ """ ++ counts_sum = 0 ++ for name, count in summary.items(): ++ if name == "all": ++ continue # IGNORE IT. ++ counts_sum += count ++ return counts_sum ++ ++ ++def format_summary0(statement_type, summary): + parts = [] +- for status in status_order: ++ for status in STATUS_ORDER: + if status.name not in summary: + continue + counts = summary[status.name] +- if status in optional_steps and counts == 0: ++ if status in OPTIONAL_STEPS and counts == 0: + # -- SHOW-ONLY: For relevant counts, suppress: untested items, etc. + continue + +@@ -40,11 +67,23 @@ def format_summary(statement_type, summary): + return ", ".join(parts) + "\n" + + +-def pluralize(word, count=1, suffix="s"): +- if count == 1: +- return word +- # -- OTHERWISE: +- return "{0}{1}".format(word, suffix) ++def format_summary(statement_type, summary): ++ parts = [] ++ for status in STATUS_ORDER: ++ if status.name not in summary: ++ continue ++ counts = summary[status.name] ++ if status in OPTIONAL_STEPS and counts == 0: ++ # -- SHOW-ONLY: For relevant counts, suppress: untested items, etc. ++ continue ++ ++ name = status.name ++ if status.name == "passed": ++ statement = pluralize(statement_type, counts) ++ name = u"%s passed" % statement ++ part = u"%d %s" % (counts, name) ++ parts.append(part) ++ return ", ".join(parts) + "\n" + + + # -- PREPARED: +@@ -60,18 +99,16 @@ def format_summary2(statement_type, summary, end="\n"): + :return: + """ + parts = [] +- counts_sum = 0 +- for status in status_order: ++ for status in STATUS_ORDER: + if status.name not in summary: + continue + counts = summary[status.name] +- if status in optional_steps and counts == 0: ++ if status in OPTIONAL_STEPS and counts == 0: + # -- SHOW-ONLY: For relevant counts, suppress: untested items, etc. + continue +- +- counts_sum += counts + parts.append((status.name, counts)) + ++ counts_sum = summary["all"] + statement = pluralize(statement_type, sum) + parts_text = ", ".join(["{0}: {1}".format(name, value) + for name, value in parts]) +@@ -79,6 +116,9 @@ def format_summary2(statement_type, summary, end="\n"): + count=counts_sum, statement=statement, parts=parts_text, end=end) + + ++# --------------------------------------------------------------------------- ++# REPORTERS: ++# --------------------------------------------------------------------------- + class SummaryReporter(Reporter): + show_failed_scenarios = True + output_stream_name = "stdout" +@@ -88,6 +128,7 @@ class SummaryReporter(Reporter): + stream = getattr(sys, self.output_stream_name, sys.stderr) + self.stream = StreamOpener.ensure_stream_with_encoder(stream) + summary_zero_data = { ++ "all": 0, + Status.passed.name: 0, + Status.failed.name: 0, + Status.skipped.name: 0, +@@ -122,10 +163,22 @@ class SummaryReporter(Reporter): + for scenario in self.failed_scenarios: + stream.write(u" %s %s\n" % (scenario.location, scenario.name)) + ++ def compute_summary_sums(self): ++ """(Re)Compute summary sum of all counts (except: all).""" ++ summaries = [ ++ self.feature_summary, ++ self.rule_summary, ++ self.scenario_summary, ++ self.step_summary ++ ] ++ for summary in summaries: ++ summary["all"] = compute_summary_sum(summary) ++ + def print_summary(self, stream=None, with_duration=True): + if stream is None: + stream = self.stream + ++ self.compute_summary_sums() + stream.write(format_summary("feature", self.feature_summary)) + rules_summary = format_summary("rule", self.rule_summary) + if self.show_rules and not rules_summary.strip().startswith("0"): +@@ -145,13 +198,7 @@ class SummaryReporter(Reporter): + # -- DISCOVER: TEST-RUN started. + self.testrun_started() + +- self.feature_summary[feature.status.name] += 1 +- self.duration += feature.duration +- for scenario in feature: +- if isinstance(scenario, ScenarioOutline): +- self.process_scenario_outline(scenario) +- else: +- self.process_scenario(scenario) ++ self.process_feature(feature) + + def end(self): + self.testrun_finished() +@@ -164,6 +211,25 @@ class SummaryReporter(Reporter): + # -- SHOW SUMMARY COUNTS: + self.print_summary() + ++ def process_run_items_for(self, parent): ++ for run_item in parent: ++ if isinstance(run_item, Rule): ++ self.process_rule(run_item) ++ elif isinstance(run_item, ScenarioOutline): ++ self.process_scenario_outline(run_item) ++ else: ++ # assert isinstance(run_item, Scenario) ++ self.process_scenario(run_item) ++ ++ def process_feature(self, feature): ++ self.duration += feature.duration ++ self.feature_summary[feature.status.name] += 1 ++ self.process_run_items_for(feature) ++ ++ def process_rule(self, rule): ++ self.rule_summary[rule.status.name] += 1 ++ self.process_run_items_for(rule) ++ + def process_scenario(self, scenario): + if scenario.status == Status.failed: + self.failed_scenarios.append(scenario) +diff --git a/tests/unit/reporters/__init__.py b/tests/unit/reporter/__init__.py +similarity index 100% +rename from tests/unit/reporters/__init__.py +rename to tests/unit/reporter/__init__.py +diff --git a/tests/unit/reporters/test_summary.py b/tests/unit/reporter/test_summary.py +similarity index 99% +rename from tests/unit/reporters/test_summary.py +rename to tests/unit/reporter/test_summary.py +index 02154db..97adbb5 100644 +--- a/tests/unit/reporters/test_summary.py ++++ b/tests/unit/reporter/test_summary.py +@@ -120,6 +120,7 @@ class TestSummaryReporter(object): + reporter.end() + + expected = { ++ "all": 5, + Status.passed.name: 2, + Status.failed.name: 1, + Status.skipped.name: 1, +@@ -156,6 +157,7 @@ class TestSummaryReporter(object): + reporter.end() + + expected = { ++ "all": 5, + Status.passed.name: 1, + Status.failed.name: 2, + Status.skipped.name: 1, +@@ -201,6 +203,7 @@ class TestSummaryReporter(object): + reporter.end() + + expected = { ++ "all": 7, + Status.passed.name: 2, + Status.failed.name: 3, + Status.skipped.name: 2, +@@ -241,6 +244,7 @@ class TestSummaryReporter(object): + reporter.end() + + expected = { ++ "all": 5, + Status.passed.name: 2, + Status.failed.name: 1, + Status.skipped.name: 1, diff --git a/meta-python/recipes-devtools/python/python3-behave/0005-SummaryReporter.print_summary-Simplify-if-Rules-are-.patch b/meta-python/recipes-devtools/python/python3-behave/0005-SummaryReporter.print_summary-Simplify-if-Rules-are-.patch new file mode 100644 index 000000000..2cbf02925 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0005-SummaryReporter.print_summary-Simplify-if-Rules-are-.patch @@ -0,0 +1,60 @@ +From db1ead991924fb71d87e02aa43ffa73eae60594e Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Wed, 13 Mar 2019 08:41:37 +0100 +Subject: [PATCH] SummaryReporter.print_summary: Simplify if Rules are used. + +--- + behave/reporter/summary.py | 7 ++++--- + tests/unit/reporter/test_summary.py | 6 +++--- + 2 files changed, 7 insertions(+), 6 deletions(-) + +diff --git a/behave/reporter/summary.py b/behave/reporter/summary.py +index 2ccdc8f..09285ea 100644 +--- a/behave/reporter/summary.py ++++ b/behave/reporter/summary.py +@@ -179,11 +179,12 @@ class SummaryReporter(Reporter): + stream = self.stream + + self.compute_summary_sums() ++ has_rules = (self.rule_summary["all"] > 0) ++ + stream.write(format_summary("feature", self.feature_summary)) +- rules_summary = format_summary("rule", self.rule_summary) +- if self.show_rules and not rules_summary.strip().startswith("0"): ++ if self.show_rules and has_rules: + # -- HINT: Show only rules, if any exists. +- self.stream.write(rules_summary) ++ self.stream.write(format_summary("rule", self.rule_summary)) + stream.write(format_summary("scenario", self.scenario_summary)) + stream.write(format_summary("step", self.step_summary)) + +diff --git a/tests/unit/reporter/test_summary.py b/tests/unit/reporter/test_summary.py +index 97adbb5..d4e85b5 100644 +--- a/tests/unit/reporter/test_summary.py ++++ b/tests/unit/reporter/test_summary.py +@@ -164,7 +164,7 @@ class TestSummaryReporter(object): + Status.untested.name: 1, + } + +- scenario_index = 2 ++ scenario_index = 1 # -- HINT: Index for scenarios if no Rules are used. + expected_parts = ("scenario", expected) + assert format_summary.call_args_list[scenario_index][0] == expected_parts + +@@ -209,7 +209,7 @@ class TestSummaryReporter(object): + Status.skipped.name: 2, + Status.untested.name: 0, + } +- scenario_index = 2 ++ scenario_index = 1 # -- HINT: Index for scenarios if no Rules are used. + expected_parts = ("scenario", expected) + assert format_summary.call_args_list[scenario_index][0] == expected_parts + +@@ -252,6 +252,6 @@ class TestSummaryReporter(object): + Status.undefined.name: 1, + } + +- step_index = 3 ++ step_index = 2 # HINT: Index for steps if not rules are used. + expected_parts = ("step", expected) + assert format_summary.call_args_list[step_index][0] == expected_parts diff --git a/meta-python/recipes-devtools/python/python3-behave/0006-Formatter-Add-basic-support-output-for-Rules.patch b/meta-python/recipes-devtools/python/python3-behave/0006-Formatter-Add-basic-support-output-for-Rules.patch new file mode 100644 index 000000000..bcfe315bf --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0006-Formatter-Add-basic-support-output-for-Rules.patch @@ -0,0 +1,395 @@ +From 24811b631e0eed92347880f1dac3f932f4b46f9d Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Wed, 13 Mar 2019 23:08:19 +0100 +Subject: [PATCH] Formatter: Add basic support/output for Rules. + +--- + behave/formatter/base.py | 18 +++++------ + behave/formatter/plain.py | 63 +++++++++++++++++++++++++++++------- + behave/formatter/pretty.py | 33 +++++++++++++++---- + behave/formatter/progress.py | 18 ++++++++++- + behave/model.py | 14 ++++---- + 5 files changed, 110 insertions(+), 36 deletions(-) + +diff --git a/behave/formatter/base.py b/behave/formatter/base.py +index f7268fa..a8b9f7c 100644 +--- a/behave/formatter/base.py ++++ b/behave/formatter/base.py +@@ -129,15 +129,6 @@ class Formatter(object): + """ + pass + +- def background(self, background): +- """Called when a (Feature) Background is provided. +- Called after :method:`feature()` is called. +- Called before processing any scenarios or scenario outlines. +- +- :param background: Background object (as :class:`behave.model.Background`) +- """ +- pass +- + def rule(self, rule): + """Called before a rule is executed. + +@@ -153,6 +144,15 @@ class Formatter(object): + # """ + # pass + ++ def background(self, background): ++ """Called when a (Feature) Background is provided. ++ Called after :method:`feature()` is called. ++ Called before processing any scenarios or scenario outlines. ++ ++ :param background: Background object (as :class:`behave.model.Background`) ++ """ ++ pass ++ + def scenario(self, scenario): + """Called before a scenario is executed (or ScenarioOutline scenarios). + +diff --git a/behave/formatter/plain.py b/behave/formatter/plain.py +index 9f1f833..e720829 100644 +--- a/behave/formatter/plain.py ++++ b/behave/formatter/plain.py +@@ -23,6 +23,8 @@ class PlainFormatter(Formatter): + + SHOW_MULTI_LINE = True + SHOW_TAGS = False ++ SHOW_RULES = True ++ SHOW_BACKGROUNDS = True + SHOW_ALIGNED_KEYWORDS = False + DEFAULT_INDENT_SIZE = 2 + RAISE_OUTPUT_ERRORS = True +@@ -35,6 +37,7 @@ class PlainFormatter(Formatter): + self.show_aligned_keywords = self.SHOW_ALIGNED_KEYWORDS + self.show_tags = self.SHOW_TAGS + self.indent_size = self.DEFAULT_INDENT_SIZE ++ self.current_rule = None + # -- ENSURE: Output stream is open. + self.stream = self.open() + self.printer = ModelPrinter(self.stream) +@@ -49,6 +52,10 @@ class PlainFormatter(Formatter): + offset = 2 + indentation = make_indentation(3 * self.indent_size + offset) + self._multiline_indentation = indentation ++ ++ if self.current_rule: ++ indent_extra = make_indentation(self.indent_size) ++ return self._multiline_indentation + indent_extra + return self._multiline_indentation + + def reset_steps(self): +@@ -60,37 +67,69 @@ class PlainFormatter(Formatter): + text = " @".join(tags) + self.stream.write(u"%s@%s\n" % (indent, text)) + ++ def write_entity(self, entity, indent="", has_tags=True): ++ if has_tags: ++ self.write_tags(entity.tags, indent) ++ text = u"%s%s: %s\n" % (indent, entity.keyword, entity.name) ++ self.stream.write(text) ++ + # -- IMPLEMENT-INTERFACE FOR: Formatter + def feature(self, feature): ++ self.current_rule = None + self.reset_steps() +- self.write_tags(feature.tags) +- self.stream.write(u"%s: %s\n" % (feature.keyword, feature.name)) ++ self.write_entity(feature) ++ # self.write_tags(feature.tags) ++ # self.stream.write(u"%s: %s\n" % (feature.keyword, feature.name)) + +- def background(self, background): ++ def rule(self, rule): ++ self.current_rule = rule + self.reset_steps() + indent = make_indentation(self.indent_size) +- text = u"%s%s: %s\n" % (indent, background.keyword, background.name) +- self.stream.write(text) ++ self.stream.write(u"\n") ++ self.write_entity(rule, indent) ++ # self.stream.write(u"%s%s: %s\n" % (indent, rule.keyword, rule.name)) ++ ++ def background(self, background): ++ self.reset_steps() ++ if not self.SHOW_BACKGROUNDS: ++ return ++ ++ indent_extra = 0 ++ if self.current_rule: ++ indent_extra = self.indent_size ++ ++ indent = make_indentation(self.indent_size + indent_extra) ++ self.write_entity(background, indent, has_tags=False) ++ # text = u"%s%s: %s\n" % (indent, background.keyword, background.name) ++ # self.stream.write(text) + + def scenario(self, scenario): ++ indent_extra = 0 ++ if self.current_rule: ++ indent_extra = self.indent_size ++ + self.reset_steps() + self.stream.write(u"\n") +- indent = make_indentation(self.indent_size) +- text = u"%s%s: %s\n" % (indent, scenario.keyword, scenario.name) +- self.write_tags(scenario.tags, indent) +- self.stream.write(text) ++ indent = make_indentation(self.indent_size + indent_extra) ++ self.write_entity(scenario, indent) ++ # text = u"%s%s: %s\n" % (indent, scenario.keyword, scenario.name) ++ # self.write_tags(scenario.tags, indent) ++ # self.stream.write(text) + + def step(self, step): + self.steps.append(step) + + def result(self, step): +- """ +- Process the result of a step (after step execution). ++ """Process the result of a step (after step execution). + + :param step: Step object with result to process. + """ ++ indent_extra = 0 ++ if self.current_rule: ++ indent_extra = self.indent_size ++ + step = self.steps.pop(0) +- indent = make_indentation(2 * self.indent_size) ++ indent = make_indentation(2 * self.indent_size + indent_extra) + if self.show_aligned_keywords: + # -- RIGHT-ALIGN KEYWORDS (max. keyword width: 6): + text = u"%s%6s %s ... " % (indent, step.keyword, step.name) +diff --git a/behave/formatter/pretty.py b/behave/formatter/pretty.py +index b6f0eac..794e1d7 100644 +--- a/behave/formatter/pretty.py ++++ b/behave/formatter/pretty.py +@@ -6,7 +6,7 @@ from behave.formatter.ansi_escapes import escapes, up + from behave.formatter.base import Formatter + from behave.model_core import Status + from behave.model_describe import escape_cell, escape_triple_quotes +-from behave.textutil import indent, text as _text ++from behave.textutil import indent, make_indentation, text as _text + import six + from six.moves import range, zip + +@@ -86,6 +86,7 @@ class PrettyFormatter(Formatter): + + def reset(self): + # -- UNUSED: self.tag_statement = None ++ self.current_rule = None + self.steps = [] + self._uri = None + self._match = None +@@ -99,7 +100,9 @@ class PrettyFormatter(Formatter): + + def feature(self, feature): + #self.print_comments(feature.comments, '') +- self.print_tags(feature.tags, '') ++ self.current_rule = None ++ prefix = "" ++ self.print_tags(feature.tags, prefix) + self.stream.write(u"%s: %s" % (feature.keyword, feature.name)) + if self.show_source: + # pylint: disable=redefined-builtin +@@ -109,6 +112,11 @@ class PrettyFormatter(Formatter): + self.print_description(feature.description, " ", False) + self.stream.flush() + ++ def rule(self, rule): ++ self.replay() ++ self.current_rule = rule ++ self.statement = rule ++ + def background(self, background): + self.replay() + self.statement = background +@@ -176,6 +184,10 @@ class PrettyFormatter(Formatter): + self.stream.flush() + + def table(self, table): ++ prefix = u" " ++ if self.current_rule: ++ prefix += u" " ++ + cell_lengths = [] + all_rows = [table.headings] + table.rows + for row in all_rows: +@@ -189,7 +201,7 @@ class PrettyFormatter(Formatter): + for i, row in enumerate(all_rows): + #for comment in row.comments: + # self.stream.write(" %s\n" % comment.value) +- self.stream.write(" |") ++ self.stream.write(u"%s|" % prefix) + for j, (cell, max_length) in enumerate(zip(row, max_lengths)): + self.stream.write(" ") + self.stream.write(self.color(cell, None, j)) +@@ -202,6 +214,8 @@ class PrettyFormatter(Formatter): + #self.stream.write(' """' + doc_string.content_type + '\n') + doc_string = _text(doc_string) + prefix = u" " ++ if self.current_rule: ++ prefix += u" " + self.stream.write(u'%s"""\n' % prefix) + doc_string = escape_triple_quotes(indent(doc_string, prefix)) + self.stream.write(doc_string) +@@ -251,12 +265,16 @@ class PrettyFormatter(Formatter): + if self.statement is None: + return + ++ prefix = u" " ++ if self.current_rule and self.statement.type != "rule": ++ prefix += prefix ++ + self.calculate_location_indentations() + self.stream.write(u"\n") + #self.print_comments(self.statement.comments, " ") + if hasattr(self.statement, "tags"): +- self.print_tags(self.statement.tags, u" ") +- self.stream.write(u" %s: %s " % (self.statement.keyword, ++ self.print_tags(self.statement.tags, prefix) ++ self.stream.write(u"%s%s: %s " % (prefix, self.statement.keyword, + self.statement.name)) + + location = self.indented_text(six.text_type(self.statement.location), True) +@@ -279,8 +297,11 @@ class PrettyFormatter(Formatter): + text_format = self.format(status.name) + arg_format = self.arg_format(status.name) + ++ prefix = u" " ++ if self.current_rule: ++ prefix += u" " + #self.print_comments(step.comments, " ") +- self.stream.write(" ") ++ self.stream.write(prefix) + self.stream.write(text_format.text(step.keyword + " ")) + line_length = 5 + len(step.keyword) + +diff --git a/behave/formatter/progress.py b/behave/formatter/progress.py +index 6d8adf6..3b471ed 100644 +--- a/behave/formatter/progress.py ++++ b/behave/formatter/progress.py +@@ -43,6 +43,7 @@ class ProgressFormatterBase(Formatter): + self.steps = [] + self.failures = [] + self.current_feature = None ++ self.current_rule = None + self.current_scenario = None + self.show_timings = config.show_timings and self.show_timings + +@@ -50,14 +51,19 @@ class ProgressFormatterBase(Formatter): + self.steps = [] + self.failures = [] + self.current_feature = None ++ self.current_rule = None + self.current_scenario = None + + # -- FORMATTER API: + def feature(self, feature): ++ self.current_rule = None + self.current_feature = feature + self.stream.write("%s " % six.text_type(feature.filename)) + self.stream.flush() + ++ def rule(self, rule): ++ self.current_rule = rule ++ + def background(self, background): + pass + +@@ -219,9 +225,16 @@ class ScenarioStepProgressFormatter(StepProgressFormatter): + + # -- FORMATTER API: + def feature(self, feature): ++ self.current_rule = None + self.current_feature = feature + self.stream.write(u"%s # %s" % (feature.name, feature.filename)) + ++ def rule(self, rule): ++ self.current_rule = rule ++ self.stream.write(u"\n\n %s: %s # %s" % ++ (rule.keyword, rule.name, rule.location)) ++ self.stream.flush() ++ + def scenario(self, scenario): + """Process the next scenario.""" + # -- LAST SCENARIO: Report failures (if any). +@@ -231,9 +244,12 @@ class ScenarioStepProgressFormatter(StepProgressFormatter): + assert not self.failures + self.current_scenario = scenario + scenario_name = scenario.name ++ prefix = self.scenario_prefix ++ if self.current_rule: ++ prefix += u" " + if scenario_name: + scenario_name += " " +- self.stream.write(u"%s%s " % (self.scenario_prefix, scenario_name)) ++ self.stream.write(u"%s%s " % (prefix, scenario_name)) + self.stream.flush() + + # -- DISABLED: +diff --git a/behave/model.py b/behave/model.py +index 6238313..3084850 100644 +--- a/behave/model.py ++++ b/behave/model.py +@@ -318,10 +318,10 @@ class ScenarioContainer(TagAndStatusStatement, Replayable): + runner.context.tags = set(self.tags) + + skip_entity_untested = runner.aborted +- run_entity = self.should_run(runner.config) ++ should_run_entity = self.should_run(runner.config) + failed_count = 0 + hooks_called = False +- if not runner.config.dry_run and run_entity: ++ if not runner.config.dry_run and should_run_entity: + hooks_called = True + for tag in self.tags: + runner.run_hook("before_tag", runner.context, tag) +@@ -332,10 +332,10 @@ class ScenarioContainer(TagAndStatusStatement, Replayable): + # -- RE-EVALUATE SHOULD-RUN STATE: + # Hook may call entity.mark_skipped() to exclude it. + skip_entity_untested = self.hook_failed or runner.aborted +- run_entity = self.should_run() ++ should_run_entity = self.should_run() + + # run this entity if the tags say so or any one of its scenarios +- if run_entity or runner.config.show_skipped: ++ if should_run_entity or runner.config.show_skipped: + for formatter in runner.formatters: + formatter_callback = getattr(formatter, entity_name, None) + if formatter_callback: +@@ -363,7 +363,7 @@ class ScenarioContainer(TagAndStatusStatement, Replayable): + break + + self.clear_status() # -- ENFORCE: compute_status() after run. +- if not self.run_items and not run_entity: ++ if not self.run_items and not should_run_entity: + # -- SPECIAL CASE: Feature without scenarios + self.set_status(Status.skipped) + +@@ -382,7 +382,7 @@ class ScenarioContainer(TagAndStatusStatement, Replayable): + # -- CLEANUP-ERROR: + self.set_status(Status.failed) + +- if run_entity or runner.config.show_skipped: ++ if should_run_entity or runner.config.show_skipped: + callback_name = "{0}_finished".format(entity_name) + if entity_name == "feature": + callback_name = "eof" +@@ -608,7 +608,6 @@ class Rule(ScenarioContainer): + .. versionadded:: 1.2.7 + .. _`feature`: gherkin.html#rule + """ +- + type = "rule" + + def __init__(self, filename, line, keyword, name, tags=None, +@@ -625,7 +624,6 @@ class Rule(ScenarioContainer): + (self.name, len(self.scenarios)) + + +- + class Background(BasicStatement, Replayable): + """A `background`_ parsed from a *feature file*. + diff --git a/meta-python/recipes-devtools/python/python3-behave/0007-BUMP-VERSION-1.2.7.dev1-was-1.2.7.dev0.patch b/meta-python/recipes-devtools/python/python3-behave/0007-BUMP-VERSION-1.2.7.dev1-was-1.2.7.dev0.patch new file mode 100644 index 000000000..b8e804269 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0007-BUMP-VERSION-1.2.7.dev1-was-1.2.7.dev0.patch @@ -0,0 +1,67 @@ +From 19a4134596217540832ed394d790d7b509ec865a Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Wed, 13 Mar 2019 23:11:50 +0100 +Subject: [PATCH] BUMP-VERSION: 1.2.7.dev1 (was: 1.2.7.dev0) + +--- + .bumpversion.cfg | 2 +- + VERSION.txt | 2 +- + behave/__init__.py | 2 +- + pytest.ini | 2 +- + setup.py | 2 +- + 5 files changed, 5 insertions(+), 5 deletions(-) + +diff --git a/.bumpversion.cfg b/.bumpversion.cfg +index f387d43..ac913c2 100644 +--- a/.bumpversion.cfg ++++ b/.bumpversion.cfg +@@ -1,5 +1,5 @@ + [bumpversion] +-current_version = 1.2.7.dev0 ++current_version = 1.2.7.dev1 + files = behave/__init__.py setup.py VERSION.txt pytest.ini .bumpversion.cfg + parse = (?P\d+)\.(?P\d+)\.(?P\d+)(?P\w*) + serialize = {major}.{minor}.{patch}{drop} +diff --git a/VERSION.txt b/VERSION.txt +index 4e63eef..c0ef36b 100644 +--- a/VERSION.txt ++++ b/VERSION.txt +@@ -1 +1 @@ +-1.2.7.dev0 ++1.2.7.dev1 +diff --git a/behave/__init__.py b/behave/__init__.py +index 8888355..31e4e55 100644 +--- a/behave/__init__.py ++++ b/behave/__init__.py +@@ -29,4 +29,4 @@ __all__ = [ + # -- DEPRECATING: + "step_matcher" + ] +-__version__ = "1.2.7.dev0" ++__version__ = "1.2.7.dev1" +diff --git a/pytest.ini b/pytest.ini +index 70e10cd..17ad388 100644 +--- a/pytest.ini ++++ b/pytest.ini +@@ -20,7 +20,7 @@ minversion = 2.8 + testpaths = test tests + python_files = test_*.py + addopts = --metadata PACKAGE_UNDER_TEST behave +- --metadata PACKAGE_VERSION 1.2.7.dev0 ++ --metadata PACKAGE_VERSION 1.2.7.dev1 + --html=build/testing/report.html --self-contained-html + --junit-xml=build/testing/report.xml + markers = +diff --git a/setup.py b/setup.py +index cb3b338..c5af262 100644 +--- a/setup.py ++++ b/setup.py +@@ -55,7 +55,7 @@ def find_packages_by_root_package(where): + # ----------------------------------------------------------------------------- + setup( + name="behave", +- version="1.2.7.dev0", ++ version="1.2.7.dev1", + description="behave is behaviour-driven development, Python style", + long_description=description, + author="Jens Engel, Benno Rice and Richard Jones", diff --git a/meta-python/recipes-devtools/python/python3-behave/0008-Correct-examples-and-docstring.patch b/meta-python/recipes-devtools/python/python3-behave/0008-Correct-examples-and-docstring.patch new file mode 100644 index 000000000..9ab6cee15 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0008-Correct-examples-and-docstring.patch @@ -0,0 +1,41 @@ +From 29d3ef4d3ff8c836bc592b92687a28bf873d0e0c Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Thu, 14 Mar 2019 22:14:54 +0100 +Subject: [PATCH] Correct examples and docstring + +--- + behave/contrib/scenario_autoretry.py | 2 +- + behave/formatter/base.py | 3 ++- + 2 files changed, 3 insertions(+), 2 deletions(-) + +diff --git a/behave/contrib/scenario_autoretry.py b/behave/contrib/scenario_autoretry.py +index 2b7f94f..2592d10 100644 +--- a/behave/contrib/scenario_autoretry.py ++++ b/behave/contrib/scenario_autoretry.py +@@ -24,7 +24,7 @@ EXAMPLE: + from behave.contrib.scenario_autoretry import patch_scenario_with_autoretry + + def before_feature(context, feature): +- for scenario in feature.scenarios: ++ for scenario in feature.walk_scenarios(): + if "autoretry" in scenario.effective_tags: + patch_scenario_with_autoretry(scenario, max_attempts=2) + +diff --git a/behave/formatter/base.py b/behave/formatter/base.py +index a8b9f7c..7f59ad4 100644 +--- a/behave/formatter/base.py ++++ b/behave/formatter/base.py +@@ -74,11 +74,12 @@ class Formatter(object): + + Processing Logic (simplified, without ScenarioOutline and skip logic):: + ++ # -- HINT: Rule processing is missing. + for feature in runner.features: + formatter = make_formatters(...) + formatter.uri(feature.filename) + formatter.feature(feature) +- for scenario in feature.scenarios: ++ for scenario in feature.walk_scenarios(): + formatter.scenario(scenario) + for step in scenario.all_steps: + formatter.step(step) diff --git a/meta-python/recipes-devtools/python/python3-behave/0009-FIX-feature.run_items-processing-with-Rule-s.patch b/meta-python/recipes-devtools/python/python3-behave/0009-FIX-feature.run_items-processing-with-Rule-s.patch new file mode 100644 index 000000000..0c70cc566 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0009-FIX-feature.run_items-processing-with-Rule-s.patch @@ -0,0 +1,58 @@ +From dee5266820aabcfe09d103cf007bb26b9db54849 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Thu, 14 Mar 2019 22:15:34 +0100 +Subject: [PATCH] FIX: feature.run_items processing with Rule(s). + +--- + behave/reporter/junit.py | 24 ++++++++++++++++-------- + 1 file changed, 16 insertions(+), 8 deletions(-) + +diff --git a/behave/reporter/junit.py b/behave/reporter/junit.py +index 48e1411..9018399 100644 +--- a/behave/reporter/junit.py ++++ b/behave/reporter/junit.py +@@ -75,7 +75,7 @@ import codecs + from xml.etree import ElementTree + from datetime import datetime + from behave.reporter.base import Reporter +-from behave.model import Scenario, ScenarioOutline, Step ++from behave.model import Rule, Scenario, ScenarioOutline, Step + from behave.model_core import Status + from behave.formatter import ansi_escapes + from behave.model_describe import ModelDescriptor +@@ -236,13 +236,8 @@ class JUnitReporter(Reporter): + feature_name = feature.name or feature_filename + suite.set(u'name', u'%s.%s' % (classname, feature_name)) + +- # -- BUILD-TESTCASES: From scenarios +- for scenario in feature: +- if isinstance(scenario, ScenarioOutline): +- scenario_outline = scenario +- self._process_scenario_outline(scenario_outline, report) +- else: +- self._process_scenario(scenario, report) ++ # -- BUILD-TESTCASES: From run_items (and scenarios) ++ self._process_run_items_for(feature, report) + + # -- ADD TESTCASES to testsuite: + for testcase in report.testcases: +@@ -457,6 +452,19 @@ class JUnitReporter(Reporter): + if scenario.status != Status.skipped or self.show_skipped: + report.testcases.append(case) + ++ def _process_run_items_for(self, parent, report): ++ for run_item in parent.run_items: ++ if isinstance(run_item, Rule): ++ self._process_rule(run_item, report) ++ elif isinstance(run_item, ScenarioOutline): ++ self._process_scenario_outline(run_item, report) ++ else: ++ assert isinstance(run_item, Scenario) ++ self._process_scenario(run_item, report) ++ ++ def _process_rule(self, rule, report): ++ self._process_run_items_for(rule, report) ++ + def _process_scenario_outline(self, scenario_outline, report): + assert isinstance(scenario_outline, ScenarioOutline) + for scenario in scenario_outline: diff --git a/meta-python/recipes-devtools/python/python3-behave/0010-docs-Disable-sphinx-intl-support-for-READTHEDOCS.patch b/meta-python/recipes-devtools/python/python3-behave/0010-docs-Disable-sphinx-intl-support-for-READTHEDOCS.patch new file mode 100644 index 000000000..122b7e9f2 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0010-docs-Disable-sphinx-intl-support-for-READTHEDOCS.patch @@ -0,0 +1,58 @@ +From b4a40c4df5872b0c9c7293b3a9fa057e208361d6 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sun, 26 May 2019 14:40:38 +0200 +Subject: [PATCH] docs: Disable sphinx-intl support for READTHEDOCS. + +--- + docs/conf.py | 17 +++++++++++++---- + 1 file changed, 13 insertions(+), 4 deletions(-) + +diff --git a/docs/conf.py b/docs/conf.py +index d38db7a..f9dfb6a 100644 +--- a/docs/conf.py ++++ b/docs/conf.py +@@ -3,6 +3,7 @@ + # SPHINX CONFIGURATION: behave documentation build configuration file + # ============================================================================= + ++from __future__ import print_function + import os.path + import sys + import importlib +@@ -13,6 +14,13 @@ import importlib + # documentation root, use os.path.abspath to make it absolute, like shown here. + sys.path.insert(0, os.path.abspath("..")) + ++# ------------------------------------------------------------------------------ ++# DETECT BUILD CONTEXT ++# ------------------------------------------------------------------------------ ++ON_READTHEDOCS = os.environ.get("READTHEDOCS", None) == "True" ++USE_SPHINX_INTERNATIONAL = not ON_READTHEDOCS ++ ++ + # ------------------------------------------------------------------------------ + # EXTENSIONS CONFIGURATION + # ------------------------------------------------------------------------------ +@@ -82,8 +90,10 @@ master_doc = "index" + # -- MULTI-LANGUAGE SUPPORT: en, ... + # SEE: https://pypi.org/project/sphinx-intl/ + # SEE: https://github.com/sphinx-doc/sphinx-intl/ +-locale_dirs = ["locale/"] # path is example but recommended. +-gettext_compact = False # optional. ++if USE_SPHINX_INTERNATIONAL: ++ locale_dirs = ["locale/"] # path is example but recommended. ++ gettext_compact = False # optional. ++ print("USE SPHINX-INTL: locale_dirs=%s" % ",".join(locale_dirs)) + + # STEPS: + # make gettext +@@ -155,8 +165,7 @@ todo_include_todos = False + html_theme = "kr" + html_theme = "bootstrap" + +-on_rtd = os.environ.get("READTHEDOCS", None) == "True" +-if on_rtd: ++if ON_READTHEDOCS: + html_theme = "default" + + if html_theme == "bootstrap": diff --git a/meta-python/recipes-devtools/python/python3-behave/0011-Cleanup-Dependent-package-versions-in-requirements.patch b/meta-python/recipes-devtools/python/python3-behave/0011-Cleanup-Dependent-package-versions-in-requirements.patch new file mode 100644 index 000000000..726402a8e --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0011-Cleanup-Dependent-package-versions-in-requirements.patch @@ -0,0 +1,81 @@ +From ffbd9840d2c2e273a0ce2ea8fe20afad034bdeb2 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Thu, 18 Apr 2019 19:02:43 +0200 +Subject: [PATCH] Cleanup: Dependent package versions in requirements. + +--- + py.requirements/basic.txt | 2 +- + py.requirements/develop.txt | 4 ++-- + py.requirements/testing.txt | 2 +- + setup.py | 6 +++--- + 4 files changed, 7 insertions(+), 7 deletions(-) + +diff --git a/py.requirements/basic.txt b/py.requirements/basic.txt +index 9eebcad..3b71bfb 100644 +--- a/py.requirements/basic.txt ++++ b/py.requirements/basic.txt +@@ -11,7 +11,7 @@ + cucumber-tag-expressions >= 1.1.1 + parse >= 1.8.2 + parse_type >= 0.4.2 +-six >= 1.11.0 ++six >= 1.12.0 + + traceback2; python_version < '3.0' + contextlib2 # MAYBE: python_version < '3.5' +diff --git a/py.requirements/develop.txt b/py.requirements/develop.txt +index c55d3cd..3deedc7 100644 +--- a/py.requirements/develop.txt ++++ b/py.requirements/develop.txt +@@ -5,8 +5,8 @@ + # -- BUILD-TOOL: + # PREPARE USAGE: invoke + # ALREADY: six >= 1.11.0 +-invoke >= 0.21.0 +-path.py >= 10.1 ++invoke >= 1.2.0 ++path.py >= 11.5.0 + pathlib; python_version <= '3.4' + pycmd + +diff --git a/py.requirements/testing.txt b/py.requirements/testing.txt +index 5876e29..3806d39 100644 +--- a/py.requirements/testing.txt ++++ b/py.requirements/testing.txt +@@ -12,4 +12,4 @@ PyHamcrest >= 1.9 + + # -- NEEDED: By some tests (as proof of concept) + # NOTE: path.py-10.1 is required for python2.6 +-path.py >= 10.1 ++path.py >= 11.5.0 +diff --git a/setup.py b/setup.py +index c5af262..ac7bddf 100644 +--- a/setup.py ++++ b/setup.py +@@ -79,7 +79,7 @@ setup( + "cucumber-tag-expressions >= 1.1.1", + "parse >= 1.8.2", + "parse_type >= 0.4.2", +- "six >= 1.11.0", ++ "six >= 1.12.0", + "traceback2; python_version < '3.0'", + "enum34; python_version < '3.4'", + # -- PREPARED: +@@ -93,7 +93,7 @@ setup( + "nose >= 1.3", + "mock >= 1.1", + "PyHamcrest >= 1.8", +- "path.py >= 10.1" ++ "path.py >= 11.5.0" + ], + cmdclass = { + "behave_test": behave_test, +@@ -110,7 +110,7 @@ setup( + "pytest-cov", + "tox", + "invoke >= 1.2.0", +- "path.py >= 10.1", ++ "path.py >= 11.5.0", + "pycmd", + "pathlib; python_version <= '3.4'", + "modernize >= 0.5", diff --git a/meta-python/recipes-devtools/python/python3-behave/0012-docs-conf.py-tweaks.patch b/meta-python/recipes-devtools/python/python3-behave/0012-docs-conf.py-tweaks.patch new file mode 100644 index 000000000..22c6c6bb6 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0012-docs-conf.py-tweaks.patch @@ -0,0 +1,30 @@ +From 430d19123b3a7adc21075b1befda8b550b7eb641 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sun, 26 May 2019 14:55:20 +0200 +Subject: [PATCH] docs: conf.py tweaks. + +--- + docs/conf.py | 3 +-- + 1 file changed, 1 insertion(+), 2 deletions(-) + +diff --git a/docs/conf.py b/docs/conf.py +index f9dfb6a..f7c2c24 100644 +--- a/docs/conf.py ++++ b/docs/conf.py +@@ -18,7 +18,7 @@ sys.path.insert(0, os.path.abspath("..")) + # DETECT BUILD CONTEXT + # ------------------------------------------------------------------------------ + ON_READTHEDOCS = os.environ.get("READTHEDOCS", None) == "True" +-USE_SPHINX_INTERNATIONAL = not ON_READTHEDOCS ++USE_SPHINX_INTERNATIONAL = True + + + # ------------------------------------------------------------------------------ +@@ -164,7 +164,6 @@ todo_include_todos = False + # a list of builtin themes. + html_theme = "kr" + html_theme = "bootstrap" +- + if ON_READTHEDOCS: + html_theme = "default" + diff --git a/meta-python/recipes-devtools/python/python3-behave/0013-FIX-Misspelled-after_rule-hook-was-after_after.patch b/meta-python/recipes-devtools/python/python3-behave/0013-FIX-Misspelled-after_rule-hook-was-after_after.patch new file mode 100644 index 000000000..aa67bd2f4 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0013-FIX-Misspelled-after_rule-hook-was-after_after.patch @@ -0,0 +1,30 @@ +From 5ea1f1b47c14cf9aeabe7d8e22511d54b15e5f1e Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sat, 8 Jun 2019 20:11:23 +0200 +Subject: [PATCH] FIX: Misspelled after_rule hook (was: after_after) + +--- + docs/context_attributes.rst | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/docs/context_attributes.rst b/docs/context_attributes.rst +index a4817d1..163664b 100644 +--- a/docs/context_attributes.rst ++++ b/docs/context_attributes.rst +@@ -23,6 +23,7 @@ config test run :class:`~behave.configuration.Configuration` Configur + aborted test run bool Set to true if test run is aborted by the user. + failed test run bool Set to true if a step fails. + feature feature :class:`~behave.model.Feature` Current feature. ++rule rule :class:`~behave.model.Feature` Current rule. + tags feature, list<:class:`~behave.model.Tag`> Effective tags of current feature, rule, scenario, scenario outline. + rule, + scenario +@@ -62,7 +63,7 @@ Hook :func:`after_tags` feature, rule or scenario + Hook :func:`before_feature` feature + Hook :func:`after_feature` feature + Hook :func:`before_rule` rule +-Hook :func:`after_after` rule ++Hook :func:`after_rule` rule + Hook :func:`before_scenario` scenario + Hook :func:`after_scenario` scenario + Hook :func:`before_step` scenario diff --git a/meta-python/recipes-devtools/python/python3-behave/0014-Add-hints-on-Gherkin-v6-grammar-issues.patch b/meta-python/recipes-devtools/python/python3-behave/0014-Add-hints-on-Gherkin-v6-grammar-issues.patch new file mode 100644 index 000000000..7322216cc --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0014-Add-hints-on-Gherkin-v6-grammar-issues.patch @@ -0,0 +1,45 @@ +From 14e4c88e9bd1a12a2f081dfb2709df9f78106ca6 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sat, 8 Jun 2019 20:12:23 +0200 +Subject: [PATCH] Add hints on Gherkin v6 grammar issues. + +--- + docs/conf.py | 4 +++- + docs/new_and_noteworthy_v1.2.7.rst | 8 ++++++++ + 2 files changed, 11 insertions(+), 1 deletion(-) + +diff --git a/docs/conf.py b/docs/conf.py +index f7c2c24..e55fb21 100644 +--- a/docs/conf.py ++++ b/docs/conf.py +@@ -54,9 +54,11 @@ for optional_module_name in optional_extensions: + extlinks = { + "pypi": ("https://pypi.org/project/%s", ""), + "github": ("https://github.com/%s", "github:/"), +- "issue": ("https://github.com/behave/behave/issue/%s", "issue #"), ++ "issue": ("https://github.com/behave/behave/issues/%s", "issue #"), + "youtube": ("https://www.youtube.com/watch?v=%s", "youtube:video="), + "behave": ("https://github.com/behave/behave", None), ++ "cucumber": ("https://github.com/cucumber/cucumber/", None), ++ "cucumber.issue": ("https://github.com/cucumber/cucumber/issues/%s", "issue #"), + } + + intersphinx_mapping = { +diff --git a/docs/new_and_noteworthy_v1.2.7.rst b/docs/new_and_noteworthy_v1.2.7.rst +index 451ed8c..80d9576 100644 +--- a/docs/new_and_noteworthy_v1.2.7.rst ++++ b/docs/new_and_noteworthy_v1.2.7.rst +@@ -92,5 +92,13 @@ Overview of the `Example Mapping`_ concepts: + * https://lisacrispin.com/2016/06/02/experiment-example-mapping/ + * https://tobythetesterblog.wordpress.com/2016/05/25/how-to-do-example-mapping/ + ++.. hint:: **Gherkin v6 Grammar Issues** ++ ++ * :cucumber.issue:`632`: Rule tags are currently only supported in `behave`. ++ The Cucumber Gherkin v6 grammar currently lacks this functionality. ++ ++ * :cucumber.issue:`590`: Rule Background: ++ A proposal is pending to remove Rule Backgrounds again ++ + + .. include:: _content.tag_expressions_v2.rst diff --git a/meta-python/recipes-devtools/python/python3-behave/0015-README-ReST-tweaks.patch b/meta-python/recipes-devtools/python/python3-behave/0015-README-ReST-tweaks.patch new file mode 100644 index 000000000..04e45f3f3 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0015-README-ReST-tweaks.patch @@ -0,0 +1,18 @@ +From 12d37ec6af46d3edb806679183ab2138e4f1f5bf Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sat, 8 Jun 2019 20:13:08 +0200 +Subject: [PATCH] README: ReST tweaks + +--- + etc/gherkin/README.rst | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/etc/gherkin/README.rst b/etc/gherkin/README.rst +index ad3cedb..7ec2108 100644 +--- a/etc/gherkin/README.rst ++++ b/etc/gherkin/README.rst +@@ -1,3 +1,4 @@ + SOURCE: ++ + * https://github.com/cucumber/cucumber/blob/master/gherkin/gherkin-languages.json + * https://raw.githubusercontent.com/cucumber/cucumber/master/gherkin/gherkin-languages.json diff --git a/meta-python/recipes-devtools/python/python3-behave/0016-Example-using-Gherkin-v6-grammar.patch b/meta-python/recipes-devtools/python/python3-behave/0016-Example-using-Gherkin-v6-grammar.patch new file mode 100644 index 000000000..cbbcfd541 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0016-Example-using-Gherkin-v6-grammar.patch @@ -0,0 +1,228 @@ +From eabcf73f0e7f26ef021cef30950c7bb3d2442226 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sat, 8 Jun 2019 20:16:01 +0200 +Subject: [PATCH] Example using Gherkin v6 grammar. + +--- + examples/gherkin_v6/README.rst | 18 ++++++++ + examples/gherkin_v6/behave.ini | 42 +++++++++++++++++++ + examples/gherkin_v6/features/rule_1.feature | 42 +++++++++++++++++++ + examples/gherkin_v6/features/rule_2.feature | 42 +++++++++++++++++++ + .../features/steps/example_steps.py | 21 ++++++++++ + .../gherkin_v6/features/steps/person_steps.py | 7 ++++ + 6 files changed, 172 insertions(+) + create mode 100644 examples/gherkin_v6/README.rst + create mode 100644 examples/gherkin_v6/behave.ini + create mode 100644 examples/gherkin_v6/features/rule_1.feature + create mode 100644 examples/gherkin_v6/features/rule_2.feature + create mode 100644 examples/gherkin_v6/features/steps/example_steps.py + create mode 100644 examples/gherkin_v6/features/steps/person_steps.py + +diff --git a/examples/gherkin_v6/README.rst b/examples/gherkin_v6/README.rst +new file mode 100644 +index 0000000..58199dd +--- /dev/null ++++ b/examples/gherkin_v6/README.rst +@@ -0,0 +1,18 @@ ++Gherkin v6 Examples ++============================================================================= ++ ++ ++SCRATCHPAD: Problems ++----------------------------------------------------------------------------- ++ ++- SummaryReporter: Shows wrong counts when Rules are present:: ++ ++ ... ++ 0 features passed, 0 failed, 1 skipped XXX ++ 3 rules passed, 0 failed, 0 skipped ++ 5 scenarios passed, 0 failed, 0 skipped ++ 13 steps passed, 0 failed, 0 skipped, 0 undefined ++ ++ ++- Formatters: PrettyFormatter, PlainFormatter (at least) need Rule support ++ +diff --git a/examples/gherkin_v6/behave.ini b/examples/gherkin_v6/behave.ini +new file mode 100644 +index 0000000..45c0f0d +--- /dev/null ++++ b/examples/gherkin_v6/behave.ini +@@ -0,0 +1,42 @@ ++# ============================================================================= ++# BEHAVE CONFIGURATION ++# ============================================================================= ++# FILE: .behaverc, behave.ini, setup.cfg, tox.ini ++# ++# SEE ALSO: ++# * http://packages.python.org/behave/behave.html#configuration-files ++# * https://github.com/behave/behave ++# * http://pypi.python.org/pypi/behave/ ++# ============================================================================= ++ ++[behave] ++default_tags = not (@xfail or @not_implemented) ++show_skipped = false ++format = rerun ++ progress3 ++outfiles = rerun.txt ++ reports/report_progress3.txt ++junit = true ++logging_level = INFO ++# logging_format = LOG.%(levelname)-8s %(name)-10s: %(message)s ++# logging_format = LOG.%(levelname)-8s %(asctime)s %(name)-10s: %(message)s ++ ++# -- ALLURE-FORMATTER REQUIRES: ++# brew install allure ++# pip install allure-behave ++# ALLURE_REPORTS_DIR=allure.reports ++# behave -f allure -o $ALLURE_REPORTS_DIR ... ++# allure serve $ALLURE_REPORTS_DIR ++# ++# SEE ALSO: ++# * https://github.com/allure-framework/allure2 ++# * https://github.com/allure-framework/allure-python ++[behave.formatters] ++allure = allure_behave.formatter:AllureFormatter ++ ++# PREPARED: ++# [behave] ++# format = ... missing_steps ... ++# output = ... features/steps/missing_steps.py ... ++# [behave.formatters] ++# missing_steps = behave.contrib.formatter_missing_steps:MissingStepsFormatter +diff --git a/examples/gherkin_v6/features/rule_1.feature b/examples/gherkin_v6/features/rule_1.feature +new file mode 100644 +index 0000000..a802e19 +--- /dev/null ++++ b/examples/gherkin_v6/features/rule_1.feature +@@ -0,0 +1,42 @@ ++Feature: Gherkin v6 Example -- with Rules ++ Feature description line 1. ++ ++ Background: Feature.Background ++ Given feature background step_1 ++ ++ Rule: R1 (with Rule.Background) ++ Rule R1 description line 1. ++ ++ Background: R1.Background ++ Given rule R1 background step_1 ++ When rule R1 background step_2 ++ ++ Example: R1.Scenario_1 ++ When rule R1 scenario_1 step_1 ++ Then rule R1 scenario_1 step_2 ++ ++ Example: R1.Scenario_2 ++ Given rule R1 scenario_2 step_1 ++ Then rule R1 scenario_2 step_2 ++ ++ Rule: R2 (without Rule.Background) ++ Rule R2 description line 1. ++ ++ Example: R2.Scenario_1 ++ When rule R2 scenario_1 step_1 ++ Then rule R2 scenario_1 step_2 ++ ++ ++ Rule: R3 (with empty Rule.Background) ++ Rule R3 description line 1. ++ Rule R3 description line 2. ++ ++ Background: R3.EmptyBackground ++ ++ Scenario Template: R3.Scenario ++ Given a person named "" ++ ++ Examples: ++ | name | ++ | Alice | ++ | Bob | +diff --git a/examples/gherkin_v6/features/rule_2.feature b/examples/gherkin_v6/features/rule_2.feature +new file mode 100644 +index 0000000..a802e19 +--- /dev/null ++++ b/examples/gherkin_v6/features/rule_2.feature +@@ -0,0 +1,42 @@ ++Feature: Gherkin v6 Example -- with Rules ++ Feature description line 1. ++ ++ Background: Feature.Background ++ Given feature background step_1 ++ ++ Rule: R1 (with Rule.Background) ++ Rule R1 description line 1. ++ ++ Background: R1.Background ++ Given rule R1 background step_1 ++ When rule R1 background step_2 ++ ++ Example: R1.Scenario_1 ++ When rule R1 scenario_1 step_1 ++ Then rule R1 scenario_1 step_2 ++ ++ Example: R1.Scenario_2 ++ Given rule R1 scenario_2 step_1 ++ Then rule R1 scenario_2 step_2 ++ ++ Rule: R2 (without Rule.Background) ++ Rule R2 description line 1. ++ ++ Example: R2.Scenario_1 ++ When rule R2 scenario_1 step_1 ++ Then rule R2 scenario_1 step_2 ++ ++ ++ Rule: R3 (with empty Rule.Background) ++ Rule R3 description line 1. ++ Rule R3 description line 2. ++ ++ Background: R3.EmptyBackground ++ ++ Scenario Template: R3.Scenario ++ Given a person named "" ++ ++ Examples: ++ | name | ++ | Alice | ++ | Bob | +diff --git a/examples/gherkin_v6/features/steps/example_steps.py b/examples/gherkin_v6/features/steps/example_steps.py +new file mode 100644 +index 0000000..f4822f3 +--- /dev/null ++++ b/examples/gherkin_v6/features/steps/example_steps.py +@@ -0,0 +1,21 @@ ++# -*- coding: UTF-8 -*- ++from __future__ import absolute_import, print_function ++from behave import step ++ ++ ++@step(u'feature background step_{step_id:d}') ++def step_rule_background(ctx, step_id): ++ print("feature background step_{0}".format(step_id)) ++ ++ ++@step(u'rule {rule_id:w} background step_{step_id:d}') ++def step_rule_background(ctx, rule_id, step_id): ++ print("rule {0} background step_{1}".format(rule_id, step_id)) ++ ++ ++@step(u'rule {rule_id:w} scenario_{scenario_id:d} step_{step_id:d}') ++def step_rule_scenario(ctx, rule_id, scenario_id, step_id): ++ print("rule {0} scenario_{1} step_{2}".format( ++ rule_id, scenario_id, step_id)) ++ ++ +diff --git a/examples/gherkin_v6/features/steps/person_steps.py b/examples/gherkin_v6/features/steps/person_steps.py +new file mode 100644 +index 0000000..714ac01 +--- /dev/null ++++ b/examples/gherkin_v6/features/steps/person_steps.py +@@ -0,0 +1,7 @@ ++# -*- coding: UTF-8 -*- ++from behave import given ++ ++ ++@given(u'a person named "{name}"') ++def step_given_person_with_name(ctx, name): ++ pass diff --git a/meta-python/recipes-devtools/python/python3-behave/0017-PREPARE-Python-3.8-support.patch b/meta-python/recipes-devtools/python/python3-behave/0017-PREPARE-Python-3.8-support.patch new file mode 100644 index 000000000..7c6e9b521 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0017-PREPARE-Python-3.8-support.patch @@ -0,0 +1,39 @@ +From 5e529b9ae17c15231f989c17fe1a09897edf6477 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sat, 8 Jun 2019 20:39:42 +0200 +Subject: [PATCH] PREPARE: Python-3.8 support + +--- + .travis.yml | 8 +++++--- + 1 file changed, 5 insertions(+), 3 deletions(-) + +diff --git a/.travis.yml b/.travis.yml +index 7015b88..d8f2443 100644 +--- a/.travis.yml ++++ b/.travis.yml +@@ -1,12 +1,14 @@ + language: python + sudo: false ++dist: xenial # required for Python >= 3.7 + python: +- - "3.6" ++ - "3.7" + - "2.7" ++ - "3.6" + - "3.5" + - "pypy" + - "pypy3" +- - "3.7-dev" ++ - "3.8-dev" + + # -- DISABLED: + # - "nightly" +@@ -19,7 +21,7 @@ python: + # -- TEST-BALLON: Check if Python 3.6 is actually Python 3.5.1 or newer + matrix: + allow_failures: +- - python: "3.7-dev" ++ - python: "3.8-dev" + - python: "nightly" + + cache: diff --git a/meta-python/recipes-devtools/python/python3-behave/0018-py3.8-Try-to-fix-logging.Formatter-validate-problem.patch b/meta-python/recipes-devtools/python/python3-behave/0018-py3.8-Try-to-fix-logging.Formatter-validate-problem.patch new file mode 100644 index 000000000..4fe596a2f --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0018-py3.8-Try-to-fix-logging.Formatter-validate-problem.patch @@ -0,0 +1,22 @@ +From c046c3a31322c7c29ff6d7f6172df41da3530683 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sun, 9 Jun 2019 13:17:10 +0200 +Subject: [PATCH] py3.8: Try to fix logging.Formatter validate problem + +--- + tests/unit/test_capture.py | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/tests/unit/test_capture.py b/tests/unit/test_capture.py +index ac2655e..d9a3f3a 100644 +--- a/tests/unit/test_capture.py ++++ b/tests/unit/test_capture.py +@@ -20,6 +20,8 @@ def create_capture_controller(config=None): + config.log_capture = True + config.logging_filter = None + config.logging_level = "INFO" ++ config.logging_format = "%(levelname)s:%(name)s:%(message)s" ++ config.logging_datefmt = None + return CaptureController(config) + + def setup_capture_controller(capture_controller, context=None): diff --git a/meta-python/recipes-devtools/python/python3-behave/0019-travis.ci-Temporarily-move-py38-dev-to-front-build-f.patch b/meta-python/recipes-devtools/python/python3-behave/0019-travis.ci-Temporarily-move-py38-dev-to-front-build-f.patch new file mode 100644 index 000000000..121da7094 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0019-travis.ci-Temporarily-move-py38-dev-to-front-build-f.patch @@ -0,0 +1,28 @@ +From fbe6b2937087db11f96e21b36a540270d7c2b165 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sun, 9 Jun 2019 13:19:58 +0200 +Subject: [PATCH] travis.ci: Temporarily move py38-dev to front (build first). + +--- + .travis.yml | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/.travis.yml b/.travis.yml +index d8f2443..35bce8c 100644 +--- a/.travis.yml ++++ b/.travis.yml +@@ -2,13 +2,13 @@ language: python + sudo: false + dist: xenial # required for Python >= 3.7 + python: ++ - "3.8-dev" + - "3.7" + - "2.7" + - "3.6" + - "3.5" + - "pypy" + - "pypy3" +- - "3.8-dev" + + # -- DISABLED: + # - "nightly" diff --git a/meta-python/recipes-devtools/python/python3-behave/0020-travis.ci-Tweaks-for-faster-builds-temporarily.patch b/meta-python/recipes-devtools/python/python3-behave/0020-travis.ci-Tweaks-for-faster-builds-temporarily.patch new file mode 100644 index 000000000..cf9aff701 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0020-travis.ci-Tweaks-for-faster-builds-temporarily.patch @@ -0,0 +1,35 @@ +From 41525c748413405d0faf6c8fc9a345047e31a1a7 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sun, 9 Jun 2019 13:26:19 +0200 +Subject: [PATCH] travis.ci: Tweaks for faster builds (temporarily). + +--- + .travis.yml | 12 ++++++------ + 1 file changed, 6 insertions(+), 6 deletions(-) + +diff --git a/.travis.yml b/.travis.yml +index 35bce8c..fbc3520 100644 +--- a/.travis.yml ++++ b/.travis.yml +@@ -5,15 +5,15 @@ python: + - "3.8-dev" + - "3.7" + - "2.7" +- - "3.6" +- - "3.5" +- - "pypy" +- - "pypy3" ++ ++# -- DISABLE-TEMPORARILY: Ensure faster builds ++# - "3.6" ++# - "3.5" ++# - "pypy" ++# - "pypy3" + + # -- DISABLED: + # - "nightly" +-# - "3.4" +-# - "3.3" + # + # NOW SUPPORTED: "3.5" => python 3.5.2 (>= 3.5.1) + # NOTE: nightly = 3.7-dev diff --git a/meta-python/recipes-devtools/python/python3-behave/0021-FIX-py3.8-logging.Formatter.validate-problem.patch b/meta-python/recipes-devtools/python/python3-behave/0021-FIX-py3.8-logging.Formatter.validate-problem.patch new file mode 100644 index 000000000..5fb1176ac --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0021-FIX-py3.8-logging.Formatter.validate-problem.patch @@ -0,0 +1,47 @@ +From fed4bb3273633e5f81fc8ba21c8b62255c7eefcd Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sun, 9 Jun 2019 13:42:20 +0200 +Subject: [PATCH] FIX py3.8: logging.Formatter.validate() problem. + +--- + test/test_runner.py | 6 ++++++ + 1 file changed, 6 insertions(+) + +diff --git a/test/test_runner.py b/test/test_runner.py +index 57c9445..6647283 100644 +--- a/test/test_runner.py ++++ b/test/test_runner.py +@@ -286,6 +286,7 @@ class TestContext(unittest.TestCase): + eq_("thing" in self.context, True) + del self.context.thing + ++ + class ExampleSteps(object): + text = None + table = None +@@ -320,6 +321,7 @@ class ExampleSteps(object): + for keyword, pattern, func in step_definitions: + step_registry.add_step_definition(keyword, pattern, func) + ++ + class TestContext_ExecuteSteps(unittest.TestCase): + """ + Test the behave.runner.Context.execute_steps() functionality. +@@ -341,6 +343,8 @@ class TestContext_ExecuteSteps(unittest.TestCase): + runner_.config.stdout_capture = False + runner_.config.stderr_capture = False + runner_.config.log_capture = False ++ runner_.config.logging_format = None ++ runner_.config.logging_datefmt = None + runner_.step_registry = self.step_registry + + self.context = runner.Context(runner_) +@@ -658,6 +662,8 @@ class TestRunWithPaths(unittest.TestCase): + self.config.logging_filter = None + self.config.outputs = [Mock(), StreamOpener(stream=sys.stdout)] + self.config.format = ["plain", "progress"] ++ self.config.logging_format = None ++ self.config.logging_datefmt = None + self.runner = runner.Runner(self.config) + self.load_hooks = self.runner.load_hooks = Mock() + self.load_step_definitions = self.runner.load_step_definitions = Mock() diff --git a/meta-python/recipes-devtools/python/python3-behave/0022-PREPARE-FOR-Python-3.8-asyncio.coroutine-is-deprecat.patch b/meta-python/recipes-devtools/python/python3-behave/0022-PREPARE-FOR-Python-3.8-asyncio.coroutine-is-deprecat.patch new file mode 100644 index 000000000..b46846478 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0022-PREPARE-FOR-Python-3.8-asyncio.coroutine-is-deprecat.patch @@ -0,0 +1,42 @@ +From d54d8c038a7e5f9d2168daa6e13362d1df7d17a5 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sun, 9 Jun 2019 13:58:22 +0200 +Subject: [PATCH] PREPARE FOR: Python 3.8, @asyncio.coroutine is deprecated + since py38. + +--- + tests/api/_test_async_step34.py | 9 ++++++--- + 1 file changed, 6 insertions(+), 3 deletions(-) + +diff --git a/tests/api/_test_async_step34.py b/tests/api/_test_async_step34.py +index 8242be7..1c0c31f 100644 +--- a/tests/api/_test_async_step34.py ++++ b/tests/api/_test_async_step34.py +@@ -37,13 +37,16 @@ from .testing_support_async import AsyncStepTheory + # ----------------------------------------------------------------------------- + # TEST MARKERS: + # ----------------------------------------------------------------------------- ++# DEPRECATED: @asyncio.coroutine decorator (since: Python >= 3.8) + _python_version = float("%s.%s" % sys.version_info[:2]) +-py34_or_newer = pytest.mark.skipif(_python_version < 3.4, reason="Needs Python >= 3.4") ++requires_py34_to_py37 = pytest.mark.skipif(not (3.4 <= _python_version < 3.8), ++ reason="Supported only for python.versions: 3.4 .. 3.7 (inclusive)") ++ + + # ----------------------------------------------------------------------------- + # TESTSUITE: + # ----------------------------------------------------------------------------- +-@py34_or_newer ++@requires_py34_to_py37 + class TestAsyncStepDecoratorPy34(object): + + def test_step_decorator_async_run_until_complete2(self): +@@ -128,7 +131,7 @@ class TestAsyncContext(object): + assert async_context.loop is loop0 + + +-@py34_or_newer ++@requires_py34_to_py37 + class TestAsyncStepRunPy34(object): + """Ensure that execution of async-steps works as expected.""" + diff --git a/meta-python/recipes-devtools/python/python3-behave/0023-UPDATE-Add-755-info.patch b/meta-python/recipes-devtools/python/python3-behave/0023-UPDATE-Add-755-info.patch new file mode 100644 index 000000000..56b8bd6c7 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0023-UPDATE-Add-755-info.patch @@ -0,0 +1,24 @@ +From db66eecf4c5301ca45db9f71c7b2c3c26d5927a1 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sun, 9 Jun 2019 14:06:14 +0200 +Subject: [PATCH] UPDATE: Add #755 info + +--- + CHANGES.rst | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/CHANGES.rst b/CHANGES.rst +index d6e96af..a91e22a 100644 +--- a/CHANGES.rst ++++ b/CHANGES.rst +@@ -30,6 +30,10 @@ ENHANCEMENTS: + * issue #675: Feature files cannot be found within symlink directories (provided by: smadness, pull #680) + + ++PARTIALLY FIXED: ++ ++* issue #755: Failures with Python 3.8 (submitted by: hroncok) ++ + FIXED: + + * issue #725: Scenario Outline description lines seem to be ignored (submitted by: nizwiz) diff --git a/meta-python/recipes-devtools/python/python3-behave/0024-FIX-WARNING-Related-to-docstring-example-and-weird-b.patch b/meta-python/recipes-devtools/python/python3-behave/0024-FIX-WARNING-Related-to-docstring-example-and-weird-b.patch new file mode 100644 index 000000000..8f9464a2d --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0024-FIX-WARNING-Related-to-docstring-example-and-weird-b.patch @@ -0,0 +1,25 @@ +From 467b223dc84f52933b2782ce9d116e89d48a26f1 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sun, 9 Jun 2019 14:27:23 +0200 +Subject: [PATCH] FIX-WARNING: Related to docstring-example and weird backslash + usage. + +--- + behave/matchers.py | 4 +--- + 1 file changed, 1 insertion(+), 3 deletions(-) + +diff --git a/behave/matchers.py b/behave/matchers.py +index c896f52..0fee0c7 100644 +--- a/behave/matchers.py ++++ b/behave/matchers.py +@@ -261,9 +261,7 @@ class CFParseMatcher(ParseMatcher): + + + def register_type(**kw): +- # pylint: disable=anomalous-backslash-in-string +- # REQUIRED-BY: code example +- """Registers a custom type that will be available to "parse" ++ r"""Registers a custom type that will be available to "parse" + for type conversion during step matching. + + Converters should be supplied as ``name=callable`` arguments (or as dict). diff --git a/meta-python/recipes-devtools/python/python3-behave/0025-FIX-invalid-escape-sequence-warnings-w-regex-pattern.patch b/meta-python/recipes-devtools/python/python3-behave/0025-FIX-invalid-escape-sequence-warnings-w-regex-pattern.patch new file mode 100644 index 000000000..9213ccd2e --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0025-FIX-invalid-escape-sequence-warnings-w-regex-pattern.patch @@ -0,0 +1,29 @@ +From 11ea2c45d5b88c92b25ab8e8027a931df81c2abc Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sun, 9 Jun 2019 14:37:41 +0200 +Subject: [PATCH] FIX: invalid escape sequence warnings (w/ regex patterns). + +--- + tests/unit/test_behave4cmd_command_shell_proc.py | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/tests/unit/test_behave4cmd_command_shell_proc.py b/tests/unit/test_behave4cmd_command_shell_proc.py +index aae5e9f..c45ab3b 100644 +--- a/tests/unit/test_behave4cmd_command_shell_proc.py ++++ b/tests/unit/test_behave4cmd_command_shell_proc.py +@@ -1,5 +1,5 @@ + # -*- coding: UTF-8 -*- +-""" ++r""" + + Regular expressions for winpath: + http://regexlib.com/Search.aspx?k=file+name +@@ -61,7 +61,7 @@ line_processor_ioerrors = [ + + line_processor_traceback = [ + ExceptionWithPathNormalizer( +- '^\s*File "(?P.*)", line \d+, in ', ++ r'^\s*File "(?P.*)", line \d+, in ', + ' File "'), + BehaveWinCommandOutputProcessor.line_processors[4], + ] diff --git a/meta-python/recipes-devtools/python/python3-behave/0026-DEPRECATING-CLEANUP-Move-deprecated-tag-matcher-clas.patch b/meta-python/recipes-devtools/python/python3-behave/0026-DEPRECATING-CLEANUP-Move-deprecated-tag-matcher-clas.patch new file mode 100644 index 000000000..67ea8860e --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0026-DEPRECATING-CLEANUP-Move-deprecated-tag-matcher-clas.patch @@ -0,0 +1,1020 @@ +From 72279d87372cf21d980c40d01fce2f59bb734884 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sun, 9 Jun 2019 16:04:43 +0200 +Subject: [PATCH] DEPRECATING-CLEANUP: Move deprecated tag matcher classes + +- behave.tag_matcher.OnlyWithCategoryTagMatcher +- behave.tag_matcher.OnlyWithAnyCategoryTagMatcher + +to "behave.attic.tag_matcher". +Move related unit tests to "tests.attic/unit/test_tag_matcher.py". +--- + behave/attic/__init__.py | 0 + behave/attic/tag_matcher.py | 181 +++++++++++++++++ + behave/tag_matcher.py | 189 +----------------- + tests.attic/__init__.py | 0 + tests.attic/unit/__init__.py | 0 + tests.attic/unit/test_tag_matcher.py | 280 +++++++++++++++++++++++++++ + tests/unit/test_tag_matcher.py | 279 +------------------------- + 7 files changed, 470 insertions(+), 459 deletions(-) + create mode 100644 behave/attic/__init__.py + create mode 100644 behave/attic/tag_matcher.py + create mode 100644 tests.attic/__init__.py + create mode 100644 tests.attic/unit/__init__.py + create mode 100644 tests.attic/unit/test_tag_matcher.py + +diff --git a/behave/attic/__init__.py b/behave/attic/__init__.py +new file mode 100644 +index 0000000..e69de29 +diff --git a/behave/attic/tag_matcher.py b/behave/attic/tag_matcher.py +new file mode 100644 +index 0000000..f07dcbf +--- /dev/null ++++ b/behave/attic/tag_matcher.py +@@ -0,0 +1,181 @@ ++# ----------------------------------------------------------------------------- ++# PROTOTYPING CLASSES: Should no longer be used ++# ----------------------------------------------------------------------------- ++ ++import warnings ++from behave.tag_matcher import TagMatcher ++ ++ ++class OnlyWithCategoryTagMatcher(TagMatcher): ++ """ ++ Provides a tag matcher that allows to determine if feature/scenario ++ should run or should be excluded from the run-set (at runtime). ++ ++ .. deprecated:: Use :class:`ActiveTagMatcher` instead. ++ ++ EXAMPLE: ++ -------- ++ ++ Run some scenarios only when runtime conditions are met: ++ ++ * Run scenario Alice only on Windows OS ++ * Run scenario Bob only on MACOSX ++ ++ .. code-block:: gherkin ++ ++ # -- FILE: features/alice.feature ++ # TAG SCHEMA: @only.with_{category}={current_value} ++ Feature: ++ ++ @only.with_os=win32 ++ Scenario: Alice (Run only on Windows) ++ Given I do something ++ ... ++ ++ @only.with_os=darwin ++ Scenario: Bob (Run only on MACOSX) ++ Given I do something else ++ ... ++ ++ ++ .. code-block:: python ++ ++ # -- FILE: features/environment.py ++ from behave.tag_matcher import OnlyWithCategoryTagMatcher ++ import sys ++ ++ # -- MATCHES TAGS: @only.with_{category}=* = @only.with_os=* ++ active_tag_matcher = OnlyWithCategoryTagMatcher("os", sys.platform) ++ ++ def before_scenario(context, scenario): ++ if active_tag_matcher.should_exclude_with(scenario.effective_tags): ++ scenario.skip() #< LATE-EXCLUDE from run-set. ++ """ ++ tag_prefix = "only.with_" ++ value_separator = "=" ++ ++ def __init__(self, category, value, tag_prefix=None, value_sep=None): ++ warnings.warn("Use ActiveTagMatcher instead.", DeprecationWarning) ++ super(OnlyWithCategoryTagMatcher, self).__init__() ++ self.active_tag = self.make_category_tag(category, value, ++ tag_prefix, value_sep) ++ self.category_tag_prefix = self.make_category_tag(category, None, ++ tag_prefix, value_sep) ++ ++ def should_exclude_with(self, tags): ++ category_tags = self.select_category_tags(tags) ++ if category_tags and self.active_tag not in category_tags: ++ return True ++ # -- OTHERWISE: feature/scenario with theses tags should run. ++ return False ++ ++ def select_category_tags(self, tags): ++ return [tag for tag in tags ++ if tag.startswith(self.category_tag_prefix)] ++ ++ @classmethod ++ def make_category_tag(cls, category, value=None, tag_prefix=None, ++ value_sep=None): ++ if tag_prefix is None: ++ tag_prefix = cls.tag_prefix ++ if value_sep is None: ++ value_sep = cls.value_separator ++ value = value or "" ++ return "%s%s%s%s" % (tag_prefix, category, value_sep, value) ++ ++ ++class OnlyWithAnyCategoryTagMatcher(TagMatcher): ++ """ ++ Provides a tag matcher that matches any category that follows the ++ "@only.with_" tag schema and determines if it should run or ++ should be excluded from the run-set (at runtime). ++ ++ TAG SCHEMA: @only.with_{category}={value} ++ ++ .. seealso:: OnlyWithCategoryTagMatcher ++ .. deprecated:: Use :class:`ActiveTagMatcher` instead. ++ ++ EXAMPLE: ++ -------- ++ ++ Run some scenarios only when runtime conditions are met: ++ ++ * Run scenario Alice only on Windows OS ++ * Run scenario Bob only with browser Chrome ++ ++ .. code-block:: gherkin ++ ++ # -- FILE: features/alice.feature ++ # TAG SCHEMA: @only.with_{category}={current_value} ++ Feature: ++ ++ @only.with_os=win32 ++ Scenario: Alice (Run only on Windows) ++ Given I do something ++ ... ++ ++ @only.with_browser=chrome ++ Scenario: Bob (Run only with Web-Browser Chrome) ++ Given I do something else ++ ... ++ ++ ++ .. code-block:: python ++ ++ # -- FILE: features/environment.py ++ from behave.tag_matcher import OnlyWithAnyCategoryTagMatcher ++ import sys ++ ++ # -- MATCHES ANY TAGS: @only.with_{category}={value} ++ # NOTE: active_tag_value_provider provides current category values. ++ active_tag_value_provider = { ++ "browser": os.environ.get("BEHAVE_BROWSER", "chrome"), ++ "os": sys.platform, ++ } ++ active_tag_matcher = OnlyWithAnyCategoryTagMatcher(active_tag_value_provider) ++ ++ def before_scenario(context, scenario): ++ if active_tag_matcher.should_exclude_with(scenario.effective_tags): ++ scenario.skip() #< LATE-EXCLUDE from run-set. ++ """ ++ ++ def __init__(self, value_provider, tag_prefix=None, value_sep=None): ++ warnings.warn("Use ActiveTagMatcher instead.", DeprecationWarning) ++ super(OnlyWithAnyCategoryTagMatcher, self).__init__() ++ if value_sep is None: ++ value_sep = OnlyWithCategoryTagMatcher.value_separator ++ self.value_provider = value_provider ++ self.tag_prefix = tag_prefix or OnlyWithCategoryTagMatcher.tag_prefix ++ self.value_separator = value_sep ++ ++ def should_exclude_with(self, tags): ++ exclude_decision_map = {} ++ for category_tag in self.select_category_tags(tags): ++ category, value = self.parse_category_tag(category_tag) ++ active_value = self.value_provider.get(category, None) ++ if active_value is None: ++ # -- CASE: Unknown category, ignore it. ++ continue ++ elif active_value == value: ++ # -- CASE: Active category value selected, decision should run. ++ exclude_decision_map[category] = False ++ else: ++ # -- CASE: Inactive category value selected, may exclude it. ++ if category not in exclude_decision_map: ++ exclude_decision_map[category] = True ++ return any(exclude_decision_map.values()) ++ ++ def select_category_tags(self, tags): ++ return [tag for tag in tags ++ if tag.startswith(self.tag_prefix)] ++ ++ def parse_category_tag(self, tag): ++ assert tag and tag.startswith(self.tag_prefix) ++ category_value = tag[len(self.tag_prefix):] ++ if self.value_separator in category_value: ++ category, value = category_value.split(self.value_separator, 1) ++ else: ++ # -- OOPS: TAG SCHEMA FORMAT MISMATCH ++ category = category_value ++ value = None ++ return category, value +diff --git a/behave/tag_matcher.py b/behave/tag_matcher.py +index f1f955b..5f9dce0 100644 +--- a/behave/tag_matcher.py ++++ b/behave/tag_matcher.py +@@ -1,9 +1,12 @@ +-# -*- coding: utf-8 -*- ++# -*- coding: UTF-8 -*- ++""" ++Contains classes and functionality to provide a skip-if logic based on tags ++in feature files. ++""" + + from __future__ import absolute_import + import re + import operator +-import warnings + import six + + +@@ -34,10 +37,10 @@ class ActiveTagMatcher(TagMatcher): + """Provides an active tag matcher for many categories. + + TAG SCHEMA: +- * active.with_{category}={value} +- * not_active.with_{category}={value} + * use.with_{category}={value} + * not.with_{category}={value} ++ * active.with_{category}={value} ++ * not_active.with_{category}={value} + * only.with_{category}={value} (NOTE: For backward compatibility) + + TAG LOGIC +@@ -285,181 +288,3 @@ def setup_active_tag_values(active_tag_values, data): + for category in list(active_tag_values.keys()): + if category in data: + active_tag_values[category] = data[category] +- +- +-# ----------------------------------------------------------------------------- +-# PROTOTYPING CLASSES: +-# ----------------------------------------------------------------------------- +-class OnlyWithCategoryTagMatcher(TagMatcher): +- """ +- Provides a tag matcher that allows to determine if feature/scenario +- should run or should be excluded from the run-set (at runtime). +- +- .. deprecated:: Use :class:`ActiveTagMatcher` instead. +- +- EXAMPLE: +- -------- +- +- Run some scenarios only when runtime conditions are met: +- +- * Run scenario Alice only on Windows OS +- * Run scenario Bob only on MACOSX +- +- .. code-block:: gherkin +- +- # -- FILE: features/alice.feature +- # TAG SCHEMA: @only.with_{category}={current_value} +- Feature: +- +- @only.with_os=win32 +- Scenario: Alice (Run only on Windows) +- Given I do something +- ... +- +- @only.with_os=darwin +- Scenario: Bob (Run only on MACOSX) +- Given I do something else +- ... +- +- +- .. code-block:: python +- +- # -- FILE: features/environment.py +- from behave.tag_matcher import OnlyWithCategoryTagMatcher +- import sys +- +- # -- MATCHES TAGS: @only.with_{category}=* = @only.with_os=* +- active_tag_matcher = OnlyWithCategoryTagMatcher("os", sys.platform) +- +- def before_scenario(context, scenario): +- if active_tag_matcher.should_exclude_with(scenario.effective_tags): +- scenario.skip() #< LATE-EXCLUDE from run-set. +- """ +- tag_prefix = "only.with_" +- value_separator = "=" +- +- def __init__(self, category, value, tag_prefix=None, value_sep=None): +- warnings.warn("Use ActiveTagMatcher instead.", DeprecationWarning) +- super(OnlyWithCategoryTagMatcher, self).__init__() +- self.active_tag = self.make_category_tag(category, value, +- tag_prefix, value_sep) +- self.category_tag_prefix = self.make_category_tag(category, None, +- tag_prefix, value_sep) +- +- def should_exclude_with(self, tags): +- category_tags = self.select_category_tags(tags) +- if category_tags and self.active_tag not in category_tags: +- return True +- # -- OTHERWISE: feature/scenario with theses tags should run. +- return False +- +- def select_category_tags(self, tags): +- return [tag for tag in tags +- if tag.startswith(self.category_tag_prefix)] +- +- @classmethod +- def make_category_tag(cls, category, value=None, tag_prefix=None, +- value_sep=None): +- if tag_prefix is None: +- tag_prefix = cls.tag_prefix +- if value_sep is None: +- value_sep = cls.value_separator +- value = value or "" +- return "%s%s%s%s" % (tag_prefix, category, value_sep, value) +- +- +-class OnlyWithAnyCategoryTagMatcher(TagMatcher): +- """ +- Provides a tag matcher that matches any category that follows the +- "@only.with_" tag schema and determines if it should run or +- should be excluded from the run-set (at runtime). +- +- TAG SCHEMA: @only.with_{category}={value} +- +- .. seealso:: OnlyWithCategoryTagMatcher +- .. deprecated:: Use :class:`ActiveTagMatcher` instead. +- +- EXAMPLE: +- -------- +- +- Run some scenarios only when runtime conditions are met: +- +- * Run scenario Alice only on Windows OS +- * Run scenario Bob only with browser Chrome +- +- .. code-block:: gherkin +- +- # -- FILE: features/alice.feature +- # TAG SCHEMA: @only.with_{category}={current_value} +- Feature: +- +- @only.with_os=win32 +- Scenario: Alice (Run only on Windows) +- Given I do something +- ... +- +- @only.with_browser=chrome +- Scenario: Bob (Run only with Web-Browser Chrome) +- Given I do something else +- ... +- +- +- .. code-block:: python +- +- # -- FILE: features/environment.py +- from behave.tag_matcher import OnlyWithAnyCategoryTagMatcher +- import sys +- +- # -- MATCHES ANY TAGS: @only.with_{category}={value} +- # NOTE: active_tag_value_provider provides current category values. +- active_tag_value_provider = { +- "browser": os.environ.get("BEHAVE_BROWSER", "chrome"), +- "os": sys.platform, +- } +- active_tag_matcher = OnlyWithAnyCategoryTagMatcher(active_tag_value_provider) +- +- def before_scenario(context, scenario): +- if active_tag_matcher.should_exclude_with(scenario.effective_tags): +- scenario.skip() #< LATE-EXCLUDE from run-set. +- """ +- +- def __init__(self, value_provider, tag_prefix=None, value_sep=None): +- warnings.warn("Use ActiveTagMatcher instead.", DeprecationWarning) +- super(OnlyWithAnyCategoryTagMatcher, self).__init__() +- if value_sep is None: +- value_sep = OnlyWithCategoryTagMatcher.value_separator +- self.value_provider = value_provider +- self.tag_prefix = tag_prefix or OnlyWithCategoryTagMatcher.tag_prefix +- self.value_separator = value_sep +- +- def should_exclude_with(self, tags): +- exclude_decision_map = {} +- for category_tag in self.select_category_tags(tags): +- category, value = self.parse_category_tag(category_tag) +- active_value = self.value_provider.get(category, None) +- if active_value is None: +- # -- CASE: Unknown category, ignore it. +- continue +- elif active_value == value: +- # -- CASE: Active category value selected, decision should run. +- exclude_decision_map[category] = False +- else: +- # -- CASE: Inactive category value selected, may exclude it. +- if category not in exclude_decision_map: +- exclude_decision_map[category] = True +- return any(exclude_decision_map.values()) +- +- def select_category_tags(self, tags): +- return [tag for tag in tags +- if tag.startswith(self.tag_prefix)] +- +- def parse_category_tag(self, tag): +- assert tag and tag.startswith(self.tag_prefix) +- category_value = tag[len(self.tag_prefix):] +- if self.value_separator in category_value: +- category, value = category_value.split(self.value_separator, 1) +- else: +- # -- OOPS: TAG SCHEMA FORMAT MISMATCH +- category = category_value +- value = None +- return category, value +diff --git a/tests.attic/__init__.py b/tests.attic/__init__.py +new file mode 100644 +index 0000000..e69de29 +diff --git a/tests.attic/unit/__init__.py b/tests.attic/unit/__init__.py +new file mode 100644 +index 0000000..e69de29 +diff --git a/tests.attic/unit/test_tag_matcher.py b/tests.attic/unit/test_tag_matcher.py +new file mode 100644 +index 0000000..d767fa7 +--- /dev/null ++++ b/tests.attic/unit/test_tag_matcher.py +@@ -0,0 +1,280 @@ ++# ----------------------------------------------------------------------------- ++# PROTOTYPING CLASSES (deprecating) -- Should no longer be used. ++# ----------------------------------------------------------------------------- ++ ++import warnings ++from unittest import TestCase ++from behave.attic.tag_matcher import \ ++ OnlyWithCategoryTagMatcher, OnlyWithAnyCategoryTagMatcher ++ ++ ++class TestOnlyWithCategoryTagMatcher(TestCase): ++ TagMatcher = OnlyWithCategoryTagMatcher ++ ++ def setUp(self): ++ category = "xxx" ++ with warnings.catch_warnings(): ++ warnings.simplefilter("ignore", DeprecationWarning) ++ self.tag_matcher = OnlyWithCategoryTagMatcher(category, "alice") ++ self.enabled_tag = self.TagMatcher.make_category_tag(category, "alice") ++ self.similar_tag = self.TagMatcher.make_category_tag(category, "alice2") ++ self.other_tag = self.TagMatcher.make_category_tag(category, "other") ++ self.category = category ++ ++ def test_should_exclude_with__returns_false_with_enabled_tag(self): ++ tags = [ self.enabled_tag ] ++ self.assertEqual(False, self.tag_matcher.should_exclude_with(tags)) ++ ++ def test_should_exclude_with__returns_false_with_enabled_tag_and_more(self): ++ test_patterns = [ ++ ([ self.enabled_tag, self.other_tag ], "case: first"), ++ ([ self.other_tag, self.enabled_tag ], "case: last"), ++ ([ "foo", self.enabled_tag, self.other_tag, "bar" ], "case: middle"), ++ ] ++ for tags, case in test_patterns: ++ self.assertEqual(False, self.tag_matcher.should_exclude_with(tags), ++ "%s: tags=%s" % (case, tags)) ++ ++ def test_should_exclude_with__returns_true_with_other_tag(self): ++ tags = [ self.other_tag ] ++ self.assertEqual(True, self.tag_matcher.should_exclude_with(tags)) ++ ++ def test_should_exclude_with__returns_true_with_other_tag_and_more(self): ++ test_patterns = [ ++ ([ self.other_tag, "foo" ], "case: first"), ++ ([ "foo", self.other_tag ], "case: last"), ++ ([ "foo", self.other_tag, "bar" ], "case: middle"), ++ ] ++ for tags, case in test_patterns: ++ self.assertEqual(True, self.tag_matcher.should_exclude_with(tags), ++ "%s: tags=%s" % (case, tags)) ++ ++ def test_should_exclude_with__returns_true_with_similar_tag(self): ++ tags = [ self.similar_tag ] ++ self.assertEqual(True, self.tag_matcher.should_exclude_with(tags)) ++ ++ def test_should_exclude_with__returns_true_with_similar_and_more(self): ++ test_patterns = [ ++ ([ self.similar_tag, "foo" ], "case: first"), ++ ([ "foo", self.similar_tag ], "case: last"), ++ ([ "foo", self.similar_tag, "bar" ], "case: middle"), ++ ] ++ for tags, case in test_patterns: ++ self.assertEqual(True, self.tag_matcher.should_exclude_with(tags), ++ "%s: tags=%s" % (case, tags)) ++ ++ def test_should_exclude_with__returns_false_without_category_tag(self): ++ test_patterns = [ ++ ([ ], "case: No tags"), ++ ([ "foo" ], "case: One tag"), ++ ([ "foo", "bar" ], "case: Two tags"), ++ ] ++ for tags, case in test_patterns: ++ self.assertEqual(False, self.tag_matcher.should_exclude_with(tags), ++ "%s: tags=%s" % (case, tags)) ++ ++ def test_should_run_with__negates_result_of_should_exclude_with(self): ++ test_patterns = [ ++ ([ ], "case: No tags"), ++ ([ "foo" ], "case: One non-category tag"), ++ ([ "foo", "bar" ], "case: Two non-category tags"), ++ ([ self.enabled_tag ], "case: enabled tag"), ++ ([ self.enabled_tag, self.other_tag ], "case: enabled and other tag"), ++ ([ self.enabled_tag, "foo" ], "case: enabled and foo tag"), ++ ([ self.other_tag ], "case: other tag"), ++ ([ self.other_tag, "foo" ], "case: other and foo tag"), ++ ([ self.similar_tag ], "case: similar tag"), ++ ([ "foo", self.similar_tag ], "case: foo and similar tag"), ++ ] ++ for tags, case in test_patterns: ++ result1 = self.tag_matcher.should_run_with(tags) ++ result2 = self.tag_matcher.should_exclude_with(tags) ++ self.assertEqual(result1, not result2, "%s: tags=%s" % (case, tags)) ++ self.assertEqual(not result1, result2, "%s: tags=%s" % (case, tags)) ++ ++ def test_make_category_tag__returns_category_tag_prefix_without_value(self): ++ category = "xxx" ++ tag1 = OnlyWithCategoryTagMatcher.make_category_tag(category) ++ tag2 = OnlyWithCategoryTagMatcher.make_category_tag(category, None) ++ tag3 = OnlyWithCategoryTagMatcher.make_category_tag(category, value=None) ++ self.assertEqual("only.with_xxx=", tag1) ++ self.assertEqual("only.with_xxx=", tag2) ++ self.assertEqual("only.with_xxx=", tag3) ++ self.assertTrue(tag1.startswith(OnlyWithCategoryTagMatcher.tag_prefix)) ++ ++ def test_make_category_tag__returns_category_tag_with_value(self): ++ category = "xxx" ++ tag1 = OnlyWithCategoryTagMatcher.make_category_tag(category, "alice") ++ tag2 = OnlyWithCategoryTagMatcher.make_category_tag(category, "bob") ++ self.assertEqual("only.with_xxx=alice", tag1) ++ self.assertEqual("only.with_xxx=bob", tag2) ++ ++ def test_make_category_tag__returns_category_tag_with_tag_prefix(self): ++ my_tag_prefix = "ONLY_WITH." ++ category = "xxx" ++ TagMatcher = OnlyWithCategoryTagMatcher ++ tag0 = TagMatcher.make_category_tag(category, tag_prefix=my_tag_prefix) ++ tag1 = TagMatcher.make_category_tag(category, "alice", my_tag_prefix) ++ tag2 = TagMatcher.make_category_tag(category, "bob", tag_prefix=my_tag_prefix) ++ self.assertEqual("ONLY_WITH.xxx=", tag0) ++ self.assertEqual("ONLY_WITH.xxx=alice", tag1) ++ self.assertEqual("ONLY_WITH.xxx=bob", tag2) ++ self.assertTrue(tag1.startswith(my_tag_prefix)) ++ ++ def test_ctor__with_tag_prefix(self): ++ tag_prefix = "ONLY_WITH." ++ tag_matcher = OnlyWithCategoryTagMatcher("xxx", "alice", tag_prefix) ++ ++ tags = ["foo", "ONLY_WITH.xxx=foo", "only.with_xxx=bar", "bar"] ++ actual_tags = tag_matcher.select_category_tags(tags) ++ self.assertEqual(["ONLY_WITH.xxx=foo"], actual_tags) ++ ++ ++class Traits4OnlyWithAnyCategoryTagMatcher(object): ++ """Test data for OnlyWithAnyCategoryTagMatcher.""" ++ ++ TagMatcher0 = OnlyWithCategoryTagMatcher ++ TagMatcher = OnlyWithAnyCategoryTagMatcher ++ category1_enabled_tag = TagMatcher0.make_category_tag("foo", "alice") ++ category1_similar_tag = TagMatcher0.make_category_tag("foo", "alice2") ++ category1_disabled_tag = TagMatcher0.make_category_tag("foo", "bob") ++ category2_enabled_tag = TagMatcher0.make_category_tag("bar", "BOB") ++ category2_similar_tag = TagMatcher0.make_category_tag("bar", "BOB2") ++ category2_disabled_tag = TagMatcher0.make_category_tag("bar", "CHARLY") ++ unknown_category_tag = TagMatcher0.make_category_tag("UNKNOWN", "one") ++ ++ ++class TestOnlyWithAnyCategoryTagMatcher(TestCase): ++ TagMatcher = OnlyWithAnyCategoryTagMatcher ++ traits = Traits4OnlyWithAnyCategoryTagMatcher ++ ++ def setUp(self): ++ value_provider = { ++ "foo": "alice", ++ "bar": "BOB", ++ } ++ with warnings.catch_warnings(): ++ warnings.simplefilter("ignore", DeprecationWarning) ++ self.tag_matcher = self.TagMatcher(value_provider) ++ ++ # def test_deprecating_warning_is_issued(self): ++ # value_provider = {"foo": "alice"} ++ # with warnings.catch_warnings(record=True) as recorder: ++ # warnings.simplefilter("always", DeprecationWarning) ++ # tag_matcher = OnlyWithAnyCategoryTagMatcher(value_provider) ++ # self.assertEqual(len(recorder), 1) ++ # last_warning = recorder[-1] ++ # assert issubclass(last_warning.category, DeprecationWarning) ++ # assert "deprecated" in str(last_warning.message) ++ ++ def test_should_exclude_with__returns_false_with_enabled_tag(self): ++ traits = self.traits ++ tags1 = [ traits.category1_enabled_tag ] ++ tags2 = [ traits.category2_enabled_tag ] ++ self.assertEqual(False, self.tag_matcher.should_exclude_with(tags1)) ++ self.assertEqual(False, self.tag_matcher.should_exclude_with(tags2)) ++ ++ def test_should_exclude_with__returns_false_with_enabled_tag_and_more(self): ++ traits = self.traits ++ test_patterns = [ ++ ([ traits.category1_enabled_tag, traits.category1_disabled_tag ], "case: first"), ++ ([ traits.category1_disabled_tag, traits.category1_enabled_tag ], "case: last"), ++ ([ "foo", traits.category1_enabled_tag, traits.category1_disabled_tag, "bar" ], "case: middle"), ++ ] ++ for tags, case in test_patterns: ++ self.assertEqual(False, self.tag_matcher.should_exclude_with(tags), ++ "%s: tags=%s" % (case, tags)) ++ ++ def test_should_exclude_with__returns_true_with_other_tag(self): ++ traits = self.traits ++ tags = [ traits.category1_disabled_tag ] ++ self.assertEqual(True, self.tag_matcher.should_exclude_with(tags)) ++ ++ def test_should_exclude_with__returns_true_with_other_tag_and_more(self): ++ traits = self.traits ++ test_patterns = [ ++ ([ traits.category1_disabled_tag, "foo" ], "case: first"), ++ ([ "foo", traits.category1_disabled_tag ], "case: last"), ++ ([ "foo", traits.category1_disabled_tag, "bar" ], "case: middle"), ++ ] ++ for tags, case in test_patterns: ++ self.assertEqual(True, self.tag_matcher.should_exclude_with(tags), ++ "%s: tags=%s" % (case, tags)) ++ ++ def test_should_exclude_with__returns_true_with_similar_tag(self): ++ traits = self.traits ++ tags = [ traits.category1_similar_tag ] ++ self.assertEqual(True, self.tag_matcher.should_exclude_with(tags)) ++ ++ def test_should_exclude_with__returns_true_with_similar_and_more(self): ++ traits = self.traits ++ test_patterns = [ ++ ([ traits.category1_similar_tag, "foo" ], "case: first"), ++ ([ "foo", traits.category1_similar_tag ], "case: last"), ++ ([ "foo", traits.category1_similar_tag, "bar" ], "case: middle"), ++ ] ++ for tags, case in test_patterns: ++ self.assertEqual(True, self.tag_matcher.should_exclude_with(tags), ++ "%s: tags=%s" % (case, tags)) ++ ++ def test_should_exclude_with__returns_false_without_category_tag(self): ++ test_patterns = [ ++ ([ ], "case: No tags"), ++ ([ "foo" ], "case: One tag"), ++ ([ "foo", "bar" ], "case: Two tags"), ++ ] ++ for tags, case in test_patterns: ++ self.assertEqual(False, self.tag_matcher.should_exclude_with(tags), ++ "%s: tags=%s" % (case, tags)) ++ ++ def test_should_exclude_with__returns_false_with_unknown_category_tag(self): ++ """Tags from unknown categories, not supported by value_provider, ++ should not be excluded. ++ """ ++ traits = self.traits ++ tags = [ traits.unknown_category_tag ] ++ self.assertEqual("only.with_UNKNOWN=one", traits.unknown_category_tag) ++ self.assertEqual(None, self.tag_matcher.value_provider.get("UNKNOWN")) ++ self.assertEqual(False, self.tag_matcher.should_exclude_with(tags)) ++ ++ def test_should_exclude_with__combinations_of_2_categories(self): ++ traits = self.traits ++ test_patterns = [ ++ ("case 00: 2 disabled category tags", True, ++ [ traits.category1_disabled_tag, traits.category2_disabled_tag]), ++ ("case 01: disabled and enabled category tags", True, ++ [ traits.category1_disabled_tag, traits.category2_enabled_tag]), ++ ("case 10: enabled and disabled category tags", True, ++ [ traits.category1_enabled_tag, traits.category2_disabled_tag]), ++ ("case 11: 2 enabled category tags", False, # -- SHOULD-RUN ++ [ traits.category1_enabled_tag, traits.category2_enabled_tag]), ++ # -- SPECIAL CASE: With unknown category ++ ("case 0x: disabled and unknown category tags", True, ++ [ traits.category1_disabled_tag, traits.unknown_category_tag]), ++ ("case 1x: enabled and unknown category tags", False, # SHOULD-RUN ++ [ traits.category1_enabled_tag, traits.unknown_category_tag]), ++ ] ++ for case, expected, tags in test_patterns: ++ actual_result = self.tag_matcher.should_exclude_with(tags) ++ self.assertEqual(expected, actual_result, ++ "%s: tags=%s" % (case, tags)) ++ ++ def test_should_run_with__negates_result_of_should_exclude_with(self): ++ traits = self.traits ++ test_patterns = [ ++ ([ ], "case: No tags"), ++ ([ "foo" ], "case: One non-category tag"), ++ ([ "foo", "bar" ], "case: Two non-category tags"), ++ ([ traits.category1_enabled_tag ], "case: enabled tag"), ++ ([ traits.category1_enabled_tag, traits.category1_disabled_tag ], "case: enabled and other tag"), ++ ([ traits.category1_enabled_tag, "foo" ], "case: enabled and foo tag"), ++ ([ traits.category1_disabled_tag ], "case: other tag"), ++ ([ traits.category1_disabled_tag, "foo" ], "case: other and foo tag"), ++ ([ traits.category1_similar_tag ], "case: similar tag"), ++ ([ "foo", traits.category1_similar_tag ], "case: foo and similar tag"), ++ ] ++ for tags, case in test_patterns: ++ result1 = self.tag_matcher.should_run_with(tags) ++ result2 = self.tag_matcher.should_exclude_with(tags) ++ self.assertEqual(result1, not result2, "%s: tags=%s" % (case, tags)) ++ self.assertEqual(not result1, result2, "%s: tags=%s" % (case, tags)) +diff --git a/tests/unit/test_tag_matcher.py b/tests/unit/test_tag_matcher.py +index c5d2266..a04c1d4 100644 +--- a/tests/unit/test_tag_matcher.py ++++ b/tests/unit/test_tag_matcher.py +@@ -8,12 +8,13 @@ Unit tests for active tag-matcher (mod:`behave.tag_matcher`). + """ + + from __future__ import absolute_import +-from behave.tag_matcher import * + from mock import Mock + from unittest import TestCase + import warnings + import pytest + ++from behave.tag_matcher import * ++ + + class Traits4ActiveTagMatcher(object): + TagMatcher = ActiveTagMatcher +@@ -460,279 +461,3 @@ class TestCompositeTagMatcher(TestCase): + actual_true_count = self.count_tag_matcher_with_result( + self.ctag_matcher.tag_matchers, tags, True) + self.assertEqual(0, actual_true_count) +- +- +-# ----------------------------------------------------------------------------- +-# PROTOTYPING CLASSES (deprecating) +-# ----------------------------------------------------------------------------- +-class TestOnlyWithCategoryTagMatcher(TestCase): +- TagMatcher = OnlyWithCategoryTagMatcher +- +- def setUp(self): +- category = "xxx" +- with warnings.catch_warnings(): +- warnings.simplefilter("ignore", DeprecationWarning) +- self.tag_matcher = OnlyWithCategoryTagMatcher(category, "alice") +- self.enabled_tag = self.TagMatcher.make_category_tag(category, "alice") +- self.similar_tag = self.TagMatcher.make_category_tag(category, "alice2") +- self.other_tag = self.TagMatcher.make_category_tag(category, "other") +- self.category = category +- +- def test_should_exclude_with__returns_false_with_enabled_tag(self): +- tags = [ self.enabled_tag ] +- self.assertEqual(False, self.tag_matcher.should_exclude_with(tags)) +- +- def test_should_exclude_with__returns_false_with_enabled_tag_and_more(self): +- test_patterns = [ +- ([ self.enabled_tag, self.other_tag ], "case: first"), +- ([ self.other_tag, self.enabled_tag ], "case: last"), +- ([ "foo", self.enabled_tag, self.other_tag, "bar" ], "case: middle"), +- ] +- for tags, case in test_patterns: +- self.assertEqual(False, self.tag_matcher.should_exclude_with(tags), +- "%s: tags=%s" % (case, tags)) +- +- def test_should_exclude_with__returns_true_with_other_tag(self): +- tags = [ self.other_tag ] +- self.assertEqual(True, self.tag_matcher.should_exclude_with(tags)) +- +- def test_should_exclude_with__returns_true_with_other_tag_and_more(self): +- test_patterns = [ +- ([ self.other_tag, "foo" ], "case: first"), +- ([ "foo", self.other_tag ], "case: last"), +- ([ "foo", self.other_tag, "bar" ], "case: middle"), +- ] +- for tags, case in test_patterns: +- self.assertEqual(True, self.tag_matcher.should_exclude_with(tags), +- "%s: tags=%s" % (case, tags)) +- +- def test_should_exclude_with__returns_true_with_similar_tag(self): +- tags = [ self.similar_tag ] +- self.assertEqual(True, self.tag_matcher.should_exclude_with(tags)) +- +- def test_should_exclude_with__returns_true_with_similar_and_more(self): +- test_patterns = [ +- ([ self.similar_tag, "foo" ], "case: first"), +- ([ "foo", self.similar_tag ], "case: last"), +- ([ "foo", self.similar_tag, "bar" ], "case: middle"), +- ] +- for tags, case in test_patterns: +- self.assertEqual(True, self.tag_matcher.should_exclude_with(tags), +- "%s: tags=%s" % (case, tags)) +- +- def test_should_exclude_with__returns_false_without_category_tag(self): +- test_patterns = [ +- ([ ], "case: No tags"), +- ([ "foo" ], "case: One tag"), +- ([ "foo", "bar" ], "case: Two tags"), +- ] +- for tags, case in test_patterns: +- self.assertEqual(False, self.tag_matcher.should_exclude_with(tags), +- "%s: tags=%s" % (case, tags)) +- +- def test_should_run_with__negates_result_of_should_exclude_with(self): +- test_patterns = [ +- ([ ], "case: No tags"), +- ([ "foo" ], "case: One non-category tag"), +- ([ "foo", "bar" ], "case: Two non-category tags"), +- ([ self.enabled_tag ], "case: enabled tag"), +- ([ self.enabled_tag, self.other_tag ], "case: enabled and other tag"), +- ([ self.enabled_tag, "foo" ], "case: enabled and foo tag"), +- ([ self.other_tag ], "case: other tag"), +- ([ self.other_tag, "foo" ], "case: other and foo tag"), +- ([ self.similar_tag ], "case: similar tag"), +- ([ "foo", self.similar_tag ], "case: foo and similar tag"), +- ] +- for tags, case in test_patterns: +- result1 = self.tag_matcher.should_run_with(tags) +- result2 = self.tag_matcher.should_exclude_with(tags) +- self.assertEqual(result1, not result2, "%s: tags=%s" % (case, tags)) +- self.assertEqual(not result1, result2, "%s: tags=%s" % (case, tags)) +- +- def test_make_category_tag__returns_category_tag_prefix_without_value(self): +- category = "xxx" +- tag1 = OnlyWithCategoryTagMatcher.make_category_tag(category) +- tag2 = OnlyWithCategoryTagMatcher.make_category_tag(category, None) +- tag3 = OnlyWithCategoryTagMatcher.make_category_tag(category, value=None) +- self.assertEqual("only.with_xxx=", tag1) +- self.assertEqual("only.with_xxx=", tag2) +- self.assertEqual("only.with_xxx=", tag3) +- self.assertTrue(tag1.startswith(OnlyWithCategoryTagMatcher.tag_prefix)) +- +- def test_make_category_tag__returns_category_tag_with_value(self): +- category = "xxx" +- tag1 = OnlyWithCategoryTagMatcher.make_category_tag(category, "alice") +- tag2 = OnlyWithCategoryTagMatcher.make_category_tag(category, "bob") +- self.assertEqual("only.with_xxx=alice", tag1) +- self.assertEqual("only.with_xxx=bob", tag2) +- +- def test_make_category_tag__returns_category_tag_with_tag_prefix(self): +- my_tag_prefix = "ONLY_WITH." +- category = "xxx" +- TagMatcher = OnlyWithCategoryTagMatcher +- tag0 = TagMatcher.make_category_tag(category, tag_prefix=my_tag_prefix) +- tag1 = TagMatcher.make_category_tag(category, "alice", my_tag_prefix) +- tag2 = TagMatcher.make_category_tag(category, "bob", tag_prefix=my_tag_prefix) +- self.assertEqual("ONLY_WITH.xxx=", tag0) +- self.assertEqual("ONLY_WITH.xxx=alice", tag1) +- self.assertEqual("ONLY_WITH.xxx=bob", tag2) +- self.assertTrue(tag1.startswith(my_tag_prefix)) +- +- def test_ctor__with_tag_prefix(self): +- tag_prefix = "ONLY_WITH." +- tag_matcher = OnlyWithCategoryTagMatcher("xxx", "alice", tag_prefix) +- +- tags = ["foo", "ONLY_WITH.xxx=foo", "only.with_xxx=bar", "bar"] +- actual_tags = tag_matcher.select_category_tags(tags) +- self.assertEqual(["ONLY_WITH.xxx=foo"], actual_tags) +- +- +-class Traits4OnlyWithAnyCategoryTagMatcher(object): +- """Test data for OnlyWithAnyCategoryTagMatcher.""" +- +- TagMatcher0 = OnlyWithCategoryTagMatcher +- TagMatcher = OnlyWithAnyCategoryTagMatcher +- category1_enabled_tag = TagMatcher0.make_category_tag("foo", "alice") +- category1_similar_tag = TagMatcher0.make_category_tag("foo", "alice2") +- category1_disabled_tag = TagMatcher0.make_category_tag("foo", "bob") +- category2_enabled_tag = TagMatcher0.make_category_tag("bar", "BOB") +- category2_similar_tag = TagMatcher0.make_category_tag("bar", "BOB2") +- category2_disabled_tag = TagMatcher0.make_category_tag("bar", "CHARLY") +- unknown_category_tag = TagMatcher0.make_category_tag("UNKNOWN", "one") +- +- +-class TestOnlyWithAnyCategoryTagMatcher(TestCase): +- TagMatcher = OnlyWithAnyCategoryTagMatcher +- traits = Traits4OnlyWithAnyCategoryTagMatcher +- +- def setUp(self): +- value_provider = { +- "foo": "alice", +- "bar": "BOB", +- } +- with warnings.catch_warnings(): +- warnings.simplefilter("ignore", DeprecationWarning) +- self.tag_matcher = self.TagMatcher(value_provider) +- +- # def test_deprecating_warning_is_issued(self): +- # value_provider = {"foo": "alice"} +- # with warnings.catch_warnings(record=True) as recorder: +- # warnings.simplefilter("always", DeprecationWarning) +- # tag_matcher = OnlyWithAnyCategoryTagMatcher(value_provider) +- # self.assertEqual(len(recorder), 1) +- # last_warning = recorder[-1] +- # assert issubclass(last_warning.category, DeprecationWarning) +- # assert "deprecated" in str(last_warning.message) +- +- def test_should_exclude_with__returns_false_with_enabled_tag(self): +- traits = self.traits +- tags1 = [ traits.category1_enabled_tag ] +- tags2 = [ traits.category2_enabled_tag ] +- self.assertEqual(False, self.tag_matcher.should_exclude_with(tags1)) +- self.assertEqual(False, self.tag_matcher.should_exclude_with(tags2)) +- +- def test_should_exclude_with__returns_false_with_enabled_tag_and_more(self): +- traits = self.traits +- test_patterns = [ +- ([ traits.category1_enabled_tag, traits.category1_disabled_tag ], "case: first"), +- ([ traits.category1_disabled_tag, traits.category1_enabled_tag ], "case: last"), +- ([ "foo", traits.category1_enabled_tag, traits.category1_disabled_tag, "bar" ], "case: middle"), +- ] +- for tags, case in test_patterns: +- self.assertEqual(False, self.tag_matcher.should_exclude_with(tags), +- "%s: tags=%s" % (case, tags)) +- +- def test_should_exclude_with__returns_true_with_other_tag(self): +- traits = self.traits +- tags = [ traits.category1_disabled_tag ] +- self.assertEqual(True, self.tag_matcher.should_exclude_with(tags)) +- +- def test_should_exclude_with__returns_true_with_other_tag_and_more(self): +- traits = self.traits +- test_patterns = [ +- ([ traits.category1_disabled_tag, "foo" ], "case: first"), +- ([ "foo", traits.category1_disabled_tag ], "case: last"), +- ([ "foo", traits.category1_disabled_tag, "bar" ], "case: middle"), +- ] +- for tags, case in test_patterns: +- self.assertEqual(True, self.tag_matcher.should_exclude_with(tags), +- "%s: tags=%s" % (case, tags)) +- +- def test_should_exclude_with__returns_true_with_similar_tag(self): +- traits = self.traits +- tags = [ traits.category1_similar_tag ] +- self.assertEqual(True, self.tag_matcher.should_exclude_with(tags)) +- +- def test_should_exclude_with__returns_true_with_similar_and_more(self): +- traits = self.traits +- test_patterns = [ +- ([ traits.category1_similar_tag, "foo" ], "case: first"), +- ([ "foo", traits.category1_similar_tag ], "case: last"), +- ([ "foo", traits.category1_similar_tag, "bar" ], "case: middle"), +- ] +- for tags, case in test_patterns: +- self.assertEqual(True, self.tag_matcher.should_exclude_with(tags), +- "%s: tags=%s" % (case, tags)) +- +- def test_should_exclude_with__returns_false_without_category_tag(self): +- test_patterns = [ +- ([ ], "case: No tags"), +- ([ "foo" ], "case: One tag"), +- ([ "foo", "bar" ], "case: Two tags"), +- ] +- for tags, case in test_patterns: +- self.assertEqual(False, self.tag_matcher.should_exclude_with(tags), +- "%s: tags=%s" % (case, tags)) +- +- def test_should_exclude_with__returns_false_with_unknown_category_tag(self): +- """Tags from unknown categories, not supported by value_provider, +- should not be excluded. +- """ +- traits = self.traits +- tags = [ traits.unknown_category_tag ] +- self.assertEqual("only.with_UNKNOWN=one", traits.unknown_category_tag) +- self.assertEqual(None, self.tag_matcher.value_provider.get("UNKNOWN")) +- self.assertEqual(False, self.tag_matcher.should_exclude_with(tags)) +- +- def test_should_exclude_with__combinations_of_2_categories(self): +- traits = self.traits +- test_patterns = [ +- ("case 00: 2 disabled category tags", True, +- [ traits.category1_disabled_tag, traits.category2_disabled_tag]), +- ("case 01: disabled and enabled category tags", True, +- [ traits.category1_disabled_tag, traits.category2_enabled_tag]), +- ("case 10: enabled and disabled category tags", True, +- [ traits.category1_enabled_tag, traits.category2_disabled_tag]), +- ("case 11: 2 enabled category tags", False, # -- SHOULD-RUN +- [ traits.category1_enabled_tag, traits.category2_enabled_tag]), +- # -- SPECIAL CASE: With unknown category +- ("case 0x: disabled and unknown category tags", True, +- [ traits.category1_disabled_tag, traits.unknown_category_tag]), +- ("case 1x: enabled and unknown category tags", False, # SHOULD-RUN +- [ traits.category1_enabled_tag, traits.unknown_category_tag]), +- ] +- for case, expected, tags in test_patterns: +- actual_result = self.tag_matcher.should_exclude_with(tags) +- self.assertEqual(expected, actual_result, +- "%s: tags=%s" % (case, tags)) +- +- def test_should_run_with__negates_result_of_should_exclude_with(self): +- traits = self.traits +- test_patterns = [ +- ([ ], "case: No tags"), +- ([ "foo" ], "case: One non-category tag"), +- ([ "foo", "bar" ], "case: Two non-category tags"), +- ([ traits.category1_enabled_tag ], "case: enabled tag"), +- ([ traits.category1_enabled_tag, traits.category1_disabled_tag ], "case: enabled and other tag"), +- ([ traits.category1_enabled_tag, "foo" ], "case: enabled and foo tag"), +- ([ traits.category1_disabled_tag ], "case: other tag"), +- ([ traits.category1_disabled_tag, "foo" ], "case: other and foo tag"), +- ([ traits.category1_similar_tag ], "case: similar tag"), +- ([ "foo", traits.category1_similar_tag ], "case: foo and similar tag"), +- ] +- for tags, case in test_patterns: +- result1 = self.tag_matcher.should_run_with(tags) +- result2 = self.tag_matcher.should_exclude_with(tags) +- self.assertEqual(result1, not result2, "%s: tags=%s" % (case, tags)) +- self.assertEqual(not result1, result2, "%s: tags=%s" % (case, tags)) +- diff --git a/meta-python/recipes-devtools/python/python3-behave/0027-Comment-tweaks.patch b/meta-python/recipes-devtools/python/python3-behave/0027-Comment-tweaks.patch new file mode 100644 index 000000000..c41068c96 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0027-Comment-tweaks.patch @@ -0,0 +1,30 @@ +From 362e778e5e57054b45099f1432abea181987d3c9 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sun, 9 Jun 2019 19:24:14 +0200 +Subject: [PATCH] Comment tweaks + +--- + behave/runner.py | 7 ++++++- + 1 file changed, 6 insertions(+), 1 deletion(-) + +diff --git a/behave/runner.py b/behave/runner.py +index f0f077a..f209cb0 100644 +--- a/behave/runner.py ++++ b/behave/runner.py +@@ -167,10 +167,15 @@ class Context(object): + self._record = {} + self._origin = {} + self._mode = self.BEHAVE ++ ++ # -- MODEL ENTITY REFERENCES/SUPPORT: + self.feature = None +- # -- RECHECK: If needed ++ # DISABLED: self.rule = None ++ # DISABLED: self.scenario = None + self.text = None + self.table = None ++ ++ # -- RUNTIME SUPPORT: + self.stdout_capture = None + self.stderr_capture = None + self.log_capture = None diff --git a/meta-python/recipes-devtools/python/python3-behave/0028-FIX-warnings-related-to-invalid-escapes-in-regex.patch b/meta-python/recipes-devtools/python/python3-behave/0028-FIX-warnings-related-to-invalid-escapes-in-regex.patch new file mode 100644 index 000000000..8f0ad3a82 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0028-FIX-warnings-related-to-invalid-escapes-in-regex.patch @@ -0,0 +1,79 @@ +From d892f81e3bd3e979d3677c39c3a9efc40131b452 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sun, 9 Jun 2019 19:32:10 +0200 +Subject: [PATCH] FIX warnings related to invalid escapes in regex. + +--- + behave4cmd0/command_shell_proc.py | 3 ++- + features/steps/behave_select_files_steps.py | 24 +++++++++++++-------- + 2 files changed, 17 insertions(+), 10 deletions(-) + +diff --git a/behave4cmd0/command_shell_proc.py b/behave4cmd0/command_shell_proc.py +index 07f1c84..03ca55a 100755 +--- a/behave4cmd0/command_shell_proc.py ++++ b/behave4cmd0/command_shell_proc.py +@@ -251,6 +251,7 @@ class BehaveWinCommandOutputProcessor(LineCommandOutputProcessor): + "No such file or directory: '(?P.*)'", + "[Errno 2] No such file or directory:"), # IOError + ExceptionWithPathNormalizer( +- '^\s*File "(?P.*)", line \d+, in ', ++ # WAS: '^\s*File "(?P.*)", line \d+, in ', ++ r'^\s*File "(?P.*)", line \d+, in ', + 'File "'), + ] +diff --git a/features/steps/behave_select_files_steps.py b/features/steps/behave_select_files_steps.py +index 431674e..0d2cd2e 100644 +--- a/features/steps/behave_select_files_steps.py ++++ b/features/steps/behave_select_files_steps.py +@@ -1,29 +1,34 @@ + # -*- coding: utf-8 -*- +-""" ++# DOCSTRING-NEEDS-REGEX-STRING-PREFIX: Due to example w/ wildcard pattern. ++r''' + Provides step definitions that test how the behave runner selects feature files. + + EXAMPLE: ++ ++.. code-block:: gherkin ++ + Given behave has the following feature fileset: +- ''' ++ """ + features/alice.feature + features/bob.feature + features/barbi.feature +- ''' ++ """ + When behave includes feature files with "features/a.*\.feature" + And behave excludes feature files with "features/b.*\.feature" + Then the following feature files are selected: +- ''' ++ """ + features/alice.feature +- ''' +-""" ++ """ ++''' + + from __future__ import absolute_import +-from behave import given, when, then +-from behave.runner_util import FeatureListParser +-from hamcrest import assert_that, equal_to + from copy import copy + import re + import six ++from hamcrest import assert_that, equal_to ++from behave import given, when, then ++from behave.runner_util import FeatureListParser ++ + + # ----------------------------------------------------------------------------- + # STEP UTILS: +@@ -47,6 +52,7 @@ class BasicBehaveRunner(object): + # ----------------------------------------------------------------------------- + # STEP DEFINITIONS: + # ----------------------------------------------------------------------------- ++# pylint: disable=invalid-name + @given('behave has the following feature fileset') + def step_given_behave_has_feature_fileset(context): + assert context.text is not None, "REQUIRE: text" diff --git a/meta-python/recipes-devtools/python/python3-behave/0029-Steps-catalog-should-not-break-configured-rerun-sett.patch b/meta-python/recipes-devtools/python/python3-behave/0029-Steps-catalog-should-not-break-configured-rerun-sett.patch new file mode 100644 index 000000000..e2d86ca99 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0029-Steps-catalog-should-not-break-configured-rerun-sett.patch @@ -0,0 +1,73 @@ +From 11c4ac862bdce901ef19864532ef6d17a85799cd Mon Sep 17 00:00:00 2001 +From: Edvin Linge +Date: Mon, 31 Jul 2017 17:05:18 +0200 +Subject: [PATCH] Steps-catalog should not break configured rerun settings + +--- + behave/configuration.py | 5 ++++- + features/formatter.rerun.feature | 17 +++++++++++++++++ + features/formatter.steps_catalog.feature | 13 +++++++++++++ + 3 files changed, 34 insertions(+), 1 deletion(-) + +diff --git a/behave/configuration.py b/behave/configuration.py +index 49b1dff..861f89f 100644 +--- a/behave/configuration.py ++++ b/behave/configuration.py +@@ -601,7 +601,10 @@ class Configuration(object): + if self.steps_catalog: + # -- SHOW STEP-CATALOG: As step summary. + self.default_format = "steps.catalog" +- self.format = ["steps.catalog"] ++ if self.format: ++ self.format.append("steps.catalog") ++ else: ++ self.format = ["steps.catalog"] + self.dry_run = True + self.summary = False + self.show_skipped = False +diff --git a/features/formatter.rerun.feature b/features/formatter.rerun.feature +index 1e645f1..b9d7e1b 100644 +--- a/features/formatter.rerun.feature ++++ b/features/formatter.rerun.feature +@@ -294,3 +294,20 @@ Feature: Rerun Formatter + features/bob.feature:13 + """ + And note that "the second RerunFormatter overwrites the output of the first one" ++ ++ @with.behave_configfile ++ Scenario: RerunFormatter with steps-catalog ++ Given a file named "behave.ini" with: ++ """ ++ [behave] ++ format = rerun ++ outfiles = rerun.txt ++ """ ++ When I run "behave --steps-catalog features/" ++ ++ Then it should pass with: ++ """ ++ Given a step passes ++ """ ++ And a file named "rerun.txt" does not exist ++ +diff --git a/features/formatter.steps_catalog.feature b/features/formatter.steps_catalog.feature +index 09591bf..936d7f4 100644 +--- a/features/formatter.steps_catalog.feature ++++ b/features/formatter.steps_catalog.feature +@@ -98,3 +98,16 @@ Feature: Steps Catalog Formatter + """ + But note that "the step definitions are ordered by step type" + And note that "'When I visit {person}' has no doc-string" ++ ++ ++ Scenario: Steps catalog formatter is used for output even when other formatter is specified ++ When I run "behave --steps-catalog -f plain features/" ++ Then it should pass with: ++ """ ++ Given {person} lives in {city} ++ Setup the data where a person lives and store in the database. ++ ++ :param person: Person's name (as string). ++ :param city: City where the person lives (as string). ++ """ ++ diff --git a/meta-python/recipes-devtools/python/python3-behave/0030-Add-feature-w-rules-and-failing-scenarios-enable-via.patch b/meta-python/recipes-devtools/python/python3-behave/0030-Add-feature-w-rules-and-failing-scenarios-enable-via.patch new file mode 100644 index 000000000..f050c9c85 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0030-Add-feature-w-rules-and-failing-scenarios-enable-via.patch @@ -0,0 +1,36 @@ +From 502382430f1bee8d0a9031fc70541488224a1029 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Mon, 1 Jul 2019 22:07:19 +0200 +Subject: [PATCH] Add feature w/ rules and failing scenarios (enable via + tags=fail or tags=xfail). + +--- + .../gherkin_v6/features/rule_fails.feature | 19 +++++++++++++++++++ + 1 file changed, 19 insertions(+) + create mode 100644 examples/gherkin_v6/features/rule_fails.feature + +diff --git a/examples/gherkin_v6/features/rule_fails.feature b/examples/gherkin_v6/features/rule_fails.feature +new file mode 100644 +index 0000000..7e3872e +--- /dev/null ++++ b/examples/gherkin_v6/features/rule_fails.feature +@@ -0,0 +1,19 @@ ++@fail ++Feature: With Rule(s) and Failing Scenario(s) ++ ++ HINT: Contains failing scenarios (by intention). ++ ++ @xfail ++ Scenario: F0 -- Fails ++ When some step fails ++ ++ Rule: Fails in Scenario F2 ++ ++ Scenario: F1 ++ Given a step passes ++ ++ @xfail ++ Scenario: F2 -- Fails ++ Given another step passes ++ When a step fails ++ Then another step passes diff --git a/meta-python/recipes-devtools/python/python3-behave/0031-examples-gherkin_v6-Tweak-ScenarioOutline-Examples-t.patch b/meta-python/recipes-devtools/python/python3-behave/0031-examples-gherkin_v6-Tweak-ScenarioOutline-Examples-t.patch new file mode 100644 index 000000000..6f71729f5 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0031-examples-gherkin_v6-Tweak-ScenarioOutline-Examples-t.patch @@ -0,0 +1,27 @@ +From dcbba864209e8784ecaa5a1cf80b346b39b2a5d6 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Mon, 1 Jul 2019 22:11:13 +0200 +Subject: [PATCH] examples/gherkin_v6: Tweak ScenarioOutline Examples titles. + +--- + examples/gherkin_v6/features/rule_2.feature | 7 ++++++- + 1 file changed, 6 insertions(+), 1 deletion(-) + +diff --git a/examples/gherkin_v6/features/rule_2.feature b/examples/gherkin_v6/features/rule_2.feature +index a802e19..8b8bb04 100644 +--- a/examples/gherkin_v6/features/rule_2.feature ++++ b/examples/gherkin_v6/features/rule_2.feature +@@ -36,7 +36,12 @@ Feature: Gherkin v6 Example -- with Rules + Scenario Template: R3.Scenario + Given a person named "" + +- Examples: ++ Examples: R3.E1 + | name | + | Alice | + | Bob | ++ ++ Examples: R3.E2 ++ | name | ++ | Charly | ++ | Doro | diff --git a/meta-python/recipes-devtools/python/python3-behave/0032-Add-info-on-merged-pull-588.patch b/meta-python/recipes-devtools/python/python3-behave/0032-Add-info-on-merged-pull-588.patch new file mode 100644 index 000000000..4d44fea08 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0032-Add-info-on-merged-pull-588.patch @@ -0,0 +1,21 @@ +From 9c9bb1e6320a6ffbac0b171d9cf98cbb311104eb Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Mon, 1 Jul 2019 08:06:43 +0200 +Subject: [PATCH] Add info on merged pull #588 + +--- + CHANGES.rst | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/CHANGES.rst b/CHANGES.rst +index a91e22a..3d805b3 100644 +--- a/CHANGES.rst ++++ b/CHANGES.rst +@@ -36,6 +36,7 @@ PARTIALLY FIXED: + + FIXED: + ++* pull #588: Steps-catalog argument should not break configured rerun settings (provided by: Lego3) + * issue #725: Scenario Outline description lines seem to be ignored (submitted by: nizwiz) + * issue #713: Background section doesn't support description (provided by: dgou) + * pull #657: Allow async steps with timeouts to fail when they raise exceptions (provided by: ALSchwalm) diff --git a/meta-python/recipes-devtools/python/python3-behave/0033-Tweak-tests-required-by-pytest-5.0.patch b/meta-python/recipes-devtools/python/python3-behave/0033-Tweak-tests-required-by-pytest-5.0.patch new file mode 100644 index 000000000..1fdda3a7d --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0033-Tweak-tests-required-by-pytest-5.0.patch @@ -0,0 +1,97 @@ +From 8fe1bc305aeea5d86de1b3c5dfcd2e7984fc5145 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Fri, 5 Jul 2019 08:27:44 +0200 +Subject: [PATCH] Tweak tests, required by pytest >= 5.0. With pytest.raises + use str(e.value) instead of str(e) in some cases. + +--- + tests/issues/test_issue0458.py | 2 +- + tests/unit/test_context_cleanups.py | 2 +- + tests/unit/test_textutil.py | 24 +++++++++++++++--------- + 3 files changed, 17 insertions(+), 11 deletions(-) + +diff --git a/tests/issues/test_issue0458.py b/tests/issues/test_issue0458.py +index 1853ad6..f66f6d3 100644 +--- a/tests/issues/test_issue0458.py ++++ b/tests/issues/test_issue0458.py +@@ -48,7 +48,7 @@ def test_issue(exception_class, message): + raise_exception(exception_class, message) + + # -- SHOULD NOT RAISE EXCEPTION HERE: +- text = _text(e) ++ text = _text(e.value) + # -- DIAGNOSTICS: + print(u"text"+ text) + print(u"exception: %s" % e) +diff --git a/tests/unit/test_context_cleanups.py b/tests/unit/test_context_cleanups.py +index b0e8ae6..bf0ab50 100644 +--- a/tests/unit/test_context_cleanups.py ++++ b/tests/unit/test_context_cleanups.py +@@ -153,7 +153,7 @@ class TestContextCleanup(object): + with pytest.raises(AssertionError) as e: + with scoped_context_layer(context): + context.add_cleanup(non_callable) +- assert "REQUIRES: callable(cleanup_func)" in str(e) ++ assert "REQUIRES: callable(cleanup_func)" in str(e.value) + + def test_on_cleanup_error__prints_error_by_default(self, capsys): + def bad_cleanup_func(): +diff --git a/tests/unit/test_textutil.py b/tests/unit/test_textutil.py +index f7f642c..e05e9ad 100644 +--- a/tests/unit/test_textutil.py ++++ b/tests/unit/test_textutil.py +@@ -214,9 +214,11 @@ class TestObjectToTextConversion(object): + with pytest.raises(AssertionError) as e: + assert False, message + +- text2 = text(e) +- expected = u"AssertionError: %s" % message +- assert text2.endswith(expected) ++ # -- FOR: pytest < 5.0 ++ # expected = u"AssertionError: %s" % message ++ text2 = text(e.value) ++ assert u"AssertionError" in text(e) ++ assert message in text2, "OOPS: text=%r" % text2 + + @requires_python2 + @pytest.mark.parametrize("message", [ +@@ -236,10 +238,11 @@ class TestObjectToTextConversion(object): + assert expected_decode_error in str(uni_error) + assert False, bytes_message.decode(self.ENCODING) + ++ # -- FOR: pytest < 5.0 ++ # expected = u"AssertionError: %s" % message + print("decode_error_occured(ascii)=%s" % decode_error_occured) +- text2 = text(e) +- expected = u"AssertionError: %s" % message +- assert text2.endswith(expected) ++ text2 = text(e.value) ++ assert message in text2, "OOPS: text=%r" % text2 + + @pytest.mark.parametrize("exception_class, message", [ + (AssertionError, u"Ärgernis"), +@@ -251,10 +254,13 @@ class TestObjectToTextConversion(object): + with pytest.raises(exception_class) as e: + raise exception_class(message) + +- text2 = text(e) ++ # -- FOR: pytest < 5.0 ++ # expected = u"AssertionError: %s" % message ++ text2 = text(e.value) + expected = u"%s: %s" % (exception_class.__name__, message) + assert isinstance(text2, six.text_type) +- assert text2.endswith(expected) ++ assert exception_class.__name__ in str(e) ++ assert message in text2, "OOPS: text=%r" % text2 + + @requires_python2 + @pytest.mark.parametrize("exception_class, message", [ +@@ -268,7 +274,7 @@ class TestObjectToTextConversion(object): + with pytest.raises(exception_class) as e: + raise exception_class(bytes_message) + +- text2 = text(e) ++ text2 = text(e.value) + unicode_message = bytes_message.decode(self.ENCODING) + expected = u"%s: %s" % (exception_class.__name__, unicode_message) + assert isinstance(text2, six.text_type) diff --git a/meta-python/recipes-devtools/python/python3-behave/0034-CLEANUP-Use-pytest-instead-of-nose-to-remove-nose.im.patch b/meta-python/recipes-devtools/python/python3-behave/0034-CLEANUP-Use-pytest-instead-of-nose-to-remove-nose.im.patch new file mode 100644 index 000000000..7a47c7fe7 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0034-CLEANUP-Use-pytest-instead-of-nose-to-remove-nose.im.patch @@ -0,0 +1,180 @@ +From dce0e40c9689a8d4fe71c9b2e8e4decd72dcf574 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Fri, 5 Jul 2019 08:45:51 +0200 +Subject: [PATCH] CLEANUP: Use pytest instead of nose to remove nose.importer + DeprecationWarning. + +--- + tests/unit/test_importer.py | 163 ++++++++++++++++++++++++++++++++++++ + 1 file changed, 163 insertions(+) + create mode 100644 tests/unit/test_importer.py + +diff --git a/tests/unit/test_importer.py b/tests/unit/test_importer.py +new file mode 100644 +index 0000000..f3f4e2c +--- /dev/null ++++ b/tests/unit/test_importer.py +@@ -0,0 +1,163 @@ ++# -*- coding: utf-8 -*- ++""" ++Tests for behave.importing. ++The module provides a lazy-loading/importing mechanism. ++""" ++ ++from __future__ import absolute_import ++import pytest ++from behave.importer import LazyObject, LazyDict, load_module, parse_scoped_name ++from behave.formatter.base import Formatter ++import sys ++import types ++# import unittest ++ ++ ++class TestTheory(object): ++ """Marker for test-theory classes as syntactic sugar.""" ++ pass ++ ++ ++class ImportModuleTheory(TestTheory): ++ """ ++ Provides a test theory for importing modules. ++ """ ++ ++ @classmethod ++ def ensure_module_is_not_imported(cls, module_name): ++ if module_name in sys.modules: ++ del sys.modules[module_name] ++ cls.assert_module_is_not_imported(module_name) ++ ++ @staticmethod ++ def assert_module_is_imported(module_name): ++ module = sys.modules.get(module_name, None) ++ assert module_name in sys.modules ++ assert module is not None ++ ++ @staticmethod ++ def assert_module_is_not_imported(module_name): ++ assert module_name not in sys.modules ++ ++ @staticmethod ++ def assert_module_with_name(module, name): ++ assert isinstance(module, types.ModuleType) ++ assert module.__name__ == name ++ ++ ++class TestLoadModule(object): ++ theory = ImportModuleTheory ++ ++ def test_load_module__should_fail_for_unknown_module(self): ++ with pytest.raises(ImportError) as e: ++ load_module("__unknown_module__") ++ # OLD: assert_raises(ImportError, load_module, "__unknown_module__") ++ ++ def test_load_module__should_succeed_for_already_imported_module(self): ++ module_name = "behave.importer" ++ self.theory.assert_module_is_imported(module_name) ++ ++ module = load_module(module_name) ++ self.theory.assert_module_with_name(module, module_name) ++ self.theory.assert_module_is_imported(module_name) ++ ++ def test_load_module__should_succeed_for_existing_module(self): ++ module_name = "test._importer_candidate" ++ self.theory.ensure_module_is_not_imported(module_name) ++ ++ module = load_module(module_name) ++ self.theory.assert_module_with_name(module, module_name) ++ self.theory.assert_module_is_imported(module_name) ++ ++ ++class TestLazyObject(object): ++ ++ def test_get__should_succeed_for_known_object(self): ++ lazy = LazyObject("behave.importer", "LazyObject") ++ value = lazy.get() ++ assert value is LazyObject ++ ++ lazy2 = LazyObject("behave.importer:LazyObject") ++ value2 = lazy2.get() ++ assert value2 is LazyObject ++ ++ lazy3 = LazyObject("behave.formatter.steps", "StepsFormatter") ++ value3 = lazy3.get() ++ assert issubclass(value3, Formatter) ++ ++ def test_get__should_fail_for_unknown_module(self): ++ lazy = LazyObject("__unknown_module__", "xxx") ++ with pytest.raises(ImportError): ++ lazy.get() ++ ++ def test_get__should_fail_for_unknown_object_in_module(self): ++ lazy = LazyObject("test._importer_candidate", "xxx") ++ with pytest.raises(ImportError): ++ lazy.get() ++ ++ ++class LazyDictTheory(TestTheory): ++ ++ @staticmethod ++ def safe_getitem(data, key): ++ return dict.__getitem__(data, key) ++ ++ @classmethod ++ def assert_item_is_lazy(cls, data, key): ++ value = cls.safe_getitem(data, key) ++ cls.assert_is_lazy_object(value) ++ ++ @classmethod ++ def assert_item_is_not_lazy(cls, data, key): ++ value = cls.safe_getitem(data, key) ++ cls.assert_is_not_lazy_object(value) ++ ++ @staticmethod ++ def assert_is_lazy_object(obj): ++ assert isinstance(obj, LazyObject) ++ ++ @staticmethod ++ def assert_is_not_lazy_object(obj): ++ assert not isinstance(obj, LazyObject) ++ ++ ++class TestLazyDict(object): ++ theory = LazyDictTheory ++ ++ def test_unknown_item_access__should_raise_keyerror(self): ++ lazy_dict = LazyDict({"alice": 42}) ++ item_access = lambda key: lazy_dict[key] ++ with pytest.raises(KeyError): ++ item_access("unknown") ++ ++ def test_plain_item_access__should_succeed(self): ++ theory = LazyDictTheory ++ lazy_dict = LazyDict({"alice": 42}) ++ theory.assert_item_is_not_lazy(lazy_dict, "alice") ++ ++ value = lazy_dict["alice"] ++ assert value == 42 ++ ++ def test_lazy_item_access__should_load_object(self): ++ ImportModuleTheory.ensure_module_is_not_imported("inspect") ++ lazy_dict = LazyDict({"alice": LazyObject("inspect:ismodule")}) ++ self.theory.assert_item_is_lazy(lazy_dict, "alice") ++ self.theory.assert_item_is_lazy(lazy_dict, "alice") ++ ++ value = lazy_dict["alice"] ++ self.theory.assert_is_not_lazy_object(value) ++ self.theory.assert_item_is_not_lazy(lazy_dict, "alice") ++ ++ def test_lazy_item_access__should_fail_with_unknown_module(self): ++ lazy_dict = LazyDict({"bob": LazyObject("__unknown_module__", "xxx")}) ++ item_access = lambda key: lazy_dict[key] ++ with pytest.raises(ImportError): ++ item_access("bob") ++ ++ def test_lazy_item_access__should_fail_with_unknown_object(self): ++ lazy_dict = LazyDict({ ++ "bob": LazyObject("behave.importer", "XUnknown") ++ }) ++ item_access = lambda key: lazy_dict[key] ++ with pytest.raises(ImportError): ++ item_access("bob") diff --git a/meta-python/recipes-devtools/python/python3-behave/0035-REMOVE-DEPENDENCY-nose-to-avoid-nose.importer-warnin.patch b/meta-python/recipes-devtools/python/python3-behave/0035-REMOVE-DEPENDENCY-nose-to-avoid-nose.importer-warnin.patch new file mode 100644 index 000000000..33738459d --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0035-REMOVE-DEPENDENCY-nose-to-avoid-nose.importer-warnin.patch @@ -0,0 +1,1815 @@ +From 65b17ccbea1a17b26e6191a1d7336088d0dd4181 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sat, 6 Jul 2019 09:10:52 +0200 +Subject: [PATCH] REMOVE-DEPENDENCY: nose (to avoid nose.importer warnings) + +Replace nose test-framework functionality w/ pytest. +Move test/*.py => tests/unit/*.py +--- + MANIFEST.in | 2 - + conftest.py | 13 ++ + invoke.yaml | 1 + + py.requirements/ci.tox.txt | 9 + + py.requirements/ci.travis.txt | 1 - + py.requirements/develop.txt | 1 - + py.requirements/testing.txt | 3 +- + pytest.ini | 7 +- + setup.py | 8 +- + tasks/test.py | 2 +- + test/__init__.py | 0 + test/test_importer.py | 151 ------------- + {test => tests/unit}/_importer_candidate.py | 0 + tests/unit/reporter/test_summary.py | 2 - + .../test_tag_expression_v1_part1.py | 17 +- + .../test_tag_expression_v1_part2.py | 18 +- + {test => tests/unit}/test_ansi_escapes.py | 14 +- + {test => tests/unit}/test_configuration.py | 75 +++--- + {test => tests/unit}/test_formatter.py | 15 +- + .../unit}/test_formatter_progress.py | 7 + + {test => tests/unit}/test_formatter_rerun.py | 12 +- + {test => tests/unit}/test_formatter_tags.py | 9 + + tests/unit/test_importer.py | 2 +- + {test => tests/unit}/test_log_capture.py | 9 +- + {test => tests/unit}/test_matchers.py | 24 +- + tests/unit/test_parser.py | 1 - + {test => tests/unit}/test_runner.py | 213 ++++++++++-------- + {test => tests/unit}/test_step_registry.py | 5 +- + tests/unit/test_textutil.py | 12 +- + tox.ini | 19 +- + 30 files changed, 283 insertions(+), 369 deletions(-) + create mode 100644 py.requirements/ci.tox.txt + delete mode 100644 test/__init__.py + delete mode 100644 test/test_importer.py + rename {test => tests/unit}/_importer_candidate.py (100%) + rename {test => tests/unit}/test_ansi_escapes.py (84%) + rename {test => tests/unit}/test_configuration.py (72%) + rename {test => tests/unit}/test_formatter.py (94%) + rename {test => tests/unit}/test_formatter_progress.py (99%) + rename {test => tests/unit}/test_formatter_rerun.py (94%) + rename {test => tests/unit}/test_formatter_tags.py (99%) + rename {test => tests/unit}/test_log_capture.py (87%) + rename {test => tests/unit}/test_matchers.py (93%) + rename {test => tests/unit}/test_runner.py (82%) + rename {test => tests/unit}/test_step_registry.py (95%) + +diff --git a/MANIFEST.in b/MANIFEST.in +index 49cb7c6..60c2601 100644 +--- a/MANIFEST.in ++++ b/MANIFEST.in +@@ -33,5 +33,3 @@ recursive-include py.requirements *.txt + + prune .tox + prune .venv* +-prune paver_ext +-exclude pavement.py +diff --git a/conftest.py b/conftest.py +index 7b3a883..71a3bd0 100644 +--- a/conftest.py ++++ b/conftest.py +@@ -10,6 +10,10 @@ Add project-specific information. + import behave + import pytest + ++ ++# --------------------------------------------------------------------------- ++# PYTEST FIXTURES: ++# --------------------------------------------------------------------------- + @pytest.fixture(autouse=True) + def _annotate_environment(request): + """Add project-specific information to test-run environment: +@@ -25,3 +29,12 @@ def _annotate_environment(request): + behave_version = behave.__version__ + environment.append(("behave", behave_version)) + ++_pytest_version = pytest.__version__ ++if _pytest_version >= "5.0": ++ # -- SUPPORTED SINCE: pytest 5.0 ++ @pytest.fixture(scope="session", autouse=True) ++ def log_global_env_facts(record_testsuite_property): ++ # SEE: https://docs.pytest.org/en/latest/usage.html ++ behave_version = behave.__version__ ++ record_testsuite_property("BEHAVE_VERSION", behave_version) ++ +diff --git a/invoke.yaml b/invoke.yaml +index 4f21328..3e93cfc 100644 +--- a/invoke.yaml ++++ b/invoke.yaml +@@ -23,6 +23,7 @@ sphinx: + languages: + - de + # PREPARED: - zh-CN ++ + cleanup: + extra_directories: + - "build" +diff --git a/py.requirements/ci.tox.txt b/py.requirements/ci.tox.txt +new file mode 100644 +index 0000000..6b3b3ae +--- /dev/null ++++ b/py.requirements/ci.tox.txt +@@ -0,0 +1,9 @@ ++# ============================================================================ ++# BEHAVE: PYTHON PACKAGE REQUIREMENTS: ci.tox.txt ++# ============================================================================ ++ ++pytest >= 4.2 ++pytest-html >= 1.19.0 ++mock >= 2.0 ++PyHamcrest >= 1.9 ++path.py >= 10.1 +diff --git a/py.requirements/ci.travis.txt b/py.requirements/ci.travis.txt +index 1cc0239..73d65f6 100644 +--- a/py.requirements/ci.travis.txt ++++ b/py.requirements/ci.travis.txt +@@ -1,5 +1,4 @@ + mock +-nose + PyHamcrest >= 1.9 + pytest >= 3.0 + pytest-html >= 1.19.0 +diff --git a/py.requirements/develop.txt b/py.requirements/develop.txt +index 3deedc7..d823389 100644 +--- a/py.requirements/develop.txt ++++ b/py.requirements/develop.txt +@@ -14,7 +14,6 @@ pycmd + bumpversion >= 0.4.0 + + # -- DEVELOPMENT SUPPORT: +-# PREPARED: nose-cov >= 1.4 + tox >= 1.8.1 + coverage >= 4.2 + pytest-cov +diff --git a/py.requirements/testing.txt b/py.requirements/testing.txt +index 3806d39..a418739 100644 +--- a/py.requirements/testing.txt ++++ b/py.requirements/testing.txt +@@ -4,9 +4,8 @@ + + # -- TESTING: Unit tests and behave self-tests. + # PREPARED-FUTURE: behave4cmd0, behave4cmd +-pytest >= 3.0 ++pytest >= 4.2 + pytest-html >= 1.19.0 +-nose >= 1.3 + mock >= 2.0 + PyHamcrest >= 1.9 + +diff --git a/pytest.ini b/pytest.ini +index 17ad388..a686596 100644 +--- a/pytest.ini ++++ b/pytest.ini +@@ -16,8 +16,8 @@ + # ============================================================================ + + [pytest] +-minversion = 2.8 +-testpaths = test tests ++minversion = 4.2 ++testpaths = tests + python_files = test_*.py + addopts = --metadata PACKAGE_UNDER_TEST behave + --metadata PACKAGE_VERSION 1.2.7.dev1 +@@ -26,6 +26,7 @@ addopts = --metadata PACKAGE_UNDER_TEST behave + markers = + smoke + slow ++ + # -- BACKWARD COMPATIBILITY: pytest < 2.8 +-norecursedirs = .git .tox build dist py.requirements tmp* _WORKSPACE ++# norecursedirs = .git .tox build dist py.requirements tmp* _WORKSPACE + +diff --git a/setup.py b/setup.py +index ac7bddf..9c7560d 100644 +--- a/setup.py ++++ b/setup.py +@@ -86,13 +86,11 @@ setup( + "win_unicode_console; python_version < '3.6'", + "colorama", + ], +- test_suite="nose.collector", + tests_require=[ +- "pytest >= 3.0", ++ "pytest >= 4.2", + "pytest-html >= 1.19.0", +- "nose >= 1.3", + "mock >= 1.1", +- "PyHamcrest >= 1.8", ++ "PyHamcrest >= 1.9", + "path.py >= 11.5.0" + ], + cmdclass = { +@@ -105,7 +103,7 @@ setup( + ], + "develop": [ + "coverage", +- "pytest >= 3.0", ++ "pytest >= 4.2", + "pytest-html >= 1.19.0", + "pytest-cov", + "tox", +diff --git a/tasks/test.py b/tasks/test.py +index 06eb175..bfa2d80 100644 +--- a/tasks/test.py ++++ b/tasks/test.py +@@ -172,7 +172,7 @@ namespace.configure({ + }, + }, + "pytest": { +- "scopes": ["test", "tests"], ++ "scopes": ["tests"], + "args": "", + "options": "", # -- NOTE: Overide in configfile "invoke.yaml" + }, +diff --git a/test/__init__.py b/test/__init__.py +deleted file mode 100644 +index e69de29..0000000 +diff --git a/test/test_importer.py b/test/test_importer.py +deleted file mode 100644 +index baca21a..0000000 +--- a/test/test_importer.py ++++ /dev/null +@@ -1,151 +0,0 @@ +-# -*- coding: utf-8 -*- +-""" +-Tests for behave.importing. +-The module provides a lazy-loading/importing mechanism. +-""" +- +-from __future__ import absolute_import +-from behave.importer import LazyObject, LazyDict, load_module, parse_scoped_name +-from behave.formatter.base import Formatter +-from nose.tools import eq_, assert_raises +-import sys +-import types +-# import unittest +- +- +-class TestTheory(object): pass +-class ImportModuleTheory(TestTheory): +- """ +- Provides a test theory for importing modules. +- """ +- +- @classmethod +- def ensure_module_is_not_imported(cls, module_name): +- if module_name in sys.modules: +- del sys.modules[module_name] +- cls.assert_module_is_not_imported(module_name) +- +- @staticmethod +- def assert_module_is_imported(module_name): +- module = sys.modules.get(module_name, None) +- assert module_name in sys.modules +- assert module is not None +- +- @staticmethod +- def assert_module_is_not_imported(module_name): +- assert module_name not in sys.modules +- +- @staticmethod +- def assert_module_with_name(module, name): +- assert isinstance(module, types.ModuleType) +- eq_(module.__name__, name) +- +- +-class TestLoadModule(object): +- theory = ImportModuleTheory +- +- def test_load_module__should_fail_for_unknown_module(self): +- assert_raises(ImportError, load_module, "__unknown_module__") +- +- def test_load_module__should_succeed_for_already_imported_module(self): +- module_name = "behave.importer" +- self.theory.assert_module_is_imported(module_name) +- +- module = load_module(module_name) +- self.theory.assert_module_with_name(module, module_name) +- self.theory.assert_module_is_imported(module_name) +- +- def test_load_module__should_succeed_for_existing_module(self): +- module_name = "test._importer_candidate" +- self.theory.ensure_module_is_not_imported(module_name) +- +- module = load_module(module_name) +- self.theory.assert_module_with_name(module, module_name) +- self.theory.assert_module_is_imported(module_name) +- +-class TestLazyObject(object): +- +- def test_get__should_succeed_for_known_object(self): +- lazy = LazyObject("behave.importer", "LazyObject") +- value = lazy.get() +- assert value is LazyObject +- +- lazy2 = LazyObject("behave.importer:LazyObject") +- value2 = lazy2.get() +- assert value2 is LazyObject +- +- lazy3 = LazyObject("behave.formatter.steps", "StepsFormatter") +- value3 = lazy3.get() +- assert issubclass(value3, Formatter) +- +- def test_get__should_fail_for_unknown_module(self): +- lazy = LazyObject("__unknown_module__", "xxx") +- assert_raises(ImportError, lazy.get) +- +- def test_get__should_fail_for_unknown_object_in_module(self): +- lazy = LazyObject("test._importer_candidate", "xxx") +- assert_raises(ImportError, lazy.get) +- +- +-class LazyDictTheory(TestTheory): +- +- @staticmethod +- def safe_getitem(data, key): +- return dict.__getitem__(data, key) +- +- @classmethod +- def assert_item_is_lazy(cls, data, key): +- value = cls.safe_getitem(data, key) +- cls.assert_is_lazy_object(value) +- +- @classmethod +- def assert_item_is_not_lazy(cls, data, key): +- value = cls.safe_getitem(data, key) +- cls.assert_is_not_lazy_object(value) +- +- @staticmethod +- def assert_is_lazy_object(obj): +- assert isinstance(obj, LazyObject) +- +- @staticmethod +- def assert_is_not_lazy_object(obj): +- assert not isinstance(obj, LazyObject) +- +- +-class TestLazyDict(object): +- theory = LazyDictTheory +- +- def test_unknown_item_access__should_raise_keyerror(self): +- lazy_dict = LazyDict({"alice": 42}) +- item_access = lambda key: lazy_dict[key] +- assert_raises(KeyError, item_access, "unknown") +- +- def test_plain_item_access__should_succeed(self): +- theory = LazyDictTheory +- lazy_dict = LazyDict({"alice": 42}) +- theory.assert_item_is_not_lazy(lazy_dict, "alice") +- +- value = lazy_dict["alice"] +- eq_(value, 42) +- +- def test_lazy_item_access__should_load_object(self): +- ImportModuleTheory.ensure_module_is_not_imported("inspect") +- lazy_dict = LazyDict({"alice": LazyObject("inspect:ismodule")}) +- self.theory.assert_item_is_lazy(lazy_dict, "alice") +- self.theory.assert_item_is_lazy(lazy_dict, "alice") +- +- value = lazy_dict["alice"] +- self.theory.assert_is_not_lazy_object(value) +- self.theory.assert_item_is_not_lazy(lazy_dict, "alice") +- +- def test_lazy_item_access__should_fail_with_unknown_module(self): +- lazy_dict = LazyDict({"bob": LazyObject("__unknown_module__", "xxx")}) +- item_access = lambda key: lazy_dict[key] +- assert_raises(ImportError, item_access, "bob") +- +- def test_lazy_item_access__should_fail_with_unknown_object(self): +- lazy_dict = LazyDict({ +- "bob": LazyObject("behave.importer", "XUnknown") +- }) +- item_access = lambda key: lazy_dict[key] +- assert_raises(ImportError, item_access, "bob") +diff --git a/test/_importer_candidate.py b/tests/unit/_importer_candidate.py +similarity index 100% +rename from test/_importer_candidate.py +rename to tests/unit/_importer_candidate.py +diff --git a/tests/unit/reporter/test_summary.py b/tests/unit/reporter/test_summary.py +index d4e85b5..6b947bd 100644 +--- a/tests/unit/reporter/test_summary.py ++++ b/tests/unit/reporter/test_summary.py +@@ -4,8 +4,6 @@ from __future__ import absolute_import, division + import sys + import pytest + from mock import Mock, patch +-# NOT-NEEDED: from nose.tools import * +- + from behave.model import ScenarioOutline, Scenario + from behave.model_core import Status + from behave.reporter.summary import SummaryReporter, format_summary +diff --git a/tests/unit/tag_expression/test_tag_expression_v1_part1.py b/tests/unit/tag_expression/test_tag_expression_v1_part1.py +index 619c710..56fb85d 100644 +--- a/tests/unit/tag_expression/test_tag_expression_v1_part1.py ++++ b/tests/unit/tag_expression/test_tag_expression_v1_part1.py +@@ -2,7 +2,7 @@ + + from __future__ import absolute_import + from behave.tag_expression import TagExpression +-from nose import tools ++import pytest + import unittest + + +@@ -476,31 +476,34 @@ class TestTagExpressionFoo3OrNotBar4AndZap5(unittest.TestCase): + self.e = TagExpression(['foo:3,-bar', 'zap:5']) + + def test_should_count_tags_for_positive_tags(self): +- tools.eq_(self.e.limits, {'foo': 3, 'zap': 5}) ++ assert self.e.limits == {'foo': 3, 'zap': 5} + + def test_should_match_foo_zap(self): + assert self.e.check(['foo', 'zap']) + ++ + class TestTagExpressionParsing(unittest.TestCase): + def setUp(self): + self.e = TagExpression([' foo:3 , -bar ', ' zap:5 ']) + + def test_should_have_limits(self): +- tools.eq_(self.e.limits, {'zap': 5, 'foo': 3}) ++ assert self.e.limits == {'zap': 5, 'foo': 3} ++ + + class TestTagExpressionTagLimits(unittest.TestCase): + def test_should_be_counted_for_negative_tags(self): + e = TagExpression(['-todo:3']) +- tools.eq_(e.limits, {'todo': 3}) ++ assert e.limits == {'todo': 3} + + def test_should_be_counted_for_positive_tags(self): + e = TagExpression(['todo:3']) +- tools.eq_(e.limits, {'todo': 3}) ++ assert e.limits == {'todo': 3} + + def test_should_raise_an_error_for_inconsistent_limits(self): +- tools.assert_raises(Exception, TagExpression, ['todo:3', '-todo:4']) ++ with pytest.raises(Exception): ++ _ = TagExpression(['todo:3', '-todo:4']) + + def test_should_allow_duplicate_consistent_limits(self): + e = TagExpression(['todo:3', '-todo:3']) +- tools.eq_(e.limits, {'todo': 3}) ++ assert e.limits == {'todo': 3} + +diff --git a/tests/unit/tag_expression/test_tag_expression_v1_part2.py b/tests/unit/tag_expression/test_tag_expression_v1_part2.py +index 690235b..cf619da 100644 +--- a/tests/unit/tag_expression/test_tag_expression_v1_part2.py ++++ b/tests/unit/tag_expression/test_tag_expression_v1_part2.py +@@ -6,10 +6,11 @@ REQUIRES: Python >= 2.6, because itertools.combinations() is used. + """ + + from __future__ import absolute_import +-from behave.tag_expression import TagExpression +-from nose import tools + import itertools + from six.moves import range ++import pytest ++from behave.tag_expression import TagExpression ++ + + has_combinations = hasattr(itertools, "combinations") + if has_combinations: +@@ -31,6 +32,7 @@ if has_combinations: + return "@" + " @".join(tags) + return NO_TAGS + ++ + TestCase = object + # ---------------------------------------------------------------------------- + # TEST: all_combinations() test helper +@@ -45,8 +47,8 @@ if has_combinations: + ('@one', '@two'), + ] + actual = all_combinations(items) +- tools.eq_(actual, expected) +- tools.eq_(len(actual), 4) ++ assert actual == expected ++ assert len(actual) == 4 + + def test_all_combinations_with_3values(self): + items = "@one @two @three".split() +@@ -61,8 +63,8 @@ if has_combinations: + ('@one', '@two', '@three'), + ] + actual = all_combinations(items) +- tools.eq_(actual, expected) +- tools.eq_(len(actual), 8) ++ assert actual == expected ++ assert len(actual) == 8 + + + # ---------------------------------------------------------------------------- +@@ -74,13 +76,13 @@ if has_combinations: + tag_combinations, expected): + matched = [ make_tags_line(c) for c in tag_combinations + if tag_expression.check(c) ] +- tools.eq_(matched, expected) ++ assert matched == expected + + def assert_tag_expression_mismatches(self, tag_expression, + tag_combinations, expected): + mismatched = [ make_tags_line(c) for c in tag_combinations + if not tag_expression.check(c) ] +- tools.eq_(mismatched, expected) ++ assert mismatched == expected + + + class TestTagExpressionWith1Term(TagExpressionTestCase): +diff --git a/test/test_ansi_escapes.py b/tests/unit/test_ansi_escapes.py +similarity index 84% +rename from test/test_ansi_escapes.py +rename to tests/unit/test_ansi_escapes.py +index 77564fd..969f3a9 100644 +--- a/test/test_ansi_escapes.py ++++ b/tests/unit/test_ansi_escapes.py +@@ -7,7 +7,7 @@ + # W0621 Redefining name ... from outer scope + + from __future__ import absolute_import +-from nose import tools ++import pytest + from behave.formatter import ansi_escapes + import unittest + from six.moves import range +@@ -44,30 +44,30 @@ class StripEscapesTest(unittest.TestCase): + + def test_should_return_same_text_without_escapes(self): + for text in self.TEXTS: +- tools.eq_(text, ansi_escapes.strip_escapes(text)) ++ assert text == ansi_escapes.strip_escapes(text) + + def test_should_return_empty_string_for_any_ansi_escape(self): + # XXX-JE-CHECK-PY23: If list() is really needed. + for text in list(ansi_escapes.colors.values()): +- tools.eq_("", ansi_escapes.strip_escapes(text)) ++ assert "" == ansi_escapes.strip_escapes(text) + for text in list(ansi_escapes.escapes.values()): +- tools.eq_("", ansi_escapes.strip_escapes(text)) ++ assert "" == ansi_escapes.strip_escapes(text) + + + def test_should_strip_color_escapes_from_text(self): + for text in self.TEXTS: + colored_text = self.colorize_text(text, self.ALL_COLORS) +- tools.eq_(text, ansi_escapes.strip_escapes(colored_text)) ++ assert text == ansi_escapes.strip_escapes(colored_text) + self.assertNotEqual(text, colored_text) + + for color in self.ALL_COLORS: + colored_text = self.colorize(text, color) +- tools.eq_(text, ansi_escapes.strip_escapes(colored_text)) ++ assert text == ansi_escapes.strip_escapes(colored_text) + self.assertNotEqual(text, colored_text) + + def test_should_strip_cursor_up_escapes_from_text(self): + for text in self.TEXTS: + for cursor_up in self.CURSOR_UPS: + colored_text = cursor_up + text + ansi_escapes.escapes["reset"] +- tools.eq_(text, ansi_escapes.strip_escapes(colored_text)) ++ assert text == ansi_escapes.strip_escapes(colored_text) + self.assertNotEqual(text, colored_text) +diff --git a/test/test_configuration.py b/tests/unit/test_configuration.py +similarity index 72% +rename from test/test_configuration.py +rename to tests/unit/test_configuration.py +index e6828e3..c96cf63 100644 +--- a/test/test_configuration.py ++++ b/tests/unit/test_configuration.py +@@ -2,7 +2,7 @@ import os.path + import sys + import tempfile + import six +-from nose.tools import * ++import pytest + from behave import configuration + from behave.configuration import Configuration, UserData + from unittest import TestCase +@@ -36,6 +36,7 @@ if sys.platform.startswith("win"): + ROOTDIR_PREFIX_DEFAULT = ROOTDIR_PREFIX_DEFAULT.lower() + ROOTDIR_PREFIX = os.environ.get("BEHAVE_ROOTDIR_PREFIX", ROOTDIR_PREFIX_DEFAULT) + ++ + class TestConfiguration(object): + + def test_read_file(self): +@@ -46,19 +47,19 @@ class TestConfiguration(object): + + # -- WINDOWS-REQUIRES: normpath + d = configuration.read_configuration(tn) +- eq_(d["outfiles"], [ ++ assert d["outfiles"] ==[ + os.path.normpath(ROOTDIR_PREFIX + "/absolute/path1"), + os.path.normpath(os.path.join(tndir, "relative/path2")), +- ]) +- eq_(d["paths"], [ ++ ] ++ assert d["paths"] == [ + os.path.normpath(ROOTDIR_PREFIX + "/absolute/path3"), + os.path.normpath(os.path.join(tndir, "relative/path4")), +- ]) +- eq_(d["format"], ["pretty", "tag-counter"]) +- eq_(d["default_tags"], ["@foo,~@bar", "@zap"]) +- eq_(d["stdout_capture"], False) +- ok_("bogus" not in d) +- eq_(d["userdata"], {"foo": "bar", "answer": "42"}) ++ ] ++ assert d["format"] == ["pretty", "tag-counter"] ++ assert d["default_tags"] == ["@foo,~@bar", "@zap"] ++ assert d["stdout_capture"] == False ++ assert "bogus" not in d ++ assert d["userdata"] == {"foo": "bar", "answer": "42"} + + def ensure_stage_environment_is_not_set(self): + if "BEHAVE_STAGE" in os.environ: +@@ -69,26 +70,26 @@ class TestConfiguration(object): + self.ensure_stage_environment_is_not_set() + assert "BEHAVE_STAGE" not in os.environ + config = Configuration("") +- eq_("steps", config.steps_dir) +- eq_("environment.py", config.environment_file) ++ assert "steps" == config.steps_dir ++ assert "environment.py" == config.environment_file + + def test_settings_with_stage(self): + config = Configuration(["--stage=STAGE1"]) +- eq_("STAGE1_steps", config.steps_dir) +- eq_("STAGE1_environment.py", config.environment_file) ++ assert "STAGE1_steps" == config.steps_dir ++ assert "STAGE1_environment.py" == config.environment_file + + def test_settings_with_stage_and_envvar(self): + os.environ["BEHAVE_STAGE"] = "STAGE2" + config = Configuration(["--stage=STAGE1"]) +- eq_("STAGE1_steps", config.steps_dir) +- eq_("STAGE1_environment.py", config.environment_file) ++ assert "STAGE1_steps" == config.steps_dir ++ assert "STAGE1_environment.py" == config.environment_file + del os.environ["BEHAVE_STAGE"] + + def test_settings_with_stage_from_envvar(self): + os.environ["BEHAVE_STAGE"] = "STAGE2" + config = Configuration("") +- eq_("STAGE2_steps", config.steps_dir) +- eq_("STAGE2_environment.py", config.environment_file) ++ assert "STAGE2_steps" == config.steps_dir ++ assert "STAGE2_environment.py" == config.environment_file + del os.environ["BEHAVE_STAGE"] + + +@@ -101,33 +102,33 @@ class TestConfigurationUserData(TestCase): + "--define=bar=bar_value", + "--define", "baz=BAZ_VALUE", + ]) +- eq_("foo_value", config.userdata["foo"]) +- eq_("bar_value", config.userdata["bar"]) +- eq_("BAZ_VALUE", config.userdata["baz"]) ++ assert "foo_value" == config.userdata["foo"] ++ assert "bar_value" == config.userdata["bar"] ++ assert "BAZ_VALUE" == config.userdata["baz"] + + def test_cmdline_defines_override_configfile(self): + userdata_init = {"foo": "XXX", "bar": "ZZZ", "baz": 42} + config = Configuration( + "-D foo=foo_value --define bar=123", + load_config=False, userdata=userdata_init) +- eq_("foo_value", config.userdata["foo"]) +- eq_("123", config.userdata["bar"]) +- eq_(42, config.userdata["baz"]) ++ assert "foo_value" == config.userdata["foo"] ++ assert "123" == config.userdata["bar"] ++ assert 42 == config.userdata["baz"] + + def test_cmdline_defines_without_value_are_true(self): + config = Configuration("-D foo --define bar -Dbaz") +- eq_("true", config.userdata["foo"]) +- eq_("true", config.userdata["bar"]) +- eq_("true", config.userdata["baz"]) +- eq_(True, config.userdata.getbool("foo")) ++ assert "true" == config.userdata["foo"] ++ assert "true" == config.userdata["bar"] ++ assert "true" == config.userdata["baz"] ++ assert True == config.userdata.getbool("foo") + + def test_cmdline_defines_with_empty_value(self): + config = Configuration("-D foo=") +- eq_("", config.userdata["foo"]) ++ assert "" == config.userdata["foo"] + + def test_cmdline_defines_with_assign_character_as_value(self): + config = Configuration("-D foo=bar=baz") +- eq_("bar=baz", config.userdata["foo"]) ++ assert "bar=baz" == config.userdata["foo"] + + def test_cmdline_defines__with_quoted_name_value_pair(self): + cmdlines = [ +@@ -136,7 +137,7 @@ class TestConfigurationUserData(TestCase): + ] + for cmdline in cmdlines: + config = Configuration(cmdline, load_config=False) +- eq_(config.userdata, dict(person="Alice and Bob")) ++ assert config.userdata == dict(person="Alice and Bob") + + def test_cmdline_defines__with_quoted_value(self): + cmdlines = [ +@@ -145,7 +146,7 @@ class TestConfigurationUserData(TestCase): + ] + for cmdline in cmdlines: + config = Configuration(cmdline, load_config=False) +- eq_(config.userdata, dict(person="Alice and Bob")) ++ assert config.userdata == dict(person="Alice and Bob") + + def test_setup_userdata(self): + config = Configuration("", load_config=False) +@@ -154,7 +155,7 @@ class TestConfigurationUserData(TestCase): + config.setup_userdata() + + expected_data = dict(person1="Alice", person2="Charly") +- eq_(config.userdata, expected_data) ++ assert config.userdata == expected_data + + def test_update_userdata__with_cmdline_defines(self): + # -- NOTE: cmdline defines are reapplied. +@@ -163,8 +164,8 @@ class TestConfigurationUserData(TestCase): + config.update_userdata(dict(person1="Alice", person2="Bob")) + + expected_data = dict(person1="Alice", person2="Bea", person3="Charly") +- eq_(config.userdata, expected_data) +- eq_(config.userdata_defines, [("person2", "Bea")]) ++ assert config.userdata == expected_data ++ assert config.userdata_defines == [("person2", "Bea")] + + def test_update_userdata__without_cmdline_defines(self): + config = Configuration("", load_config=False) +@@ -172,5 +173,5 @@ class TestConfigurationUserData(TestCase): + config.update_userdata(dict(person1="Alice", person2="Bob")) + + expected_data = dict(person1="Alice", person2="Bob", person3="Charly") +- eq_(config.userdata, expected_data) +- self.assertFalse(config.userdata_defines) ++ assert config.userdata == expected_data ++ assert config.userdata_defines is None +diff --git a/test/test_formatter.py b/tests/unit/test_formatter.py +similarity index 94% +rename from test/test_formatter.py +rename to tests/unit/test_formatter.py +index 42e5f0d..c1a0945 100644 +--- a/test/test_formatter.py ++++ b/tests/unit/test_formatter.py +@@ -6,9 +6,8 @@ import sys + import tempfile + import unittest + import six ++import pytest + from mock import Mock, patch +-from nose.tools import * # pylint: disable=wildcard-import, unused-wildcard-import +- + from behave.formatter._registry import make_formatters + from behave.formatter import pretty + from behave.formatter.base import StreamOpener +@@ -35,7 +34,7 @@ class TestGetTerminalSize(unittest.TestCase): + platform = sys.platform + sys.platform = "windows" + +- eq_(pretty.get_terminal_size(), (80, 24)) ++ assert pretty.get_terminal_size() == (80, 24) + + sys.platform = platform + +@@ -46,7 +45,7 @@ class TestGetTerminalSize(unittest.TestCase): + except ImportError: + pass + +- eq_(pretty.get_terminal_size(), (80, 24)) ++ assert pretty.get_terminal_size() == (80, 24) + + def test_exception_in_ioctl(self): + try: +@@ -59,7 +58,7 @@ class TestGetTerminalSize(unittest.TestCase): + + self.ioctl.side_effect = raiser + +- eq_(pretty.get_terminal_size(), (80, 24)) ++ assert pretty.get_terminal_size() == (80, 24) + self.ioctl.assert_called_with(0, termios.TIOCGWINSZ, self.zero_struct) + + def test_happy_path(self): +@@ -70,7 +69,7 @@ class TestGetTerminalSize(unittest.TestCase): + + self.ioctl.return_value = struct.pack("HHHH", 17, 23, 5, 5) + +- eq_(pretty.get_terminal_size(), (23, 17)) ++ assert pretty.get_terminal_size() == (23, 17) + self.ioctl.assert_called_with(0, termios.TIOCGWINSZ, self.zero_struct) + + def test_zero_size_fallback(self): +@@ -81,7 +80,7 @@ class TestGetTerminalSize(unittest.TestCase): + + self.ioctl.return_value = self.zero_struct + +- eq_(pretty.get_terminal_size(), (80, 24)) ++ assert pretty.get_terminal_size() == (80, 24) + self.ioctl.assert_called_with(0, termios.TIOCGWINSZ, self.zero_struct) + + +@@ -204,7 +203,7 @@ class TestTagsCount(FormatterTests): + p.feature(f) + p.scenario(s) + +- eq_(p.tag_counts, {"ham": [f, s], "spam": [f], "foo": [s]}) ++ assert p.tag_counts == {"ham": [f, s], "spam": [f], "foo": [s]} + + + class MultipleFormattersTests(FormatterTests): +diff --git a/test/test_formatter_progress.py b/tests/unit/test_formatter_progress.py +similarity index 99% +rename from test/test_formatter_progress.py +rename to tests/unit/test_formatter_progress.py +index 29c8e68..19cdf64 100644 +--- a/test/test_formatter_progress.py ++++ b/tests/unit/test_formatter_progress.py +@@ -9,6 +9,7 @@ from __future__ import absolute_import + from .test_formatter import FormatterTests as FormatterTest + from .test_formatter import MultipleFormattersTests as MultipleFormattersTest + ++ + class TestScenarioProgressFormatter(FormatterTest): + formatter_name = "progress" + +@@ -20,20 +21,26 @@ class TestStepProgressFormatter(FormatterTest): + class TestPrettyAndScenarioProgress(MultipleFormattersTest): + formatters = ['pretty', 'progress'] + ++ + class TestPlainAndScenarioProgress(MultipleFormattersTest): + formatters = ['plain', 'progress'] + ++ + class TestJSONAndScenarioProgress(MultipleFormattersTest): + formatters = ['json', 'progress'] + ++ + class TestPrettyAndStepProgress(MultipleFormattersTest): + formatters = ['pretty', 'progress2'] + ++ + class TestPlainAndStepProgress(MultipleFormattersTest): + formatters = ['plain', 'progress2'] + ++ + class TestJSONAndStepProgress(MultipleFormattersTest): + formatters = ['json', 'progress2'] + ++ + class TestScenarioProgressAndStepProgress(MultipleFormattersTest): + formatters = ['progress', 'progress2'] +diff --git a/test/test_formatter_rerun.py b/tests/unit/test_formatter_rerun.py +similarity index 94% +rename from test/test_formatter_rerun.py +rename to tests/unit/test_formatter_rerun.py +index 6357f92..154588f 100644 +--- a/test/test_formatter_rerun.py ++++ b/tests/unit/test_formatter_rerun.py +@@ -8,7 +8,7 @@ from __future__ import absolute_import + from behave.model_core import Status + from .test_formatter import FormatterTests as FormatterTest, _tf + from .test_formatter import MultipleFormattersTests as MultipleFormattersTest +-from nose.tools import * ++ + + class TestRerunFormatter(FormatterTest): + formatter_name = "rerun" +@@ -26,7 +26,7 @@ class TestRerunFormatter(FormatterTest): + p.scenario(scenario) + assert scenario.status == Status.passed + p.eof() +- eq_([], p.failed_scenarios) ++ assert [] == p.failed_scenarios + # -- EMIT REPORT: + p.close() + +@@ -49,7 +49,7 @@ class TestRerunFormatter(FormatterTest): + assert scenarios[0].status == Status.passed + assert scenarios[1].status == Status.failed + p.eof() +- eq_([ failing_scenario ], p.failed_scenarios) ++ assert [ failing_scenario ] == p.failed_scenarios + # -- EMIT REPORT: + p.close() + +@@ -76,7 +76,7 @@ class TestRerunFormatter(FormatterTest): + assert scenarios[1].status == Status.passed + assert scenarios[2].status == Status.failed + p.eof() +- eq_([ failing_scenario1, failing_scenario2 ], p.failed_scenarios) ++ assert [ failing_scenario1, failing_scenario2 ] == p.failed_scenarios + # -- EMIT REPORT: + p.close() + +@@ -84,14 +84,18 @@ class TestRerunFormatter(FormatterTest): + class TestRerunAndPrettyFormatters(MultipleFormattersTest): + formatters = ["rerun", "pretty"] + ++ + class TestRerunAndPlainFormatters(MultipleFormattersTest): + formatters = ["rerun", "plain"] + ++ + class TestRerunAndScenarioProgressFormatters(MultipleFormattersTest): + formatters = ["rerun", "progress"] + ++ + class TestRerunAndStepProgressFormatters(MultipleFormattersTest): + formatters = ["rerun", "progress2"] + ++ + class TestRerunAndJsonFormatter(MultipleFormattersTest): + formatters = ["rerun", "json"] +diff --git a/test/test_formatter_tags.py b/tests/unit/test_formatter_tags.py +similarity index 99% +rename from test/test_formatter_tags.py +rename to tests/unit/test_formatter_tags.py +index 5125d51..9f95374 100644 +--- a/test/test_formatter_tags.py ++++ b/tests/unit/test_formatter_tags.py +@@ -9,12 +9,14 @@ from __future__ import absolute_import + from .test_formatter import FormatterTests as FormatterTest + from .test_formatter import MultipleFormattersTests as MultipleFormattersTest + ++ + # ----------------------------------------------------------------------------- + # FORMATTER TESTS: With TagCountFormatter + # ----------------------------------------------------------------------------- + class TestTagsCountFormatter(FormatterTest): + formatter_name = "tags" + ++ + # ----------------------------------------------------------------------------- + # FORMATTER TESTS: With TagLocationFormatter + # ----------------------------------------------------------------------------- +@@ -28,12 +30,15 @@ class TestTagsLocationFormatter(FormatterTest): + class TestPrettyAndTagsCount(MultipleFormattersTest): + formatters = ["pretty", "tags"] + ++ + class TestPlainAndTagsCount(MultipleFormattersTest): + formatters = ["plain", "tags"] + ++ + class TestJSONAndTagsCount(MultipleFormattersTest): + formatters = ["json", "tags"] + ++ + class TestRerunAndTagsCount(MultipleFormattersTest): + formatters = ["rerun", "tags"] + +@@ -44,14 +49,18 @@ class TestRerunAndTagsCount(MultipleFormattersTest): + class TestPrettyAndTagsLocation(MultipleFormattersTest): + formatters = ["pretty", "tags.location"] + ++ + class TestPlainAndTagsLocation(MultipleFormattersTest): + formatters = ["plain", "tags.location"] + ++ + class TestJSONAndTagsLocation(MultipleFormattersTest): + formatters = ["json", "tags.location"] + ++ + class TestRerunAndTagsLocation(MultipleFormattersTest): + formatters = ["rerun", "tags.location"] + ++ + class TestTagsCountAndTagsLocation(MultipleFormattersTest): + formatters = ["tags", "tags.location"] +diff --git a/tests/unit/test_importer.py b/tests/unit/test_importer.py +index f3f4e2c..055b9fb 100644 +--- a/tests/unit/test_importer.py ++++ b/tests/unit/test_importer.py +@@ -62,7 +62,7 @@ class TestLoadModule(object): + self.theory.assert_module_is_imported(module_name) + + def test_load_module__should_succeed_for_existing_module(self): +- module_name = "test._importer_candidate" ++ module_name = "tests.unit._importer_candidate" + self.theory.ensure_module_is_not_imported(module_name) + + module = load_module(module_name) +diff --git a/test/test_log_capture.py b/tests/unit/test_log_capture.py +similarity index 87% +rename from test/test_log_capture.py +rename to tests/unit/test_log_capture.py +index bfdbed7..bf1874c 100644 +--- a/test/test_log_capture.py ++++ b/tests/unit/test_log_capture.py +@@ -1,11 +1,10 @@ + from __future__ import absolute_import, with_statement +- +-from nose.tools import * ++import pytest + from mock import patch +- + from behave.log_capture import LoggingCapture + from six.moves import range + ++ + class TestLogCapture(object): + def test_get_value_returns_all_log_records(self): + class FakeConfig(object): +@@ -23,7 +22,7 @@ class TestLogCapture(object): + format.return_value = 'foo' + expected = '\n'.join(['foo'] * len(fake_records)) + +- eq_(handler.getvalue(), expected) ++ assert handler.getvalue() == expected + + calls = [args[0][0] for args in format.call_args_list] +- eq_(calls, fake_records) ++ assert calls == fake_records +diff --git a/test/test_matchers.py b/tests/unit/test_matchers.py +similarity index 93% +rename from test/test_matchers.py +rename to tests/unit/test_matchers.py +index bfe04fc..815581c 100644 +--- a/test/test_matchers.py ++++ b/tests/unit/test_matchers.py +@@ -1,7 +1,7 @@ + # -*- coding: UTF-8 -*- + from __future__ import absolute_import, with_statement ++import pytest + from mock import Mock, patch +-from nose.tools import * # pylint: disable=wildcard-import, unused-wildcard-import + import parse + from behave.matchers import Match, Matcher, ParseMatcher, RegexMatcher, \ + SimplifiedRegexMatcher, CucumberRegexMatcher +@@ -80,7 +80,7 @@ class TestParseMatcher(object): + assert m.func is func + args = m.arguments + have = [(a.start, a.end, a.original, a.value, a.name) for a in args] +- eq_(have, expected) ++ assert have == expected + + def test_named_arguments(self): + text = "has a {string}, an {integer:d} and a {decimal:f}" +@@ -89,11 +89,11 @@ class TestParseMatcher(object): + + m = matcher.match("has a foo, an 11 and a 3.14159") + m.run(context) +- eq_(self.recorded_args, ((context,), { ++ assert self.recorded_args, ((context,) == { + 'string': 'foo', + 'integer': 11, + 'decimal': 3.14159 +- })) ++ }) + + def test_positional_arguments(self): + text = "has a {}, an {:d} and a {:f}" +@@ -102,7 +102,7 @@ class TestParseMatcher(object): + + m = matcher.match("has a foo, an 11 and a 3.14159") + m.run(context) +- eq_(self.recorded_args, ((context, 'foo', 11, 3.14159), {})) ++ assert self.recorded_args == ((context, 'foo', 11, 3.14159), {}) + + class TestRegexMatcher(object): + # pylint: disable=invalid-name, no-self-use +@@ -151,7 +151,7 @@ class TestRegexMatcher(object): + assert m.func is func + args = m.arguments + have = [(a.start, a.end, a.original, a.value, a.name) for a in args] +- eq_(have, expected) ++ assert have == expected + + + +@@ -179,17 +179,17 @@ class TestSimplifiedRegexMatcher(TestRegexMatcher): + assert isinstance(matched1, Match) + assert isinstance(matched2, Match) + +- @raises(AssertionError) + def test_step_should_not_use_regex_begin_marker(self): +- SimplifiedRegexMatcher(None, "^I do something") ++ with pytest.raises(AssertionError): ++ SimplifiedRegexMatcher(None, "^I do something") + +- @raises(AssertionError) + def test_step_should_not_use_regex_end_marker(self): +- SimplifiedRegexMatcher(None, "I do something$") ++ with pytest.raises(AssertionError): ++ SimplifiedRegexMatcher(None, "I do something$") + +- @raises(AssertionError) + def test_step_should_not_use_regex_begin_and_end_marker(self): +- SimplifiedRegexMatcher(None, "^I do something$") ++ with pytest.raises(AssertionError): ++ SimplifiedRegexMatcher(None, "^I do something$") + + + class TestCucumberRegexMatcher(TestRegexMatcher): +diff --git a/tests/unit/test_parser.py b/tests/unit/test_parser.py +index 5603a4b..ecbb1bf 100644 +--- a/tests/unit/test_parser.py ++++ b/tests/unit/test_parser.py +@@ -7,7 +7,6 @@ Unit tests for Gherkin parser: :mod:`behave.parser`. + from __future__ import absolute_import, print_function + import pytest + from behave import i18n, model, parser +-# NOT-NEEDED: from nose.tools import * + + + # --------------------------------------------------------------------------- +diff --git a/test/test_runner.py b/tests/unit/test_runner.py +similarity index 82% +rename from test/test_runner.py +rename to tests/unit/test_runner.py +index 6647283..030dffa 100644 +--- a/test/test_runner.py ++++ b/tests/unit/test_runner.py +@@ -11,10 +11,8 @@ import tempfile + import unittest + import six + from six import StringIO +- ++import pytest + from mock import Mock, patch +-from nose.tools import * # pylint: disable=wildcard-import, unused-wildcard-import +- + from behave import runner_util + from behave.model import Table + from behave.step_registry import StepRegistry +@@ -39,31 +37,31 @@ class TestContext(unittest.TestCase): + def test_user_mode_shall_restore_behave_mode(self): + # -- CASE: No exception is raised. + initial_mode = runner.Context.BEHAVE +- eq_(self.context._mode, initial_mode) ++ assert self.context._mode == initial_mode + with self.context.use_with_user_mode(): +- eq_(self.context._mode, runner.Context.USER) ++ assert self.context._mode == runner.Context.USER + self.context.thing = "stuff" +- eq_(self.context._mode, initial_mode) ++ assert self.context._mode == initial_mode + + def test_user_mode_shall_restore_behave_mode_if_assert_fails(self): + initial_mode = runner.Context.BEHAVE +- eq_(self.context._mode, initial_mode) ++ assert self.context._mode == initial_mode + try: + with self.context.use_with_user_mode(): +- eq_(self.context._mode, runner.Context.USER) ++ assert self.context._mode == runner.Context.USER + assert False, "XFAIL" + except AssertionError: +- eq_(self.context._mode, initial_mode) ++ assert self.context._mode == initial_mode + + def test_user_mode_shall_restore_behave_mode_if_exception_is_raised(self): + initial_mode = runner.Context.BEHAVE +- eq_(self.context._mode, initial_mode) ++ assert self.context._mode == initial_mode + try: + with self.context.use_with_user_mode(): +- eq_(self.context._mode, runner.Context.USER) ++ assert self.context._mode == runner.Context.USER + raise RuntimeError("XFAIL") + except RuntimeError: +- eq_(self.context._mode, initial_mode) ++ assert self.context._mode == initial_mode + + def test_use_with_user_mode__shall_restore_initial_mode(self): + # -- CASE: No exception is raised. +@@ -71,9 +69,9 @@ class TestContext(unittest.TestCase): + initial_mode = runner.Context.BEHAVE + self.context._mode = initial_mode + with self.context.use_with_user_mode(): +- eq_(self.context._mode, runner.Context.USER) ++ assert self.context._mode == runner.Context.USER + self.context.thing = "stuff" +- eq_(self.context._mode, initial_mode) ++ assert self.context._mode == initial_mode + + def test_use_with_user_mode__shall_restore_initial_mode_with_error(self): + # -- CASE: Exception is raised. +@@ -82,10 +80,10 @@ class TestContext(unittest.TestCase): + self.context._mode = initial_mode + try: + with self.context.use_with_user_mode(): +- eq_(self.context._mode, runner.Context.USER) ++ assert self.context._mode == runner.Context.USER + raise RuntimeError("XFAIL") + except RuntimeError: +- eq_(self.context._mode, initial_mode) ++ assert self.context._mode == initial_mode + + def test_use_with_behave_mode__shall_restore_initial_mode(self): + # -- CASE: No exception is raised. +@@ -93,9 +91,9 @@ class TestContext(unittest.TestCase): + initial_mode = runner.Context.USER + self.context._mode = initial_mode + with self.context._use_with_behave_mode(): +- eq_(self.context._mode, runner.Context.BEHAVE) ++ assert self.context._mode == runner.Context.BEHAVE + self.context.thing = "stuff" +- eq_(self.context._mode, initial_mode) ++ assert self.context._mode == initial_mode + + def test_use_with_behave_mode__shall_restore_initial_mode_with_error(self): + # -- CASE: Exception is raised. +@@ -104,22 +102,22 @@ class TestContext(unittest.TestCase): + self.context._mode = initial_mode + try: + with self.context._use_with_behave_mode(): +- eq_(self.context._mode, runner.Context.BEHAVE) ++ assert self.context._mode == runner.Context.BEHAVE + raise RuntimeError("XFAIL") + except RuntimeError: +- eq_(self.context._mode, initial_mode) ++ assert self.context._mode == initial_mode + + def test_context_contains(self): +- eq_("thing" in self.context, False) ++ assert "thing" not in self.context + self.context.thing = "stuff" +- eq_("thing" in self.context, True) ++ assert "thing" in self.context + self.context._push() +- eq_("thing" in self.context, True) ++ assert "thing" in self.context + + def test_attribute_set_at_upper_level_visible_at_lower_level(self): + self.context.thing = "stuff" + self.context._push() +- eq_(self.context.thing, "stuff") ++ assert self.context.thing == "stuff" + + def test_attribute_set_at_lower_level_not_visible_at_upper_level(self): + self.context._push() +@@ -130,16 +128,16 @@ class TestContext(unittest.TestCase): + def test_attributes_set_at_upper_level_visible_at_lower_level(self): + self.context.thing = "stuff" + self.context._push() +- eq_(self.context.thing, "stuff") ++ assert self.context.thing == "stuff" + self.context.other_thing = "more stuff" + self.context._push() +- eq_(self.context.thing, "stuff") +- eq_(self.context.other_thing, "more stuff") ++ assert self.context.thing == "stuff" ++ assert self.context.other_thing == "more stuff" + self.context.third_thing = "wombats" + self.context._push() +- eq_(self.context.thing, "stuff") +- eq_(self.context.other_thing, "more stuff") +- eq_(self.context.third_thing, "wombats") ++ assert self.context.thing == "stuff" ++ assert self.context.other_thing == "more stuff" ++ assert self.context.third_thing == "wombats" + + def test_attributes_set_at_lower_level_not_visible_at_upper_level(self): + self.context.thing = "stuff" +@@ -149,17 +147,17 @@ class TestContext(unittest.TestCase): + + self.context._push() + self.context.third_thing = "wombats" +- eq_(self.context.thing, "stuff") +- eq_(self.context.other_thing, "more stuff") +- eq_(self.context.third_thing, "wombats") ++ assert self.context.thing == "stuff" ++ assert self.context.other_thing == "more stuff" ++ assert self.context.third_thing == "wombats" + + self.context._pop() +- eq_(self.context.thing, "stuff") +- eq_(self.context.other_thing, "more stuff") ++ assert self.context.thing == "stuff" ++ assert self.context.other_thing == "more stuff" + assert getattr(self.context, "third_thing", None) is None, "%s is not None" % self.context.third_thing + + self.context._pop() +- eq_(self.context.thing, "stuff") ++ assert self.context.thing == "stuff" + assert getattr(self.context, "other_thing", None) is None, "%s is not None" % self.context.other_thing + assert getattr(self.context, "third_thing", None) is None, "%s is not None" % self.context.third_thing + +@@ -270,21 +268,22 @@ class TestContext(unittest.TestCase): + assert filename in info, "%r not in %r" % (filename, info) + + def test_context_deletable(self): +- eq_("thing" in self.context, False) ++ assert "thing" not in self.context + self.context.thing = "stuff" +- eq_("thing" in self.context, True) ++ assert "thing" in self.context + del self.context.thing +- eq_("thing" in self.context, False) ++ assert "thing" not in self.context + +- @raises(AttributeError) ++ # OLD: @raises(AttributeError) + def test_context_deletable_raises(self): + # pylint: disable=protected-access +- eq_("thing" in self.context, False) ++ assert "thing" not in self.context + self.context.thing = "stuff" +- eq_("thing" in self.context, True) ++ assert "thing" in self.context + self.context._push() +- eq_("thing" in self.context, True) +- del self.context.thing ++ assert "thing" in self.context ++ with pytest.raises(AttributeError): ++ del self.context.thing + + + class ExampleSteps(object): +@@ -362,7 +361,7 @@ Then a step passes + """.lstrip() + with patch("behave.step_registry.registry", self.step_registry): + result = self.context.execute_steps(doc) +- eq_(result, True) ++ assert result is True + + def test_execute_steps_with_failing_step(self): + doc = u""" +@@ -374,7 +373,7 @@ Then a step passes + try: + result = self.context.execute_steps(doc) + except AssertionError as e: +- ok_("FAILED SUB-STEP: When a step fails" in _text(e)) ++ assert "FAILED SUB-STEP: When a step fails" in _text(e) + + def test_execute_steps_with_undefined_step(self): + doc = u""" +@@ -386,7 +385,7 @@ Then a step passes + try: + result = self.context.execute_steps(doc) + except AssertionError as e: +- ok_("UNDEFINED SUB-STEP: When a step is undefined" in _text(e)) ++ assert "UNDEFINED SUB-STEP: When a step is undefined" in _text(e) + + def test_execute_steps_with_text(self): + doc = u''' +@@ -401,8 +400,8 @@ Then a step passes + with patch("behave.step_registry.registry", self.step_registry): + result = self.context.execute_steps(doc) + expected_text = "Lorem ipsum\nIpsum lorem" +- eq_(result, True) +- eq_(expected_text, ExampleSteps.text) ++ assert result is True ++ assert expected_text == ExampleSteps.text + + def test_execute_steps_with_table(self): + doc = u""" +@@ -419,8 +418,8 @@ Then a step passes + [u"Alice", u"12"], + [u"Bob", u"23"], + ]) +- eq_(result, True) +- eq_(expected_table, ExampleSteps.table) ++ assert result is True ++ assert expected_table == ExampleSteps.table + + def test_context_table_is_restored_after_execute_steps_without_table(self): + doc = u""" +@@ -431,7 +430,7 @@ Then a step passes + original_table = "" + self.context.table = original_table + self.context.execute_steps(doc) +- eq_(self.context.table, original_table) ++ assert self.context.table == original_table + + def test_context_table_is_restored_after_execute_steps_with_table(self): + doc = u""" +@@ -445,7 +444,7 @@ Then a step passes + original_table = "" + self.context.table = original_table + self.context.execute_steps(doc) +- eq_(self.context.table, original_table) ++ assert self.context.table == original_table + + def test_context_text_is_restored_after_execute_steps_without_text(self): + doc = u""" +@@ -456,7 +455,7 @@ Then a step passes + original_text = "" + self.context.text = original_text + self.context.execute_steps(doc) +- eq_(self.context.text, original_text) ++ assert self.context.text == original_text + + def test_context_text_is_restored_after_execute_steps_with_text(self): + doc = u''' +@@ -471,10 +470,10 @@ When a step with text: + original_text = "" + self.context.text = original_text + self.context.execute_steps(doc) +- eq_(self.context.text, original_text) ++ assert self.context.text == original_text + + +- @raises(ValueError) ++ # OLD: @raises(ValueError) + def test_execute_steps_should_fail_when_called_without_feature(self): + doc = u""" + Given a passes +@@ -482,7 +481,8 @@ Then a step passes + """.lstrip() + with patch("behave.step_registry.registry", self.step_registry): + self.context.feature = None +- self.context.execute_steps(doc) ++ with pytest.raises(ValueError): ++ self.context.execute_steps(doc) + + + def create_mock_config(): +@@ -588,11 +588,11 @@ class TestRunner(object): + r.setup_capture() + r.start_capture() + +- eq_(sys.stdout, r.capture_controller.stdout_capture) ++ assert sys.stdout == r.capture_controller.stdout_capture + + r.stop_capture() + +- eq_(sys.stdout, new_stdout) ++ assert sys.stdout == new_stdout + + sys.stdout = old_stdout + +@@ -605,11 +605,11 @@ class TestRunner(object): + + r.start_capture() + +- eq_(sys.stdout, old_stdout) ++ assert sys.stdout == old_stdout + + r.stop_capture() + +- eq_(sys.stdout, old_stdout) ++ assert sys.stdout == old_stdout + + def test_teardown_capture_removes_log_tap(self): + r = runner.Runner(Mock()) +@@ -633,7 +633,7 @@ class TestRunner(object): + # pylint: disable=too-many-format-args + assert "spam" in l, '"spam" variable not set in locals (%r)' % (g, l) + # pylint: enable=too-many-format-args +- eq_(l["spam"], fn) ++ assert l["spam"] == fn + + def test_run_returns_true_if_everything_passed(self): + r = runner.Runner(Mock()) +@@ -694,11 +694,11 @@ class TestRunWithPaths(unittest.TestCase): + self.runner.context = Mock() + self.runner.run_with_paths() + +- eq_(self.run_hook.call_args_list, [ ++ assert self.run_hook.call_args_list == [ + ((), {}), + (("before_all", self.runner.context), {}), + (("after_all", self.runner.context), {}), +- ]) ++ ] + + @patch("behave.parser.parse_file") + @patch("os.path.abspath") +@@ -724,8 +724,8 @@ class TestRunWithPaths(unittest.TestCase): + + expected_parse_file_args = \ + [((x.upper(),), {"language": "fritz"}) for x in feature_locations] +- eq_(parse_file.call_args_list, expected_parse_file_args) +- eq_(self.runner.features, [feature] * 3) ++ assert parse_file.call_args_list == expected_parse_file_args ++ assert self.runner.features == [feature] * 3 + + + class FsMock(object): +@@ -829,9 +829,12 @@ class TestFeatureDirectory(object): + + # will look for a "features" directory and not find one + with patch("os.path", fs): +- assert_raises(ConfigError, r.setup_paths) ++ with pytest.raises(ConfigError): ++ r.setup_paths() ++ # OLD: assert_raises(ConfigError, r.setup_paths) + +- ok_(("isdir", os.path.join(fs.base, "features", "steps")) in fs.calls) ++ assert ("isdir", os.path.join(fs.base, "features", "steps")) in fs.calls ++ # ok_(("isdir", os.path.join(fs.base, "features", "steps")) in fs.calls) + + def test_default_path_no_features(self): + config = create_mock_config() +@@ -842,7 +845,9 @@ class TestFeatureDirectory(object): + fs = FsMock("features/steps/") + with patch("os.path", fs): + with patch("os.walk", fs.walk): +- assert_raises(ConfigError, r.setup_paths) ++ with pytest.raises(ConfigError): ++ r.setup_paths() ++ # OLD: assert_raises(ConfigError, r.setup_paths) + + def test_default_path(self): + config = create_mock_config() +@@ -857,7 +862,7 @@ class TestFeatureDirectory(object): + with r.path_manager: + r.setup_paths() + +- eq_(r.base_dir, os.path.abspath("features")) ++ assert r.base_dir == os.path.abspath("features") + + def test_supplied_feature_file(self): + config = create_mock_config() +@@ -872,10 +877,12 @@ class TestFeatureDirectory(object): + with patch("os.walk", fs.walk): + with r.path_manager: + r.setup_paths() +- ok_(("isdir", os.path.join(fs.base, "steps")) in fs.calls) +- ok_(("isfile", os.path.join(fs.base, "foo.feature")) in fs.calls) ++ assert ("isdir", os.path.join(fs.base, "steps")) in fs.calls ++ assert ("isfile", os.path.join(fs.base, "foo.feature")) in fs.calls ++ # OLD: ok_(("isdir", os.path.join(fs.base, "steps")) in fs.calls) ++ # OLD: ok_(("isfile", os.path.join(fs.base, "foo.feature")) in fs.calls) + +- eq_(r.base_dir, fs.base) ++ assert r.base_dir == fs.base + + def test_supplied_feature_file_no_steps(self): + config = create_mock_config() +@@ -888,7 +895,9 @@ class TestFeatureDirectory(object): + with patch("os.path", fs): + with patch("os.walk", fs.walk): + with r.path_manager: +- assert_raises(ConfigError, r.setup_paths) ++ with pytest.raises(ConfigError): ++ r.setup_paths() ++ # OLD: assert_raises(ConfigError, r.setup_paths) + + def test_supplied_feature_directory(self): + config = create_mock_config() +@@ -903,9 +912,10 @@ class TestFeatureDirectory(object): + with r.path_manager: + r.setup_paths() + +- ok_(("isdir", os.path.join(fs.base, "spam", "steps")) in fs.calls) ++ assert ("isdir", os.path.join(fs.base, "spam", "steps")) in fs.calls ++ # OLD ok_(("isdir", os.path.join(fs.base, "spam", "steps")) in fs.calls) + +- eq_(r.base_dir, os.path.join(fs.base, "spam")) ++ assert r.base_dir == os.path.join(fs.base, "spam") + + def test_supplied_feature_directory_no_steps(self): + config = create_mock_config() +@@ -917,9 +927,12 @@ class TestFeatureDirectory(object): + + with patch("os.path", fs): + with patch("os.walk", fs.walk): +- assert_raises(ConfigError, r.setup_paths) ++ with pytest.raises(ConfigError): ++ r.setup_paths() ++ # OLD: assert_raises(ConfigError, r.setup_paths) + +- ok_(("isdir", os.path.join(fs.base, "spam", "steps")) in fs.calls) ++ assert ("isdir", os.path.join(fs.base, "spam", "steps")) in fs.calls ++ # OLD: ok_(("isdir", os.path.join(fs.base, "spam", "steps")) in fs.calls) + + def test_supplied_feature_directory_missing(self): + config = create_mock_config() +@@ -931,7 +944,9 @@ class TestFeatureDirectory(object): + + with patch("os.path", fs): + with patch("os.walk", fs.walk): +- assert_raises(ConfigError, r.setup_paths) ++ with pytest.raises(ConfigError): ++ r.setup_paths() ++ # OLD: assert_raises(ConfigError, r.setup_paths) + + + class TestFeatureDirectoryLayout2(object): +@@ -955,7 +970,7 @@ class TestFeatureDirectoryLayout2(object): + with r.path_manager: + r.setup_paths() + +- eq_(r.base_dir, os.path.abspath("features")) ++ assert r.base_dir == os.path.abspath("features") + + def test_supplied_root_directory(self): + config = create_mock_config() +@@ -975,8 +990,9 @@ class TestFeatureDirectoryLayout2(object): + with r.path_manager: + r.setup_paths() + +- ok_(("isdir", os.path.join(fs.base, "features", "steps")) in fs.calls) +- eq_(r.base_dir, os.path.join(fs.base, "features")) ++ # OLD: ok_(("isdir", os.path.join(fs.base, "features", "steps")) in fs.calls) ++ assert ("isdir", os.path.join(fs.base, "features", "steps")) in fs.calls ++ assert r.base_dir == os.path.join(fs.base, "features") + + def test_supplied_root_directory_no_steps(self): + config = create_mock_config() +@@ -993,10 +1009,13 @@ class TestFeatureDirectoryLayout2(object): + with patch("os.path", fs): + with patch("os.walk", fs.walk): + with r.path_manager: +- assert_raises(ConfigError, r.setup_paths) ++ with pytest.raises(ConfigError): ++ r.setup_paths() ++ # OLD: assert_raises(ConfigError, r.setup_paths) + +- ok_(("isdir", os.path.join(fs.base, "features", "steps")) in fs.calls) +- eq_(r.base_dir, None) ++ # OLD: ok_(("isdir", os.path.join(fs.base, "features", "steps")) in fs.calls) ++ assert ("isdir", os.path.join(fs.base, "features", "steps")) in fs.calls ++ assert r.base_dir is None + + + def test_supplied_feature_file(self): +@@ -1018,9 +1037,11 @@ class TestFeatureDirectoryLayout2(object): + with r.path_manager: + r.setup_paths() + +- ok_(("isdir", os.path.join(fs.base, "features", "steps")) in fs.calls) +- ok_(("isfile", os.path.join(fs.base, "features", "group1", "foo.feature")) in fs.calls) +- eq_(r.base_dir, fs.join(fs.base, "features")) ++ # OLD: ok_(("isdir", os.path.join(fs.base, "features", "steps")) in fs.calls) ++ # OLD: ok_(("isfile", os.path.join(fs.base, "features", "group1", "foo.feature")) in fs.calls) ++ assert ("isdir", os.path.join(fs.base, "features", "steps")) in fs.calls ++ assert ("isfile", os.path.join(fs.base, "features", "group1", "foo.feature")) in fs.calls ++ assert r.base_dir == fs.join(fs.base, "features") + + def test_supplied_feature_file_no_steps(self): + config = create_mock_config() +@@ -1037,7 +1058,9 @@ class TestFeatureDirectoryLayout2(object): + with patch("os.path", fs): + with patch("os.walk", fs.walk): + with r.path_manager: +- assert_raises(ConfigError, r.setup_paths) ++ with pytest.raises(ConfigError): ++ r.setup_paths() ++ # OLD assert_raises(ConfigError, r.setup_paths) + + def test_supplied_feature_directory(self): + config = create_mock_config() +@@ -1057,8 +1080,9 @@ class TestFeatureDirectoryLayout2(object): + with r.path_manager: + r.setup_paths() + +- ok_(("isdir", os.path.join(fs.base, "features", "steps")) in fs.calls) +- eq_(r.base_dir, os.path.join(fs.base, "features")) ++ # OLD: ok_(("isdir", os.path.join(fs.base, "features", "steps")) in fs.calls) ++ assert ("isdir", os.path.join(fs.base, "features", "steps")) in fs.calls ++ assert r.base_dir == os.path.join(fs.base, "features") + + + def test_supplied_feature_directory_no_steps(self): +@@ -1075,6 +1099,9 @@ class TestFeatureDirectoryLayout2(object): + + with patch("os.path", fs): + with patch("os.walk", fs.walk): +- assert_raises(ConfigError, r.setup_paths) ++ with pytest.raises(ConfigError): ++ r.setup_paths() ++ # OLD: assert_raises(ConfigError, r.setup_paths) + +- ok_(("isdir", os.path.join(fs.base, "features", "steps")) in fs.calls) ++ # OLD: ok_(("isdir", os.path.join(fs.base, "features", "steps")) in fs.calls) ++ assert ("isdir", os.path.join(fs.base, "features", "steps")) in fs.calls +diff --git a/test/test_step_registry.py b/tests/unit/test_step_registry.py +similarity index 95% +rename from test/test_step_registry.py +rename to tests/unit/test_step_registry.py +index f5b5a4d..6f85729 100644 +--- a/test/test_step_registry.py ++++ b/tests/unit/test_step_registry.py +@@ -2,7 +2,6 @@ + # pylint: disable=unused-wildcard-import + from __future__ import absolute_import, with_statement + from mock import Mock, patch +-from nose.tools import * # pylint: disable=wildcard-import + from six.moves import range # pylint: disable=redefined-builtin + from behave import step_registry + +@@ -26,7 +25,7 @@ class TestStepRegistry(object): + + registry.add_step_definition(step_type.upper(), pattern, func) + get_matcher.assert_called_with(func, pattern) +- eq_(l, [magic_object]) ++ assert l == [magic_object] + + def test_find_match_with_specific_step_type_also_searches_generic(self): + registry = step_registry.StepRegistry() +@@ -80,7 +79,7 @@ class TestStepRegistry(object): + + assert registry.find_match(step) is magic_object + for mock in step_defs[6:]: +- eq_(mock.match.call_count, 0) ++ assert mock.match.call_count == 0 + + # pylint: disable=line-too-long + @patch.object(step_registry.registry, 'add_step_definition') +diff --git a/tests/unit/test_textutil.py b/tests/unit/test_textutil.py +index e05e9ad..3ffab3c 100644 +--- a/tests/unit/test_textutil.py ++++ b/tests/unit/test_textutil.py +@@ -9,6 +9,10 @@ import pytest + import codecs + import six + ++ ++pytest_version = pytest.__version__ ++ ++ + # ----------------------------------------------------------------------------- + # TEST SUPPORT: + # ----------------------------------------------------------------------------- +@@ -263,6 +267,7 @@ class TestObjectToTextConversion(object): + assert message in text2, "OOPS: text=%r" % text2 + + @requires_python2 ++ @pytest.mark.skipif(pytest_version >= "5.0", reason="Fails with pytest 5.0") + @pytest.mark.parametrize("exception_class, message", [ + (AssertionError, u"Ärgernis"), + (RuntimeError, u"Übermütig"), +@@ -274,10 +279,15 @@ class TestObjectToTextConversion(object): + with pytest.raises(exception_class) as e: + raise exception_class(bytes_message) + +- text2 = text(e.value) ++ # -- REQUIRES: pytest < 5.0 ++ # HINT: pytest >= 5.0 needs: text(e.value) ++ # NEW: text2 = text(e.value) # Causes problems w/ decoding and comparison. ++ assert isinstance(e.value, Exception) ++ text2 = text(e) + unicode_message = bytes_message.decode(self.ENCODING) + expected = u"%s: %s" % (exception_class.__name__, unicode_message) + assert isinstance(text2, six.text_type) ++ assert unicode_message in text2 + assert text2.endswith(expected) + # -- DIAGNOSTICS: + print(u"text2: "+ text2) +diff --git a/tox.ini b/tox.ini +index 392bb39..d2fbce2 100644 +--- a/tox.ini ++++ b/tox.ini +@@ -67,17 +67,11 @@ deps= + install_command = pip install -U {opts} {packages} + changedir = {toxinidir} + commands= +- pytest {posargs:test tests} ++ pytest {posargs:tests} + behave --format=progress {posargs:features} + behave --format=progress {posargs:tools/test-features} + behave --format=progress {posargs:issue.features} +-deps= +- pytest>=3.0 +- pytest-html >= 1.19.0 +- nose>=1.3 +- mock>=2.0 +- PyHamcrest>=1.9 +- path.py >= 10.1 ++deps= -r {toxinidir}/py.requirements/ci.tox.txt + setenv = + PYTHONPATH = {toxinidir} + +@@ -97,13 +91,12 @@ changedir = {envdir} + commands= + behave --version + {toxinidir}/bin/toxcmd.py copytree ../../behave4cmd0 . +- {toxinidir}/bin/toxcmd.py copytree ../../test . + {toxinidir}/bin/toxcmd.py copytree ../../tests . + {toxinidir}/bin/toxcmd.py copytree ../../features . + {toxinidir}/bin/toxcmd.py copytree ../../tools . + {toxinidir}/bin/toxcmd.py copytree ../../issue.features . + {toxinidir}/bin/toxcmd.py copy ../../behave.ini . +- pytest {posargs:test tests} ++ pytest {posargs:tests} + behave --format=progress {posargs:features} + behave --format=progress {posargs:tools/test-features} + behave --format=progress {posargs:issue.features} +@@ -119,18 +112,16 @@ changedir = {envdir} + commands= + behave --version + {toxinidir}/bin/toxcmd.py copytree ../../behave4cmd0 . +- {toxinidir}/bin/toxcmd.py copytree ../../test . + {toxinidir}/bin/toxcmd.py copytree ../../tests . + {toxinidir}/bin/toxcmd.py copytree ../../features . + {toxinidir}/bin/toxcmd.py copytree ../../tools . + {toxinidir}/bin/toxcmd.py copytree ../../issue.features . + {toxinidir}/bin/toxcmd.py copy ../../behave.ini . + {toxinidir}/bin/toxcmd.py 2to3 -w -n --no-diffs behave4cmd0 +- {toxinidir}/bin/toxcmd.py 2to3 -w -n --no-diffs test + {toxinidir}/bin/toxcmd.py 2to3 -w -n --no-diffs tools + {toxinidir}/bin/toxcmd.py 2to3 -w -n --no-diffs features + {toxinidir}/bin/toxcmd.py 2to3 -w -n --no-diffs issue.features +- pytest {posargs:test tests} ++ pytest {posargs:tests} + behave --format=progress {posargs:features} + behave --format=progress {posargs:tools/test-features} + behave --format=progress {posargs:issue.features} +@@ -148,7 +139,7 @@ setenv = + [testenv:jy27] + basepython= jython + commands= +- pytest {posargs:test tests} ++ pytest {posargs:tests} + behave --format=progress {posargs:features} + behave --format=progress {posargs:tools/test-features} + behave --format=progress {posargs:issue.features} diff --git a/meta-python/recipes-devtools/python/python3-behave/0036-FIX-Remove-test-from-pytest-run.patch b/meta-python/recipes-devtools/python/python3-behave/0036-FIX-Remove-test-from-pytest-run.patch new file mode 100644 index 000000000..7fe451b2f --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0036-FIX-Remove-test-from-pytest-run.patch @@ -0,0 +1,22 @@ +From 2c886d201c19085ab52065d47e4f86a82a77f991 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sat, 6 Jul 2019 09:17:54 +0200 +Subject: [PATCH] FIX: Remove test/ from pytest run. + +--- + .travis.yml | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/.travis.yml b/.travis.yml +index fbc3520..c6027e0 100644 +--- a/.travis.yml ++++ b/.travis.yml +@@ -35,7 +35,7 @@ install: + + script: + - python --version +- - pytest test tests ++ - pytest tests + - behave -f progress --junit features/ + - behave -f progress --junit tools/test-features/ + - behave -f progress --junit issue.features/ diff --git a/meta-python/recipes-devtools/python/python3-behave/0037-Select-by-location-Add-support-for-Scenario-containe.patch b/meta-python/recipes-devtools/python/python3-behave/0037-Select-by-location-Add-support-for-Scenario-containe.patch new file mode 100644 index 000000000..3e9a46a0c --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0037-Select-by-location-Add-support-for-Scenario-containe.patch @@ -0,0 +1,652 @@ +From 57a9cc0e1c99b0ed2ec5dbfd3fcfce456cfe13c8 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sat, 6 Jul 2019 09:50:46 +0200 +Subject: [PATCH] Select-by-location: Add support for "Scenario container" + (Feature, Rule, ScenarioOutline) (related to: #391) + +--- + CHANGES.rst | 2 +- + behave/model.py | 49 +++-- + behave/runner_util.py | 186 +++++++++++++++++- + ....select_scenarios_by_file_location.feature | 27 ++- + pytest.ini | 2 +- + tests/unit/test_runner_util.py | 175 ++++++++++++++++ + 6 files changed, 416 insertions(+), 25 deletions(-) + create mode 100644 tests/unit/test_runner_util.py + +diff --git a/CHANGES.rst b/CHANGES.rst +index 3d805b3..01bd1bd 100644 +--- a/CHANGES.rst ++++ b/CHANGES.rst +@@ -28,7 +28,7 @@ ENHANCEMENTS: + * Use `cucumber-tag-expressions`_ with tag-matching extension (superceeds: old-style tag-expressions) + * issue #678: Scenario Outline: Support tags with commas and semicolons (provided by: lawnmowerlatte, pull #679) + * issue #675: Feature files cannot be found within symlink directories (provided by: smadness, pull #680) +- ++* Select-by-location: Add support for "Scenario container" (Feature, Rule, ScenarioOutline) (related to: #391) + + PARTIALLY FIXED: + +diff --git a/behave/model.py b/behave/model.py +index 3084850..7fc534a 100644 +--- a/behave/model.py ++++ b/behave/model.py +@@ -55,11 +55,11 @@ class ScenarioContainer(TagAndStatusStatement, Replayable): + .. attribute:: keyword + + This is the keyword as seen in the *feature file*. In English this will +- be "Feature". ++ be "Feature" or "Rule". + + .. attribute:: name + +- The name of the feature (the text after "Feature".) ++ The name (or title) of the model entity (the text after the keyword.) + + .. attribute:: description + +@@ -93,12 +93,16 @@ class ScenarioContainer(TagAndStatusStatement, Replayable): + + Status.untested + The feature was has not been completely tested yet. ++ + Status.skipped +- One or more steps of this feature was passed over during testing. ++ The execution of this model entity (feature or rule) ++ should be / was skipped (excluded from the test run). ++ + Status.passed +- The feature was tested successfully. ++ The model entity (feature or rule) was tested successfully. ++ + Status.failed +- One or more steps of this feature failed. ++ One or more run items of this model entity failed. + + .. versionchanged:: 1.2.6 + Use Status enum class (was: string). +@@ -147,6 +151,11 @@ class ScenarioContainer(TagAndStatusStatement, Replayable): + for run_item in self.run_items: + run_item.reset() + ++ def _setup_context_for_run(self, context): ++ """Setup/Init runner context for run.""" ++ # -- OVERRIDDEN: By derived classes. ++ pass ++ + def __iter__(self): + return iter(self.run_items) + +@@ -204,7 +213,7 @@ class ScenarioContainer(TagAndStatusStatement, Replayable): + # feature_duration = self.run_endtime - self.run_starttime + return feature_duration + +- def walk_scenarios(self, with_outlines=False): ++ def walk_scenarios(self, with_outlines=False, with_rules=False): + """Provides a flat list of all scenarios of this ScenarioContainer. + A ScenarioOutline element adds its scenarios to this list. + But the ScenarioOutline element itself is only added when specified. +@@ -215,20 +224,20 @@ class ScenarioContainer(TagAndStatusStatement, Replayable): + :param with_outlines: If ScenarioOutline items should be added, too. + :return: List of all scenarios of this feature. + """ +- # TODO: Better use self.run_items + all_scenarios = [] +- # for scenario in self.scenarios: + for run_item in self.run_items: + if isinstance(run_item, Rule): + rule = run_item ++ if with_rules: ++ all_scenarios.append(rule) + all_scenarios.extend(rule.walk_scenarios(with_outlines=with_outlines)) +- if isinstance(run_item, ScenarioOutline): ++ elif isinstance(run_item, ScenarioOutline): + scenario_outline = run_item + if with_outlines: + all_scenarios.append(scenario_outline) + all_scenarios.extend(scenario_outline.scenarios) + else: +- assert isinstance(run_item, Scenario) ++ assert isinstance(run_item, Scenario), "OOPS: %r" % run_item + all_scenarios.append(run_item) + return all_scenarios + +@@ -274,9 +283,9 @@ class ScenarioContainer(TagAndStatusStatement, Replayable): + assert self.status == Status.skipped or self.hook_failed + + def skip(self, reason=None, require_not_executed=False): +- """Skip executing this feature or the remaining parts of it. +- Note that this feature may be already partly executed +- when this function is called. ++ """Skip executing this model entity or the remaining parts of it. ++ Note that this model entity (feature or rule) may be already partly ++ executed when this function is called. + + :param reason: Optional reason why feature should be skipped (as string). + :param require_not_executed: Optional, requires that feature is not +@@ -314,8 +323,8 @@ class ScenarioContainer(TagAndStatusStatement, Replayable): + hook_after_entity = "after_{0}".format(entity_name) + + runner.context._push(layer_name=entity_name) # pylint: disable=protected-access +- runner.context.feature = self + runner.context.tags = set(self.tags) ++ self._setup_context_for_run(runner.context) + + skip_entity_untested = runner.aborted + should_run_entity = self.should_run(runner.config) +@@ -497,6 +506,9 @@ class Feature(ScenarioContainer): + (self.name, len(self.run_items), + len(self.rules), len(self.scenarios)) + ++ def _setup_context_for_run(self, context): ++ context.feature = self ++ + def add_rule(self, rule): + """Add a rule to this feature.""" + feature = self +@@ -619,6 +631,9 @@ class Rule(ScenarioContainer): + self.parent = parent + self.feature = parent + ++ def _setup_context_for_run(self, context): ++ context.rule = self ++ + def __repr__(self): + return '' % \ + (self.name, len(self.scenarios)) +@@ -664,6 +679,10 @@ class Background(BasicStatement, Replayable): + + .. _`background`: gherkin.html#backgrounds + """ ++ # TODO: Background inheritance ++ # Rule.background should inherit its Feature.background steps (if available) ++ # Rule.background = Feature.background iff not Rule.background exists (ALREADY-SOLVED) ++ # Rule may override background inheritance mechanism + type = "background" + + def __init__(self, filename, line, keyword, name, steps=None, description=None): +@@ -796,7 +815,7 @@ class Scenario(TagAndStatusStatement, Replayable): + + @property + def background_steps(self): +- """Provide background steps if feature has a background. ++ """Provide background steps if feature/rule has a background. + Lazy init that copies the background steps. + + Note that a copy of the background steps is needed to ensure +diff --git a/behave/runner_util.py b/behave/runner_util.py +index 2210f78..7e0807f 100644 +--- a/behave/runner_util.py ++++ b/behave/runner_util.py +@@ -58,6 +58,100 @@ class FileLocationParser(object): + # ----------------------------------------------------------------------------- + # CLASSES: + # ----------------------------------------------------------------------------- ++from collections import OrderedDict ++from .model import Feature, Rule, ScenarioOutline, Scenario ++ ++ ++class FeatureLineDatabase(object): ++ """Helper class that supports select-by-location mechanism (FileLocation) ++ within a feature file by storing the feature line numbers for each entity. ++ ++ RESPONSIBILITY(s): ++ ++ * Can use the line number to select the best matching entity(s) in a feature ++ * Implements the select-by-location mechanism for each entity in the feature ++ """ ++ ++ def __init__(self, entity=None, line_data=None): ++ if entity and not line_data: ++ line_data = self.make_line_data_for(entity) ++ self.entity = entity ++ self.data = OrderedDict(line_data or []) ++ self._line_numbers = None ++ self._line_entities = None ++ ++ def select_run_item_by_line(self, line): ++ """Select one run-items by using the line number. ++ ++ * Exact match returns run-time entity (Feature, Rule, ScenarioOutline, Scenario) ++ * Any other line in between uses the predecessor entity ++ ++ :param line: Line number in Feature file (as int) ++ :return: Selected run-item object. ++ """ ++ run_item = self.data.get(line, None) ++ if run_item is None: ++ # -- CASE: BEST-MATCH in ordered line database ++ if self._line_numbers is None: ++ self._line_numbers = list(self.data.keys()) ++ self._line_entities = list(self.data.values()) ++ ++ pos = bisect(self._line_numbers, line) - 1 ++ if pos < 0: ++ pos = 0 ++ run_item = self._line_entities[pos] ++ return run_item ++ ++ def select_scenarios_by_line(self, line): ++ """Select one or more scenarios by using the line number. ++ ++ * line = 0: Selects all scenarios in the Feature file ++ * Feature / Rule / ScenarioOutline.location.line selects its scenarios ++ * Scenario.location.line selects the Scenario ++ * Any other lines use the predecessor entity (and its scenarios) ++ ++ :param line: Line number in Feature file (as int) ++ :return: List of selected scenarios ++ """ ++ run_item = self.select_run_item_by_line(line) ++ scenarios = [] ++ if isinstance(run_item, Feature): ++ scenarios = list(run_item.walk_scenarios()) ++ elif isinstance(run_item, Rule): ++ scenarios = list(run_item.walk_scenarios()) ++ elif isinstance(run_item, ScenarioOutline): ++ scenarios = list(run_item.scenarios) ++ elif isinstance(run_item, Scenario): ++ scenarios = [run_item] ++ return scenarios ++ ++ @classmethod ++ def make_line_data_for(cls, entity): ++ line_data = [] ++ run_items = [] ++ if isinstance(entity, Feature): ++ line_data.append((0, entity)) ++ run_items = entity.run_items ++ elif isinstance(entity, Rule): ++ run_items = entity.run_items ++ elif isinstance(entity, ScenarioOutline): ++ run_items = entity.scenarios ++ ++ line_data.append((entity.location.line, entity)) ++ for run_item in run_items: ++ line_data.extend(cls.make_line_data_for(run_item)) ++ # -- MAYBE: ++ # if isinstance(entity, ScenarioOutline) and run_items: ++ # # -- SPECIAL CASE: Lines after last Examples row => Use ScenarioOutline ++ # line_data.append((run_items[-1].location.line + 1, entity)) ++ return sorted(line_data) ++ ++ @classmethod ++ def make(cls, entity): ++ return cls(entity, cls.make_line_data_for(entity)) ++ ++ ++ + class FeatureScenarioLocationCollector(object): + """ + Collects FileLocation objects for a feature. +@@ -200,6 +294,94 @@ class FeatureScenarioLocationCollector(object): + return self.feature + + ++class FeatureScenarioLocationCollector1(FeatureScenarioLocationCollector): ++ ++ @staticmethod ++ def select_scenario_line_for(line, scenario_lines): ++ """ ++ Select scenario line for any given line. ++ ++ ALGORITHM: scenario.line <= line < next_scenario.line ++ ++ :param line: A line number in the file (as number). ++ :param scenario_lines: Sorted list of scenario lines. ++ :return: Scenario.line (first line) for the given line. ++ """ ++ if not scenario_lines: ++ return 0 # -- Select all scenarios. ++ pos = bisect(scenario_lines, line) - 1 ++ if pos < 0: ++ pos = 0 ++ return scenario_lines[pos] ++ ++ def discover_selected_scenarios(self, strict=False): ++ """ ++ Discovers selected scenarios based on the provided file locations. ++ In addition: ++ * discover all scenarios ++ * auto-correct BAD LINE-NUMBERS ++ ++ :param strict: If true, raises exception if file location is invalid. ++ :return: List of selected scenarios of this feature (as set). ++ :raises InvalidFileLocationError: ++ If file location is no exactly correct and strict is true. ++ """ ++ assert self.feature ++ if not self.all_scenarios: ++ self.all_scenarios = self.feature.walk_scenarios() ++ ++ # -- STEP: Check if lines are correct. ++ existing_lines = [scenario.line for scenario in self.all_scenarios] ++ selected_lines = list(self.scenario_lines) ++ for line in selected_lines: ++ new_line = self.select_scenario_line_for(line, existing_lines) ++ if new_line != line: ++ # -- AUTO-CORRECT BAD-LINE: ++ self.scenario_lines.remove(line) ++ self.scenario_lines.add(new_line) ++ if strict: ++ msg = "Scenario location '...:%d' should be: '%s:%d'" % \ ++ (line, self.filename, new_line) ++ raise InvalidFileLocationError(msg) ++ ++ # -- STEP: Determine selected scenarios and store them. ++ scenario_lines = set(self.scenario_lines) ++ selected_scenarios = set() ++ for scenario in self.all_scenarios: ++ if scenario.line in scenario_lines: ++ selected_scenarios.add(scenario) ++ scenario_lines.remove(scenario.line) ++ # -- CHECK ALL ARE RESOLVED: ++ assert not scenario_lines ++ return selected_scenarios ++ ++ ++class FeatureScenarioLocationCollector2(FeatureScenarioLocationCollector): ++ ++ def discover_selected_scenarios(self, strict=False): ++ """Discovers selected scenarios based on the provided file locations. ++ In addition: ++ * discover all scenarios ++ * auto-correct BAD LINE-NUMBERS ++ ++ :param strict: If true, raises exception if file location is invalid. ++ :return: List of selected scenarios of this feature (as set). ++ :raises InvalidFileLocationError: ++ If file location is no exactly correct and strict is true. ++ """ ++ assert self.feature ++ if not self.all_scenarios: ++ self.all_scenarios = self.feature.walk_scenarios() ++ ++ line_database = FeatureLineDatabase.make(self.feature) ++ selected_lines = list(self.scenario_lines) ++ selected_scenarios = set() ++ for line in selected_lines: ++ more_scenarios = line_database.select_scenarios_by_line(line) ++ selected_scenarios.update(more_scenarios) ++ return selected_scenarios ++ ++ + class FeatureListParser(object): + """ + Read textual file, ala '@features.txt'. This file contains: +@@ -304,7 +486,7 @@ def parse_features(feature_files, language=None): + :param language: Default language to use. + :return: List of feature objects. + """ +- scenario_collector = FeatureScenarioLocationCollector() ++ scenario_collector = FeatureScenarioLocationCollector2() + features = [] + for location in feature_files: + if not isinstance(location, FileLocation): +@@ -315,7 +497,7 @@ def parse_features(feature_files, language=None): + scenario_collector.add_location(location) + continue + elif scenario_collector.feature: +- # -- ADD CURRENT FEATURE: As collection of scenarios. ++ # -- NEW FEATURE DETECTED: Add current feature. + current_feature = scenario_collector.build_feature() + features.append(current_feature) + scenario_collector.clear() +diff --git a/features/runner.select_scenarios_by_file_location.feature b/features/runner.select_scenarios_by_file_location.feature +index f60c43f..69e23fe 100644 +--- a/features/runner.select_scenarios_by_file_location.feature ++++ b/features/runner.select_scenarios_by_file_location.feature +@@ -13,15 +13,28 @@ Feature: Select Scenarios by File Location + . * A file location with filename but without line number + . refers to the complete file + . * A file location with line number 0 (zero) refers to the complete file ++ . * A file location within a scenario container (Feature, Rule, ScenarioOutline), ++ . that does not refer to the file location within a scenario, ++ . selects all scenarios of this scenario container. + . + . SPECIFICATION: Scenario selection by file locations + . * scenario.line == file_location.line selects scenario (preferred method). + . * Any line number in the following range is acceptable: +- . scenario.line <= file_location.line < next_scenario.line +- . * The first scenario is selected, +- . if the file location line number is less than first scenario.line. ++ . scenario.line <= file_location.line < next_entity.line (maybe: scenario) ++ . * If the file location line number is less than first scenario.line, ++ . the preceeding scenario container (Feature or Rule) is selected. + . * The last scenario is selected, + . if the file location line number is greater than the lines in the file. ++ . * For ScenarioOutline.scenarios: ++ . scenario.line == ScenarioOutline.examples[x].row.line ++ . The line number of the Examples row that created the scenario is assigned to it. ++ . ++ . SPECIFICATION: "Scenario container" selection by file locations ++ . * Scenario containers are: Feature, Rule, ScenarioOutline ++ . * A file location that points into the matching range of a scenario container, ++ . selects all scenarios / run-items within this scenario container. ++ . * Any line number in the following range selects the scenario container: ++ . entity.line <= file_location.line < next_entity.line (maybe: child) + . + . SPECIFICATION: Runner with scenario locations (file locations) + . * Adjacent file locations are merged if they refer to the same file, like: +@@ -162,22 +175,24 @@ Feature: Select Scenarios by File Location + """ + + @file_location.select_first +- Scenario: Select first scenario if line number is smaller than first scenario line ++ Scenario: Select all scenarios if line number is smaller than first scenario line + + CASE: 0 < file_location.line < first_scenario.line ++ HINT: Any line number outside of a scenario may point into a "scenario container". ++ In this case, all the scenarios of the scenario container are selected. + + When I run "behave -f plain --dry-run --no-skipped features/alice.feature:1" + Then it should pass with: + """ + 0 features passed, 0 failed, 0 skipped, 1 untested +- 0 scenarios passed, 0 failed, 1 skipped, 1 untested ++ 0 scenarios passed, 0 failed, 0 skipped, 2 untested + """ + And the command output should contain: + """ + Feature: Alice + Scenario: Alice First + """ +- But the command output should not contain: ++ But the command output should contain: + """ + Scenario: Alice Last + """ +diff --git a/pytest.ini b/pytest.ini +index a686596..ff2a8a2 100644 +--- a/pytest.ini ++++ b/pytest.ini +@@ -16,7 +16,7 @@ + # ============================================================================ + + [pytest] +-minversion = 4.2 ++minversion = 2.8 + testpaths = tests + python_files = test_*.py + addopts = --metadata PACKAGE_UNDER_TEST behave +diff --git a/tests/unit/test_runner_util.py b/tests/unit/test_runner_util.py +new file mode 100644 +index 0000000..b5019b8 +--- /dev/null ++++ b/tests/unit/test_runner_util.py +@@ -0,0 +1,175 @@ ++# -*- coding: UTF-8 -*- ++ ++from __future__ import absolute_import, print_function ++from collections import OrderedDict ++from behave.runner_util import FeatureLineDatabase ++from behave.parser import parse_feature ++from behave.model import Feature, Rule, ScenarioOutline, Scenario, Background ++import pytest ++ ++ ++# --------------------------------------------------------------------------------------- ++# TEST DATA: FeatureLineDatabase ++# --------------------------------------------------------------------------------------- ++feature_text1 = u""" ++ Feature: Alice ++ Background: Alice.Background ++ Given a background step passes ++ ++ Scenario: A1 ++ Given a scenario step passes ++ ++ Scenario: A2 ++ Given a scenario step passes ++ When a scenario step passes ++ """ ++ ++feature_text_with_scenario_outline = u""" ++ Feature: Bob ++ ++ Scenario Outline: Bob.SO_2_ ++ Given a person with name "" ++ Then the person is born in ++ ++ Examples: ++ | Name | Birthyear | ++ | Alice | 1990 | ++ | Bob | 1991 | ++ ++ Scenario: Bob.S3 ++ Given a scenario step passes ++ When a scenario step passes ++ """ ++ ++feature_text_with_rule = u""" ++ Feature: Charly ++ Background: Charly.Background ++ Given a background step passes ++ ++ Scenario: C1 ++ Given a scenario step passes ++ ++ Rule: Charly.Rule_1 ++ ++ Scenario: Rule_1.C2 ++ Given a scenario step passes ++ When a scenario step passes ++ """ ++ ++feature_file_map = { ++ "basic.feature": feature_text1, ++ "scenario_outline.feature": feature_text_with_scenario_outline, ++ "rule.feature": feature_text_with_rule, ++} ++ ++# --------------------------------------------------------------------------------------- ++# TEST SUITE FOR: FeatureLineDatabase ++# --------------------------------------------------------------------------------------- ++class TestFeatureLineDatabase(object): ++ def test_make(self): ++ feature = parse_feature(feature_text1.strip(), ++ filename="features/Alice.feature") ++ scenario_0 = feature.scenarios[0] ++ scenario_1 = feature.scenarios[1] ++ ++ line_database = FeatureLineDatabase.make(feature) ++ expected = OrderedDict([ ++ (0, feature), ++ (feature.location.line, feature), ++ (scenario_0.line, scenario_0), ++ (scenario_1.line, scenario_1), ++ ]) ++ assert line_database.data == expected ++ assert feature.location.line == 1 ++ ++ def test_make__with_scenario_outline(self): ++ feature = parse_feature(feature_text_with_scenario_outline.strip(), ++ filename="features/Bob.feature") ++ scenarios = feature.walk_scenarios(with_outlines=True) ++ scenario_outline = scenarios[0] ++ assert scenario_outline is feature.run_items[0] ++ scenario_1 = scenarios[1] ++ scenario_2 = scenarios[2] ++ scenario_3 = scenarios[3] ++ ++ line_database = FeatureLineDatabase.make(feature) ++ expected = OrderedDict([ ++ (0, feature), ++ (feature.location.line, feature), ++ (scenario_outline.line, scenario_outline), ++ (scenario_1.line, scenario_1), ++ (scenario_2.line, scenario_2), ++ (scenario_3.line, scenario_3), ++ ]) ++ assert line_database.data == expected ++ assert feature.location.line < scenario_outline.location.line ++ assert scenario_outline.location.line < scenario_1.location.line ++ assert scenario_1.location.line < scenario_2.location.line ++ assert scenario_2.location.line < scenario_3.location.line ++ ++ ++ def test_select_run_items_by_line__feature_line_selects_feature(self): ++ feature = parse_feature(feature_text1, filename="features/Alice.feature") ++ line_database = FeatureLineDatabase.make(feature) ++ selected = line_database.select_run_item_by_line(feature.location.line) ++ assert selected is feature ++ assert isinstance(selected, Feature) ++ ++ @pytest.mark.parametrize("filename", [ ++ "basic.feature", "scenario_outline.feature", "rule.feature" ++ ]) ++ def test_select_run_items_by_line__entity_line_selects_entity(self, filename): ++ feature_text = feature_file_map[filename] ++ feature = parse_feature(feature_text, filename=filename) ++ line_database = FeatureLineDatabase.make(feature) ++ last_line = 0 ++ all_run_items = feature.walk_scenarios(with_outlines=True, with_rules=True) ++ for run_item in all_run_items: ++ selected = line_database.select_run_item_by_line(run_item.location.line) ++ assert selected is run_item ++ assert last_line < selected.location.line ++ last_line = run_item.location.line ++ ++ @pytest.mark.parametrize("filename", [ ++ "basic.feature", "scenario_outline.feature", "rule.feature" ++ ]) ++ def test_select_run_items_by_line__line_before_entity_selects_last_entity(self, filename): ++ feature_text = feature_file_map[filename] ++ feature = parse_feature(feature_text, filename=filename) ++ line_database = FeatureLineDatabase.make(feature) ++ all_run_items = feature.walk_scenarios(with_outlines=True, with_rules=True) ++ last_run_item = feature ++ for run_item in all_run_items: ++ predecessor_line = run_item.location.line - 1 ++ selected = line_database.select_run_item_by_line(predecessor_line) ++ assert selected is last_run_item ++ assert selected is not run_item ++ last_run_item = run_item ++ ++ @pytest.mark.parametrize("filename", [ ++ "basic.feature", "scenario_outline.feature", "rule.feature" ++ ]) ++ def test_select_run_items_by_line__line_after_entity_selects_entity(self, filename): ++ # -- HINT: In most cases ++ # EXCEPT: ++ # * Scenarios of ScenarioOutline: scenario.line == SO.examples.row.line ++ # * Empty entity without steps is directly followed by other entity ++ feature_text = feature_file_map[filename] ++ feature = parse_feature(feature_text, filename=filename) ++ line_database = FeatureLineDatabase.make(feature) ++ all_run_items = feature.walk_scenarios(with_outlines=True, with_rules=True) ++ file_end_line = all_run_items[-1].location.line + 1000 ++ for index, run_item in enumerate(all_run_items): ++ next_line = run_item.location.line + 1 ++ next_entity_line = file_end_line ++ if index+1 < len(all_run_items): ++ next_entity = all_run_items[index+1] ++ next_entity_line = next_entity.line ++ if next_line >= next_entity_line: ++ # -- EXCLUDE: Scenarios in a ScenarioOutline ++ print("EXCLUDED: %s: %s (line=%s)" % ++ (run_item.keyword, run_item.name, run_item.line)) ++ continue ++ ++ selected = line_database.select_run_item_by_line(next_line) ++ assert selected is run_item diff --git a/meta-python/recipes-devtools/python/python3-behave/0038-docs-Add-description-for-Select-by-location-for-Scen.patch b/meta-python/recipes-devtools/python/python3-behave/0038-docs-Add-description-for-Select-by-location-for-Scen.patch new file mode 100644 index 000000000..14394603f --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0038-docs-Add-description-for-Select-by-location-for-Scen.patch @@ -0,0 +1,58 @@ +From a6388c0e97e4378a4ef628361a46ff6d02d3159e Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sat, 6 Jul 2019 10:50:54 +0200 +Subject: [PATCH] docs: Add description for "Select-by-location for Scenario + Containers" + +--- + docs/new_and_noteworthy_v1.2.7.rst | 33 ++++++++++++++++++++++++++++++ + 1 file changed, 33 insertions(+) + +diff --git a/docs/new_and_noteworthy_v1.2.7.rst b/docs/new_and_noteworthy_v1.2.7.rst +index 80d9576..ff1cd1f 100644 +--- a/docs/new_and_noteworthy_v1.2.7.rst ++++ b/docs/new_and_noteworthy_v1.2.7.rst +@@ -7,6 +7,7 @@ Summary: + * Use/Support :pypi:`cucumber-tag-expressions` (superceed: old-style tag-expressions) + * :pypi:`cucumber-tag-expressions` are extended by "tag-matching" + to match partial tag names, like: ``@foo.*`` ++* `Select-by-location for Scenario Containers`_ (Feature, Rule, ScenarioOutline) + + .. _`Example Mapping`: https://cucumber.io/blog/example-mapping-introduction/ + .. _`Example Mapping Webinar`: https://cucumber.io/blog/example-mapping-webinar/ +@@ -102,3 +103,35 @@ Overview of the `Example Mapping`_ concepts: + + + .. include:: _content.tag_expressions_v2.rst ++ ++ ++Select-by-location for Scenario Containers ++------------------------------------------------------------------------------- ++ ++In the past, it was already possible to scenario(s) by using its **file-location**. ++ ++A **file-location** has the schema: ``:``. ++Example: ``features/alice.feature:12`` ++(refers to ``line 12`` in ``features/alice.feature`` file). ++ ++Rules to select **Scenarios** by using the file-location: ++ ++* **Scenario:** Use a file-location that points to the keyword/title or its steps ++ (until next Scenario/Entity starts). ++ ++* **Scenario of a ScenarioOutline:** ++ Use the file-location of its Examples row. ++ ++Now you can select all entities of a **Scenario Container** (Feature, Rule, ScenarioOutline): ++ ++* **Feature:** ++ Use file-location before first contained entity/Scenario starts. ++ ++* **Rule:** ++ Use file-location from keyword/title line to line before its first Scenario/Background. ++ ++* **ScenarioOutline:** ++ Use file-location from keyword/title line to line before its Examples rows. ++ ++A file-location into a **Scenario Container** selects all its entities ++(Scenarios, ...). diff --git a/meta-python/recipes-devtools/python/python3-behave/0039-tests-Fix-warnings-for-python3.patch b/meta-python/recipes-devtools/python/python3-behave/0039-tests-Fix-warnings-for-python3.patch new file mode 100644 index 000000000..2b80d5df1 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0039-tests-Fix-warnings-for-python3.patch @@ -0,0 +1,50 @@ +From 93f578698844d7276e5b2f287ee7ff3bcfc049bf Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sat, 6 Jul 2019 13:35:51 +0200 +Subject: [PATCH] tests: Fix warnings for python3. + +--- + tests/issues/test_issue0336.py | 1 + + tests/unit/test_parser.py | 11 +++++++---- + 2 files changed, 8 insertions(+), 4 deletions(-) + +diff --git a/tests/issues/test_issue0336.py b/tests/issues/test_issue0336.py +index eb4c3fd..09201ae 100644 +--- a/tests/issues/test_issue0336.py ++++ b/tests/issues/test_issue0336.py +@@ -52,6 +52,7 @@ AssertionError + assert file_line_text in text2 + + # @require_python2 ++ @pytest.mark.filterwarnings("ignore:invalid escape sequence") + def test__problem_exists_with_problematic_encoding(self): + """Test ensures that problem exists with encoding=unicode-escape""" + # -- NOTE: Explicit use of problematic encoding +diff --git a/tests/unit/test_parser.py b/tests/unit/test_parser.py +index ecbb1bf..01006f9 100644 +--- a/tests/unit/test_parser.py ++++ b/tests/unit/test_parser.py +@@ -564,16 +564,19 @@ Feature: Stuff + ('then', 'Then', 'stuff is in buckets', None, None), + ]) + ++ @pytest.mark.filterwarnings("ignore:invalid escape sequence") + def test_parses_feature_with_table_and_escaped_pipe_in_cell_values(self): ++ # -- HINT py37: DeprecationWarning: invalid escape sequence '\|' ++ # USE: Double escaped-backslashes. + doc = u''' + Feature: + Scenario: + Given we have special cell values: + | name | value | +- | alice | one\|two | +- | bob |\|one | +- | charly | one\|| +- | doro | one\|two\|three\|four | ++ | alice | one\\|two | ++ | bob |\\|one | ++ | charly | one\\|| ++ | doro | one\\|two\\|three\\|four | + '''.lstrip() + feature = parser.parse_feature(doc) + assert len(feature.scenarios) == 1 diff --git a/meta-python/recipes-devtools/python/python3-behave/0040-Use-cucumber-tag-expressions-1.1.2-to-fix-warnings.patch b/meta-python/recipes-devtools/python/python3-behave/0040-Use-cucumber-tag-expressions-1.1.2-to-fix-warnings.patch new file mode 100644 index 000000000..85c1bc16a --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0040-Use-cucumber-tag-expressions-1.1.2-to-fix-warnings.patch @@ -0,0 +1,55 @@ +From 21a399bf6e2e2eb09c98d173f1d0975bcecbf1e5 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sat, 6 Jul 2019 13:36:59 +0200 +Subject: [PATCH] Use cucumber-tag-expressions 1.1.2 (to fix warnings). + +py.requirements: Add twine, bump2version +--- + py.requirements/basic.txt | 2 +- + py.requirements/develop.txt | 6 +++++- + setup.py | 2 +- + 3 files changed, 7 insertions(+), 3 deletions(-) + +diff --git a/py.requirements/basic.txt b/py.requirements/basic.txt +index 3b71bfb..ad5b9a6 100644 +--- a/py.requirements/basic.txt ++++ b/py.requirements/basic.txt +@@ -8,7 +8,7 @@ + # * http://www.pip-installer.org/ + # ============================================================================ + +-cucumber-tag-expressions >= 1.1.1 ++cucumber-tag-expressions >= 1.1.2 + parse >= 1.8.2 + parse_type >= 0.4.2 + six >= 1.12.0 +diff --git a/py.requirements/develop.txt b/py.requirements/develop.txt +index d823389..a16d7bf 100644 +--- a/py.requirements/develop.txt ++++ b/py.requirements/develop.txt +@@ -11,7 +11,11 @@ pathlib; python_version <= '3.4' + pycmd + + # -- CONFIGURATION MANAGEMENT (helpers): +-bumpversion >= 0.4.0 ++# FORMER: bumpversion >= 0.4.0 ++bump2version >= 0.5.6 ++ ++# -- RELEASE MANAGEMENT: Push package to pypi. ++twine >= 1.13.0 + + # -- DEVELOPMENT SUPPORT: + tox >= 1.8.1 +diff --git a/setup.py b/setup.py +index 9c7560d..cea4392 100644 +--- a/setup.py ++++ b/setup.py +@@ -76,7 +76,7 @@ setup( + # SUPPORT: python2.7, python3.3 (or higher) + python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*", + install_requires=[ +- "cucumber-tag-expressions >= 1.1.1", ++ "cucumber-tag-expressions >= 1.1.2", + "parse >= 1.8.2", + "parse_type >= 0.4.2", + "six >= 1.12.0", diff --git a/meta-python/recipes-devtools/python/python3-behave/0041-MENTION-ENHANCEMENT-Support-emojis-in-feature-files-.patch b/meta-python/recipes-devtools/python/python3-behave/0041-MENTION-ENHANCEMENT-Support-emojis-in-feature-files-.patch new file mode 100644 index 000000000..6d69f4754 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0041-MENTION-ENHANCEMENT-Support-emojis-in-feature-files-.patch @@ -0,0 +1,91 @@ +From 1d3c288a2eed8caef24356e2a92483425bee0378 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sat, 6 Jul 2019 14:18:16 +0200 +Subject: [PATCH] MENTION ENHANCEMENT: Support emojis in feature files and + steps. + +--- + CHANGES.rst | 3 ++- + docs/new_and_noteworthy_v1.2.7.rst | 17 +++++++++++++++++ + features/i18n_emoji.feature | 7 +++++++ + features/steps/i18n_emoji_steps.py | 10 ++++++++++ + 4 files changed, 36 insertions(+), 1 deletion(-) + create mode 100644 features/i18n_emoji.feature + create mode 100644 features/steps/i18n_emoji_steps.py + +diff --git a/CHANGES.rst b/CHANGES.rst +index 01bd1bd..d165275 100644 +--- a/CHANGES.rst ++++ b/CHANGES.rst +@@ -24,8 +24,9 @@ GOALS: + ENHANCEMENTS: + + * Add support for Gherkin v6 grammar and syntax in ``*.feature`` files +-* Use cucumber "gherkin-languages.json" now (simplify: Gherkin v6 aliases, language usage) + * Use `cucumber-tag-expressions`_ with tag-matching extension (superceeds: old-style tag-expressions) ++* Use cucumber "gherkin-languages.json" now (simplify: Gherkin v6 aliases, language usage) ++* Support emojis in ``*.feature`` files and steps + * issue #678: Scenario Outline: Support tags with commas and semicolons (provided by: lawnmowerlatte, pull #679) + * issue #675: Feature files cannot be found within symlink directories (provided by: smadness, pull #680) + * Select-by-location: Add support for "Scenario container" (Feature, Rule, ScenarioOutline) (related to: #391) +diff --git a/docs/new_and_noteworthy_v1.2.7.rst b/docs/new_and_noteworthy_v1.2.7.rst +index ff1cd1f..b7242f7 100644 +--- a/docs/new_and_noteworthy_v1.2.7.rst ++++ b/docs/new_and_noteworthy_v1.2.7.rst +@@ -8,6 +8,7 @@ Summary: + * :pypi:`cucumber-tag-expressions` are extended by "tag-matching" + to match partial tag names, like: ``@foo.*`` + * `Select-by-location for Scenario Containers`_ (Feature, Rule, ScenarioOutline) ++* `Support for emojis in feature files and steps`_ + + .. _`Example Mapping`: https://cucumber.io/blog/example-mapping-introduction/ + .. _`Example Mapping Webinar`: https://cucumber.io/blog/example-mapping-webinar/ +@@ -135,3 +136,19 @@ Now you can select all entities of a **Scenario Container** (Feature, Rule, Scen + + A file-location into a **Scenario Container** selects all its entities + (Scenarios, ...). ++ ++ ++Support for Emojis in Feature Files and Steps ++------------------------------------------------------------------------------- ++ ++* Emojis can now be used in ``*.feature`` files. ++* Emojis can now be used in step definitions. ++* You can now use ``language=emoji (em)`` in ``*.feature`` files ;-) ++ ++.. literalinclude:: ../features/i18n_emoji.feature ++ :prepend: # -- FILE: features/i18n_emoji.feature ++ :language: gherkin ++ ++.. literalinclude:: ../features/steps/i18n_emoji_steps.py ++ :prepend: # -- FILE: features/steps/i18n_emoji_steps.py ++ :language: python +diff --git a/features/i18n_emoji.feature b/features/i18n_emoji.feature +new file mode 100644 +index 0000000..db23ac2 +--- /dev/null ++++ b/features/i18n_emoji.feature +@@ -0,0 +1,7 @@ ++# language: em ++# SOURCE: https://github.com/cucumber/cucumber/blob/master/gherkin/testdata/good/i18n_emoji.feature ++ ++📚: 🙈🙉🙊 ++ ++ 📕: 💃 ++ 😐🎸 +diff --git a/features/steps/i18n_emoji_steps.py b/features/steps/i18n_emoji_steps.py +new file mode 100644 +index 0000000..381d2bb +--- /dev/null ++++ b/features/steps/i18n_emoji_steps.py +@@ -0,0 +1,10 @@ ++# -*- coding: UTF-8 -*- ++# NEEDED-BY: features/i18n_emoji.feature ++ ++from behave import given ++ ++@given(u'🎸') ++def step_impl(context): ++ """Step implementation example with emoji(s).""" ++ pass ++ diff --git a/meta-python/recipes-devtools/python/python3-behave/0042-FIX-Invalid-escape-char-in-regex-w-python3.patch b/meta-python/recipes-devtools/python/python3-behave/0042-FIX-Invalid-escape-char-in-regex-w-python3.patch new file mode 100644 index 000000000..00db4a627 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0042-FIX-Invalid-escape-char-in-regex-w-python3.patch @@ -0,0 +1,250 @@ +From 281bb19a47566c01ce44e7184640728bb56d4de7 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sat, 6 Jul 2019 19:06:11 +0200 +Subject: [PATCH] FIX: Invalid escape char (in regex) w/ python3. + +--- + behave/formatter/ansi_escapes.py | 53 ++++++++---- + tests/unit/test_ansi_escapes.py | 144 ++++++++++++++++++------------- + 2 files changed, 122 insertions(+), 75 deletions(-) + +diff --git a/behave/formatter/ansi_escapes.py b/behave/formatter/ansi_escapes.py +index 6c93e6c..3ec84db 100644 +--- a/behave/formatter/ansi_escapes.py ++++ b/behave/formatter/ansi_escapes.py +@@ -7,6 +7,9 @@ from __future__ import absolute_import + import os + import re + ++# --------------------------------------------------------------------------- ++# MODULE DATA ++# --------------------------------------------------------------------------- + colors = { + "black": u"\x1b[30m", + "red": u"\x1b[31m", +@@ -38,27 +41,48 @@ escapes = { + "up": u"\x1b[1A", + } + +-if "GHERKIN_COLORS" in os.environ: +- new_aliases = [p.split("=") for p in os.environ["GHERKIN_COLORS"].split(":")] +- aliases.update(dict(new_aliases)) + +-for alias in aliases: +- escapes[alias] = "".join([colors[c] for c in aliases[alias].split(",")]) +- arg_alias = alias + "_arg" +- arg_seq = aliases.get(arg_alias, aliases[alias] + ",bold") +- escapes[arg_alias] = "".join([colors[c] for c in arg_seq.split(",")]) ++# -- NEEDED-FOR: strip_escapes(), ... ++_ANSI_ESCAPE_PATTERN = re.compile(u"\x1b\\[\\d+[mA]", re.UNICODE) + + +-# pylint: disable=anomalous-backslash-in-string ++ ++# --------------------------------------------------------------------------- ++# MODULE SETUP ++# --------------------------------------------------------------------------- ++def _setup_module(): ++ """Setup the remaining ANSI color aliases and ANSI escape sequences. ++ ++ .. note:: May modify/extend the module attributes: ++ ++ * :attr:`aliases` ++ * :attr:`escapes` ++ """ ++ # MAYBE: global aliases, escapes ++ if "GHERKIN_COLORS" in os.environ: ++ new_aliases = [p.split("=") for p in os.environ["GHERKIN_COLORS"].split(":")] ++ aliases.update(dict(new_aliases)) ++ ++ for alias in aliases: ++ escapes[alias] = "".join([colors[c] for c in aliases[alias].split(",")]) ++ arg_alias = alias + "_arg" ++ arg_seq = aliases.get(arg_alias, aliases[alias] + ",bold") ++ escapes[arg_alias] = "".join([colors[c] for c in arg_seq.split(",")]) ++ ++ ++# -- ONCE: During module-import. ++_setup_module() ++ ++ ++# --------------------------------------------------------------------------- ++# FUNCTIONS: ++# --------------------------------------------------------------------------- + def up(n): + return u"\x1b[%dA" % n + + +-_ANSI_ESCAPE_PATTERN = re.compile(u"\x1b\[\d+[mA]", re.UNICODE) +-# pylint: enable=anomalous-backslash-in-string + def strip_escapes(text): +- """ +- Removes ANSI escape sequences from text (if any are contained). ++ """Removes ANSI escape sequences from text (if any are contained). + + :param text: Text that may or may not contain ANSI escape sequences. + :return: Text without ANSI escape sequences. +@@ -67,8 +91,7 @@ def strip_escapes(text): + + + def use_ansi_escape_colorbold_composites(): # pragma: no cover +- """ +- Patch for "sphinxcontrib-ansi" to process the following ANSI escapes ++ """Patch for "sphinxcontrib-ansi" to process the following ANSI escapes + correctly (set-color set-bold sequences): + + ESC[{color}mESC[1m => ESC[{color};1m +diff --git a/tests/unit/test_ansi_escapes.py b/tests/unit/test_ansi_escapes.py +index 969f3a9..4fb78c2 100644 +--- a/tests/unit/test_ansi_escapes.py ++++ b/tests/unit/test_ansi_escapes.py +@@ -9,65 +9,89 @@ + from __future__ import absolute_import + import pytest + from behave.formatter import ansi_escapes +-import unittest + from six.moves import range + +-class StripEscapesTest(unittest.TestCase): +- ALL_COLORS = list(ansi_escapes.colors.keys()) +- CURSOR_UPS = [ ansi_escapes.up(count) for count in range(10) ] +- TEXTS = [ +- u"lorem ipsum", +- u"Alice\nBob\nCharly\nDennis", +- ] +- +- @classmethod +- def colorize(cls, text, color): +- color_escape = "" +- if color: +- color_escape = ansi_escapes.colors[color] +- return color_escape + text + ansi_escapes.escapes["reset"] +- +- @classmethod +- def colorize_text(cls, text, colors=None): +- if not colors: +- colors = [] +- colors_size = len(colors) +- color_index = 0 +- colored_chars = [] +- for char in text: +- color = colors[color_index] +- colored_chars.append(cls.colorize(char, color)) +- color_index += 1 +- if color_index >= colors_size: +- color_index = 0 +- return "".join(colored_chars) +- +- def test_should_return_same_text_without_escapes(self): +- for text in self.TEXTS: +- assert text == ansi_escapes.strip_escapes(text) +- +- def test_should_return_empty_string_for_any_ansi_escape(self): +- # XXX-JE-CHECK-PY23: If list() is really needed. +- for text in list(ansi_escapes.colors.values()): +- assert "" == ansi_escapes.strip_escapes(text) +- for text in list(ansi_escapes.escapes.values()): +- assert "" == ansi_escapes.strip_escapes(text) +- +- +- def test_should_strip_color_escapes_from_text(self): +- for text in self.TEXTS: +- colored_text = self.colorize_text(text, self.ALL_COLORS) +- assert text == ansi_escapes.strip_escapes(colored_text) +- self.assertNotEqual(text, colored_text) +- +- for color in self.ALL_COLORS: +- colored_text = self.colorize(text, color) +- assert text == ansi_escapes.strip_escapes(colored_text) +- self.assertNotEqual(text, colored_text) +- +- def test_should_strip_cursor_up_escapes_from_text(self): +- for text in self.TEXTS: +- for cursor_up in self.CURSOR_UPS: +- colored_text = cursor_up + text + ansi_escapes.escapes["reset"] +- assert text == ansi_escapes.strip_escapes(colored_text) +- self.assertNotEqual(text, colored_text) ++ ++# -------------------------------------------------------------------------- ++# TEST SUPPORT and TEST DATA ++# -------------------------------------------------------------------------- ++TEXTS = [ ++ u"lorem ipsum", ++ u"Alice and Bob", ++ u"Alice\nBob", ++] ++ALL_COLORS = list(ansi_escapes.colors.keys()) ++CURSOR_UPS = [ansi_escapes.up(count) for count in range(10)] ++ ++ ++def colorize(text, color): ++ color_escape = "" ++ if color: ++ color_escape = ansi_escapes.colors[color] ++ return color_escape + text + ansi_escapes.escapes["reset"] ++ ++ ++def colorize_text(text, colors=None): ++ if not colors: ++ colors = [] ++ colors_size = len(colors) ++ color_index = 0 ++ colored_chars = [] ++ for char in text: ++ color = colors[color_index] ++ colored_chars.append(colorize(char, color)) ++ color_index += 1 ++ if color_index >= colors_size: ++ color_index = 0 ++ return "".join(colored_chars) ++ ++ ++# -------------------------------------------------------------------------- ++# TEST SUITE ++# -------------------------------------------------------------------------- ++def test_module_setup(): ++ """Ensure that the module setup (aliases, escapes) occured.""" ++ # colors_count = len(ansi_escapes.colors) ++ aliases_count = len(ansi_escapes.aliases) ++ escapes_count = len(ansi_escapes.escapes) ++ assert escapes_count >= (2 + aliases_count + aliases_count) ++ ++ ++class TestStripEscapes(object): ++ ++ @pytest.mark.parametrize("text", TEXTS) ++ def test_should_return_same_text_without_escapes(self, text): ++ assert text == ansi_escapes.strip_escapes(text) ++ ++ @pytest.mark.parametrize("text", ansi_escapes.colors.values()) ++ def test_should_return_empty_string_for_any_ansi_escape_color(self, text): ++ assert "" == ansi_escapes.strip_escapes(text) ++ ++ @pytest.mark.parametrize("text", ansi_escapes.escapes.values()) ++ def test_should_return_empty_string_for_any_ansi_escape(self, text): ++ assert "" == ansi_escapes.strip_escapes(text) ++ ++ @pytest.mark.parametrize("text", TEXTS) ++ def test_should_strip_color_escapes_from_all_colored_text(self, text): ++ colored_text = colorize_text(text, ALL_COLORS) ++ assert text == ansi_escapes.strip_escapes(colored_text) ++ assert text != colored_text ++ ++ @pytest.mark.parametrize("text", TEXTS) ++ @pytest.mark.parametrize("color", ALL_COLORS) ++ def test_should_strip_color_escapes_from_text(self, text, color): ++ colored_text = colorize(text, color) ++ assert text == ansi_escapes.strip_escapes(colored_text) ++ assert text != colored_text ++ ++ colored_text2 = colorize(text, color) + text ++ text2 = text + text ++ assert text2 == ansi_escapes.strip_escapes(colored_text2) ++ assert text2 != colored_text2 ++ ++ @pytest.mark.parametrize("text", TEXTS) ++ @pytest.mark.parametrize("cursor_up", CURSOR_UPS) ++ def test_should_strip_cursor_up_escapes_from_text(self, text, cursor_up): ++ colored_text = cursor_up + text + ansi_escapes.escapes["reset"] ++ assert text == ansi_escapes.strip_escapes(colored_text) ++ assert text != colored_text diff --git a/meta-python/recipes-devtools/python/python3-behave/0043-Example-related-to-question-in-756.patch b/meta-python/recipes-devtools/python/python3-behave/0043-Example-related-to-question-in-756.patch new file mode 100644 index 000000000..82bf8bb08 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0043-Example-related-to-question-in-756.patch @@ -0,0 +1,335 @@ +From 8ab2ff875382a92a1a72cf82a346c4acf2ec6859 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sat, 6 Jul 2019 19:15:44 +0200 +Subject: [PATCH] Example related to question in #756 + +--- + .../fixture.override_background/README.rst | 116 ++++++++++++++++++ + .../behave_fixture_lib/__init__.py | 0 + .../behave_fixture_lib/override_background.py | 80 ++++++++++++ + .../features/environment.py | 35 ++++++ + .../features/example.feature | 18 +++ + .../features/steps/basic_steps.py | 13 ++ + .../features/steps/use_steplib_behave4cmd.py | 12 ++ + 7 files changed, 274 insertions(+) + create mode 100644 examples/fixture.override_background/README.rst + create mode 100644 examples/fixture.override_background/behave_fixture_lib/__init__.py + create mode 100644 examples/fixture.override_background/behave_fixture_lib/override_background.py + create mode 100644 examples/fixture.override_background/features/environment.py + create mode 100644 examples/fixture.override_background/features/example.feature + create mode 100644 examples/fixture.override_background/features/steps/basic_steps.py + create mode 100644 examples/fixture.override_background/features/steps/use_steplib_behave4cmd.py + +diff --git a/examples/fixture.override_background/README.rst b/examples/fixture.override_background/README.rst +new file mode 100644 +index 0000000..9c150cc +--- /dev/null ++++ b/examples/fixture.override_background/README.rst +@@ -0,0 +1,116 @@ ++EXAMPLE: Override / Disable Background Inheritance Mechanism for Scenario ++=============================================================================== ++ ++:RELATED-TO: #756 ++ ++This example shows how the Background inheritance mechanism in Gherkin ++can be disabled in ``behave``. ++ ++Parts of the recipe: ++ ++* features/example.feature (Feature file as example) ++* features/environment.py (glue code and hooks for fixture-tag / fixture) ++* behave_fixture_lib/override_background.py (fixture implementation, workhorse) ++ ++ ++.. warning:: BEWARE: This shows you how can do it, not that you should do it ++ ++ BETTER: ++ ++ * Use Rules to group Scenarios, each with its own Background (in Gherkin v6) ++ * Split Feature aspects into multiple feature files (if needed) ++ * ... (see issue #756 above) ++ ++ ++Explanation ++------------------------------------------------------------------------ ++ ++Example code how to provide a behave fixture to disable the ++background inheritance mechanism by using a fixture / fixture-tag. ++The fixture-tag "@ixture.behave.override_background" marks the ++location in Gherkin (which Scenario) where the fixture should be used ++ ++.. code-block:: gherkin ++ ++ # -- FILE: features/example.feature ++ Feature: Show how @fixture.behave.override_background is used ++ ++ Background: ++ Given a background step ++ ++ Scenario: Alice ++ When a step passes ++ And note that "Background steps are executed here" ++ ++ @fixture.behave.overide_background ++ Scenario: Bob ++ Given I need another scenario setup ++ When another step passes ++ And note that "NO-BACKGROUND STEPS are executed here" ++ ++When the feature is executed, you see that: ++ ++* First Scenario "Alice": Background steps are inherited and executed first. ++* Second Scenario "Bob": No Background step is executed. ++ ++.. code-block:: sh ++ ++ $ ../../bin/behave -f plain features/example.feature ++ Feature: Override the Background Inheritance Mechanism in some Scenarios ++ Background: ++ ++ Scenario: Alice ++ Given a background step passes ... passed ++ When a step passes ... passed ++ And note that "Background steps are executed here" ... passed ++ FIXTURE-HINT: DISABLE-BACKGROUND FOR: Bob ++ ++ Scenario: Bob ++ Given I need another scenario setup ... passed ++ When another step passes ... passed ++ And note that "NO-BACKGROUND STEPS are executed here" ... passed ++ ++ 1 feature passed, 0 failed, 0 skipped ++ 2 scenarios passed, 0 failed, 0 skipped ++ 6 steps passed, 0 failed, 0 skipped, 0 undefined ++ ++ ++The environment file provides the glue code that the fixture is called: ++ ++.. code-block:: python ++ ++ # -- FILE: features/environment.py ++ from behave_fixture_lib.override_background import behave_override_background ++ from behave.fixture import use_fixture_by_tag ++ ++ # -- FIXTURE REGISTRY: ++ fixture_registry = { ++ "fixture.behave.overide_background": behave_override_background, ++ } ++ ++ # ----------------------------------------------------------------------------- ++ # HOOKS: ++ # ----------------------------------------------------------------------------- ++ def before_tag(context, tag): ++ if tag.startswith("fixture."): ++ return use_fixture_by_tag(tag, context, fixture_registry) ++ ++ ++.. code-block:: python ++ ++ # -- FILE: behave_fixture_lib/override_background.py (fixture implementation) ++ from behave import fixture ++ ++ @fixture(name="fixture.behave.override_background") ++ def behave_override_background(ctx): ++ # -- SETUP-PART-ONLY: Disable background inheritance (for scenarios only). ++ current_scenario = ctx.scenario ++ if current_scenario: ++ behave_disable_background_inheritance_for_scenario(current_scenario) ++ ++ # ----------------------------------------------------------------------------- ++ # BEHAVE UTILITY: ++ # ----------------------------------------------------------------------------- ++ def behave_disable_background_inheritance_for_scenario(scenario): ++ print("FIXTURE-HINT: DISABLE-BACKGROUND FOR: %s" % scenario.name) ++ scenario.background = None +diff --git a/examples/fixture.override_background/behave_fixture_lib/__init__.py b/examples/fixture.override_background/behave_fixture_lib/__init__.py +new file mode 100644 +index 0000000..e69de29 +diff --git a/examples/fixture.override_background/behave_fixture_lib/override_background.py b/examples/fixture.override_background/behave_fixture_lib/override_background.py +new file mode 100644 +index 0000000..6c572cf +--- /dev/null ++++ b/examples/fixture.override_background/behave_fixture_lib/override_background.py +@@ -0,0 +1,80 @@ ++# -*- coding: UTF-8 -*- ++# RELATED-TO: #756 ++""" ++Example code how to provide a behave fixture to disable the ++background inheritance mechanism. ++ ++.. code-block:: gherkin ++ ++ # -- FILE: features/example.feature ++ Feature: Show how @fixture.behave.override_background is used ++ ++ Background: ++ Given a background step ++ ++ Scenario: Alice ++ When a step passes ++ And note that "Background steps are executed here" ++ ++ @fixture.behave.overide_background ++ Scenario: Bob ++ Given I need another scenario setup ++ When another step passes ++ And note that "NO-BACKGROUND STEPS are executed here" ++ ++.. code-block:: python ++ ++ # -- FILE: features/environment.py ++ from behave_fixture_lib.override_background import behave_override_background ++ from behave.fixture import use_fixture_by_tag ++ ++ # -- FIXTURE REGISTRY: ++ fixture_registry = { ++ "fixture.behave.overide_background": behave_override_background, ++ } ++ ++ def before_tag(context, tag): ++ if tag.startswith("fixture."): ++ return use_fixture_by_tag(tag, context, fixture_registry) ++ ++""" ++ ++from __future__ import absolute_import, print_function ++from behave import fixture ++ ++ ++# ----------------------------------------------------------------------------- ++# BEHAVE FIXTURES: ++# ----------------------------------------------------------------------------- ++@fixture(name="fixture.behave.override_background") ++def behave_override_background(ctx): ++ """Override the Background inherintance mechanism. ++ If a Feature / Rule Background exists in a Feature, ++ all contained Scenarios inherit the Background's steps. ++ ++ This fixture disables this mechanism. ++ The tagged Gherkin element will no longer inherit the background steps. ++ ++ :param ctx: Context object to use (during a test run). ++ """ ++ # -- SETUP-PART-ONLY: Disable background inheritance (for scenarios only). ++ current_scenario = ctx.scenario ++ if current_scenario: ++ behave_disable_background_inheritance_for_scenario(current_scenario) ++ ++ ++# ----------------------------------------------------------------------------- ++# BEHAVE UTILITY: ++# ----------------------------------------------------------------------------- ++def behave_disable_background_inheritance_for_scenario(scenario): ++ print("FIXTURE-HINT: DISABLE-BACKGROUND FOR: %s" % scenario.name) ++ scenario.background = None ++ # scenario._background_steps = [] ++ ++ ++# ----------------------------------------------------------------------------- ++# MODULE SPECIFIC: ++# ----------------------------------------------------------------------------- ++fixture_registry = { ++ "fixture.behave.overide_background": behave_override_background, ++} +diff --git a/examples/fixture.override_background/features/environment.py b/examples/fixture.override_background/features/environment.py +new file mode 100644 +index 0000000..7a4b735 +--- /dev/null ++++ b/examples/fixture.override_background/features/environment.py +@@ -0,0 +1,35 @@ ++# -*- coding: UTF-8 -*- ++# -- FILE: features/environment.py ++import os.path ++import sys ++ ++# ----------------------------------------------------------------------------- ++# PYTHON PATH SETUP: ++# ----------------------------------------------------------------------------- ++HERE = os.path.dirname(__file__) ++TOPA = os.path.abspath(os.path.join(HERE, "..")) ++ ++def setup_python_path(): ++ sys.path.insert(0, TOPA) ++ ++setup_python_path() ++ ++# ----------------------------------------------------------------------------- ++# NORMAL PART: ++# ----------------------------------------------------------------------------- ++from behave_fixture_lib.override_background import behave_override_background ++from behave.fixture import use_fixture_by_tag ++ ++# -- FIXTURE REGISTRY: ++fixture_registry = { ++ "fixture.behave.overide_background": behave_override_background, ++} ++ ++ ++# ----------------------------------------------------------------------------- ++# HOOKS: ++# ----------------------------------------------------------------------------- ++def before_tag(context, tag): ++ if tag.startswith("fixture."): ++ return use_fixture_by_tag(tag, context, fixture_registry) ++ +diff --git a/examples/fixture.override_background/features/example.feature b/examples/fixture.override_background/features/example.feature +new file mode 100644 +index 0000000..5ddd874 +--- /dev/null ++++ b/examples/fixture.override_background/features/example.feature +@@ -0,0 +1,18 @@ ++Feature: Override the Background Inheritance Mechanism in some Scenarios ++ ++ . BEWARE: ++ . This is only an example how this can be done (PROOF-OF-CONCEPT). ++ . This is not an example that you should do this !!! ++ ++ Background: ++ Given a background step passes ++ ++ Scenario: Alice ++ When a step passes ++ And note that "Background steps are executed here" ++ ++ @fixture.behave.overide_background ++ Scenario: Bob ++ Given I need another scenario setup ++ When another step passes ++ And note that "NO-BACKGROUND STEPS are executed here" +diff --git a/examples/fixture.override_background/features/steps/basic_steps.py b/examples/fixture.override_background/features/steps/basic_steps.py +new file mode 100644 +index 0000000..34f2107 +--- /dev/null ++++ b/examples/fixture.override_background/features/steps/basic_steps.py +@@ -0,0 +1,13 @@ ++from behave import given, step ++ ++# @step(u'{word} step passes') ++# def step_passes_with_word(context, word): ++# pass ++ ++@step(u'{word} background step passes') ++def step_background_step_passes(context, word): ++ pass ++ ++@given(u'I need {word} scenario setup') ++def step_given_i_need_scenario_setup(context, word): ++ pass +diff --git a/examples/fixture.override_background/features/steps/use_steplib_behave4cmd.py b/examples/fixture.override_background/features/steps/use_steplib_behave4cmd.py +new file mode 100644 +index 0000000..bc32a32 +--- /dev/null ++++ b/examples/fixture.override_background/features/steps/use_steplib_behave4cmd.py +@@ -0,0 +1,12 @@ ++# -*- coding: utf-8 -*- ++""" ++Use behave4cmd0 step library (predecessor of behave4cmd). ++""" ++ ++from __future__ import absolute_import ++ ++# -- REGISTER-STEPS FROM STEP-LIBRARY: ++# import behave4cmd0.__all_steps__ ++# import behave4cmd0.failing_steps ++import behave4cmd0.passing_steps ++import behave4cmd0.note_steps diff --git a/meta-python/recipes-devtools/python/python3-behave/0044-FIX-python3.8-regressions-on-CI-server.patch b/meta-python/recipes-devtools/python/python3-behave/0044-FIX-python3.8-regressions-on-CI-server.patch new file mode 100644 index 000000000..c383e39f5 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0044-FIX-python3.8-regressions-on-CI-server.patch @@ -0,0 +1,489 @@ +From 64e58e5721f4fccc9cdf49b524271265a0c776e4 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sun, 7 Jul 2019 12:43:19 +0200 +Subject: [PATCH] FIX: python3.8 regressions on CI server + +Issues w/ python3.8: +* Traceback: Line numbers of step functions differ (+1) +* JUnit XML: Attribute ordering of XML element differs (in output) +--- + behave.ini | 3 +- + features/environment.py | 14 ++++++ + features/step.duplicated_step.feature | 20 ++++---- + issue.features/environment.py | 38 ++++++++++++--- + issue.features/issue0330.feature | 64 ++++++++++++++++++++++++ + issue.features/issue0446.feature | 70 +++++++++++++++++++++++++++ + issue.features/issue0457.feature | 49 +++++++++++++++++++ + tox.ini | 2 +- + 8 files changed, 242 insertions(+), 18 deletions(-) + +diff --git a/behave.ini b/behave.ini +index 45c0f0d..952240d 100644 +--- a/behave.ini ++++ b/behave.ini +@@ -15,8 +15,9 @@ show_skipped = false + format = rerun + progress3 + outfiles = rerun.txt +- reports/report_progress3.txt ++ build/behave.reports/report_progress3.txt + junit = true ++junit_directory = build/behave.reports + logging_level = INFO + # logging_format = LOG.%(levelname)-8s %(name)-10s: %(message)s + # logging_format = LOG.%(levelname)-8s %(asctime)s %(name)-10s: %(message)s +diff --git a/features/environment.py b/features/environment.py +index 4744e89..3769ee4 100644 +--- a/features/environment.py ++++ b/features/environment.py +@@ -1,5 +1,7 @@ + # -*- coding: UTF-8 -*- ++# FILE: features/environemnt.py + ++from __future__ import absolute_import, print_function + from behave.tag_matcher import ActiveTagMatcher, setup_active_tag_values + from behave4cmd0.setup_command_shell import setup_command_shell_processors4behave + import platform +@@ -20,6 +22,15 @@ active_tag_value_provider = { + } + active_tag_matcher = ActiveTagMatcher(active_tag_value_provider) + ++ ++def print_active_tags_summary(): ++ active_tag_data = active_tag_value_provider ++ print("ACTIVE-TAG SUMMARY:") ++ print("use.with_python.version=%s" % active_tag_data.get("python.version")) ++ # print("use.with_os=%s" % active_tag_data.get("os")) ++ print() ++ ++ + # ----------------------------------------------------------------------------- + # HOOKS: + # ----------------------------------------------------------------------------- +@@ -30,11 +41,14 @@ def before_all(context): + setup_python_path() + setup_context_with_global_params_test(context) + setup_command_shell_processors4behave() ++ print_active_tags_summary() ++ + + def before_feature(context, feature): + if active_tag_matcher.should_exclude_with(feature.tags): + feature.skip(reason=active_tag_matcher.exclude_reason) + ++ + def before_scenario(context, scenario): + if active_tag_matcher.should_exclude_with(scenario.effective_tags): + scenario.skip(reason=active_tag_matcher.exclude_reason) +diff --git a/features/step.duplicated_step.feature b/features/step.duplicated_step.feature +index 59888b0..396cca2 100644 +--- a/features/step.duplicated_step.feature ++++ b/features/step.duplicated_step.feature +@@ -32,11 +32,11 @@ Feature: Duplicated Step Definitions + AmbiguousStep: @given('I call Alice') has already been defined in + existing step @given('I call Alice') at features/steps/alice_steps.py:3 + """ +- And the command output should contain: +- """ +- File "features/steps/alice_steps.py", line 7, in +- @given(u'I call Alice') +- """ ++ # -- DISABLED: Python 3.8 traceback line numbers differ w/ decorators (+1). ++ # And the command output should contain: ++ # """ ++ # File "features/steps/alice_steps.py", line 7, in ++ # """ + + + Scenario: Duplicated Step Definition in another File +@@ -70,11 +70,11 @@ Feature: Duplicated Step Definitions + AmbiguousStep: @given('I call Bob') has already been defined in + existing step @given('I call Bob') at features/steps/bob1_steps.py:3 + """ +- And the command output should contain: +- """ +- File "features/steps/bob2_steps.py", line 3, in +- @given('I call Bob') +- """ ++ # -- DISABLED: Python 3.8 traceback line numbers differ w/ decorators (+1). ++ # And the command output should contain: ++ # """ ++ # File "features/steps/bob2_steps.py", line 3, in ++ # """ + + @xfail + Scenario: Duplicated Same Step Definition via import from another File +diff --git a/issue.features/environment.py b/issue.features/environment.py +index 2dfec75..7e48ee0 100644 +--- a/issue.features/environment.py ++++ b/issue.features/environment.py +@@ -1,5 +1,5 @@ + # -*- coding: UTF-8 -*- +-# FILE: features/environment.py ++# FILE: issue.features/environemnt.py + # pylint: disable=unused-argument + """ + Functionality: +@@ -7,17 +7,20 @@ Functionality: + * active tags + """ + +-from __future__ import print_function ++ ++from __future__ import absolute_import, print_function + import sys + import platform + import os.path + import six + from behave.tag_matcher import ActiveTagMatcher + from behave4cmd0.setup_command_shell import setup_command_shell_processors4behave +-# PREPARED: +-# from behave.tag_matcher import setup_active_tag_values ++# PREPARED: from behave.tag_matcher import setup_active_tag_values + + ++# --------------------------------------------------------------------------- ++# TEST SUPPORT: For Active Tags ++# --------------------------------------------------------------------------- + def require_tool(tool_name): + """Check if a tool (an executable program) is provided on this platform. + +@@ -45,12 +48,14 @@ def require_tool(tool_name): + # print("TOOL-NOT-FOUND: %s" % tool_name) + return False + ++ + def as_bool_string(value): + if bool(value): + return "yes" + else: + return "no" + ++ + def discover_ci_server(): + # pylint: disable=invalid-name + ci_server = "none" +@@ -67,12 +72,17 @@ def discover_ci_server(): + return ci_server + + ++# --------------------------------------------------------------------------- ++# BEHAVE SUPPORT: Active Tags ++# --------------------------------------------------------------------------- + # -- MATCHES ANY TAGS: @use.with_{category}={value} + # NOTE: active_tag_value_provider provides category values for active tags. ++python_version = "%s.%s" % sys.version_info[:2] + active_tag_value_provider = { + "platform": sys.platform, + "python2": str(six.PY2).lower(), + "python3": str(six.PY3).lower(), ++ "python.version": python_version, + # -- python.implementation: cpython, pypy, jython, ironpython + "python.implementation": platform.python_implementation().lower(), + "pypy": str("__pypy__" in sys.modules).lower(), +@@ -82,17 +92,33 @@ active_tag_value_provider = { + } + active_tag_matcher = ActiveTagMatcher(active_tag_value_provider) + ++ ++def print_active_tags_summary(): ++ active_tag_data = active_tag_value_provider ++ print("ACTIVE-TAG SUMMARY:") ++ print("use.with_python.version=%s" % active_tag_data.get("python.version")) ++ # print("use.with_platform=%s" % active_tag_data.get("platform")) ++ # print("use.with_os=%s" % active_tag_data.get("os")) ++ print() ++ ++ ++# --------------------------------------------------------------------------- ++# BEHAVE HOOKS: ++# --------------------------------------------------------------------------- + def before_all(context): + # -- SETUP ACTIVE-TAG MATCHER (with userdata): + # USE: behave -D browser=safari ... +- # NOT-NEEDED: setup_active_tag_values(active_tag_value_provider, +- # context.config.userdata) ++ # NOT-NEEDED: ++ # setup_active_tag_values(active_tag_value_provider, context.config.userdata) + setup_command_shell_processors4behave() ++ print_active_tags_summary() ++ + + def before_feature(context, feature): + if active_tag_matcher.should_exclude_with(feature.tags): + feature.skip(reason=active_tag_matcher.exclude_reason) + ++ + def before_scenario(context, scenario): + if active_tag_matcher.should_exclude_with(scenario.effective_tags): + scenario.skip(reason=active_tag_matcher.exclude_reason) +diff --git a/issue.features/issue0330.feature b/issue.features/issue0330.feature +index dc1ebe7..81cb6e2 100644 +--- a/issue.features/issue0330.feature ++++ b/issue.features/issue0330.feature +@@ -70,6 +70,7 @@ Feature: Issue #330: Skipped scenarios are included in junit reports when --no-s + And note that "bob.feature is skipped" + + ++ @not.with_python.version=3.8 + Scenario: Junit report for skipped feature is created with --show-skipped + When I run "behave --junit -t @tag1 --show-skipped @alice_and_bob.featureset" + Then it should pass with: +@@ -83,6 +84,23 @@ Feature: Issue #330: Skipped scenarios are included in junit reports when --no-s + + """ + ++ @use.with_python.version=3.8 ++ Scenario: Junit report for skipped feature is created with --show-skipped ++ When I run "behave --junit -t @tag1 --show-skipped @alice_and_bob.featureset" ++ Then it should pass with: ++ """ ++ 1 feature passed, 0 failed, 1 skipped ++ """ ++ And a file named "test_results/TESTS-alice.xml" exists ++ And a file named "test_results/TESTS-bob.xml" exists ++ And the file "test_results/TESTS-bob.xml" should contain: ++ """ ++ ++ """ ++ # -- HINT FOR: Python < 3.8 ++ # ++ ++ @not.with_python.version=3.8 + Scenario: Junit report for skipped scenario is neither shown nor counted with --no-skipped + When I run "behave --junit -t @tag1 --no-skipped" + Then it should pass with: +@@ -102,7 +120,30 @@ Feature: Issue #330: Skipped scenarios are included in junit reports when --no-s + """ + And note that "Charly2 is the skipped scenarion in charly.feature" + ++ @use.with_python.version=3.8 ++ Scenario: Junit report for skipped scenario is neither shown nor counted with --no-skipped ++ When I run "behave --junit -t @tag1 --no-skipped" ++ Then it should pass with: ++ """ ++ 2 features passed, 0 failed, 1 skipped ++ 2 scenarios passed, 0 failed, 2 skipped ++ """ ++ And a file named "test_results/TESTS-alice.xml" exists ++ And a file named "test_results/TESTS-charly.xml" exists ++ And the file "test_results/TESTS-charly.xml" should contain: ++ """ ++ " + + ++ @use.with_python.version=3.8 ++ Scenario: Hook error in before_scenario() ++ When I run "behave -f plain --junit features/before_scenario_failure.feature" ++ Then it should fail with: ++ """ ++ 0 scenarios passed, 1 failed, 0 skipped ++ """ ++ And the command output should contain: ++ """ ++ HOOK-ERROR in before_scenario: RuntimeError: OOPS ++ """ ++ And the file "reports/TESTS-before_scenario_failure.xml" should contain: ++ """ ++ ++ """ ++ # -- HINT FOR: Python < 3.8 ++ # ++ And the file "reports/TESTS-before_scenario_failure.xml" should contain: ++ """ ++ File "features/environment.py", line 6, in before_scenario ++ cause_hook_failure() ++ File "features/environment.py", line 2, in cause_hook_failure ++ raise RuntimeError("OOPS") ++ """ ++ And note that "the traceback is contained in the XML element " ++ ++ ++ @not.with_python.version=3.8 + Scenario: Hook error in after_scenario() + When I run "behave -f plain --junit features/after_scenario_failure.feature" + Then it should fail with: +@@ -114,3 +149,38 @@ Feature: Issue #446 -- Support scenario hook-errors with JUnitReporter + raise RuntimeError("OOPS") + """ + And note that "the traceback is contained in the XML element " ++ ++ ++ @use.with_python.version=3.8 ++ Scenario: Hook error in after_scenario() ++ When I run "behave -f plain --junit features/after_scenario_failure.feature" ++ Then it should fail with: ++ """ ++ 0 scenarios passed, 1 failed, 0 skipped ++ """ ++ And the command output should contain: ++ """ ++ Scenario: B1 ++ Given another step passes ... passed ++ HOOK-ERROR in after_scenario: RuntimeError: OOPS ++ """ ++ And the file "reports/TESTS-after_scenario_failure.xml" should contain: ++ """ ++ ++ """ ++ # -- HINT FOR: Python < 3.8 ++ # ++ And the file "reports/TESTS-after_scenario_failure.xml" should contain: ++ """ ++ File "features/environment.py", line 10, in after_scenario ++ cause_hook_failure() ++ File "features/environment.py", line 2, in cause_hook_failure ++ raise RuntimeError("OOPS") ++ """ ++ And note that "the traceback is contained in the XML element " +diff --git a/issue.features/issue0457.feature b/issue.features/issue0457.feature +index f80640e..46f96e9 100644 +--- a/issue.features/issue0457.feature ++++ b/issue.features/issue0457.feature +@@ -24,6 +24,7 @@ Feature: Issue #457 -- Double-quotes in error messages of JUnit XML reports + """ + + ++ @not.with_python.version=3.8 + Scenario: Use failing assertation in a JUnit XML report + Given a file named "features/fails1.feature" with: + """ +@@ -44,6 +45,31 @@ Feature: Issue #457 -- Double-quotes in error messages of JUnit XML reports + ++ """ ++ # -- HINT FOR: Python < 3.8 ++ # I am ++ ''' ++ """ ++ When I run "behave --junit features/fails2.feature" ++ Then it should fail with: ++ """ ++ 0 scenarios passed, 1 failed, 0 skipped ++ """ ++ And the file "reports/TESTS-fails2.xml" should contain: ++ """ ++ ++ """ ++ # -- HINT FOR: Python < 3.8 ++ # +Date: Sun, 7 Jul 2019 12:59:57 +0200 +Subject: [PATCH] UPDATE: Mark issue #755 as fixed. + +--- + CHANGES.rst | 11 ++++------- + 1 file changed, 4 insertions(+), 7 deletions(-) + +diff --git a/CHANGES.rst b/CHANGES.rst +index d165275..312cbba 100644 +--- a/CHANGES.rst ++++ b/CHANGES.rst +@@ -27,28 +27,25 @@ ENHANCEMENTS: + * Use `cucumber-tag-expressions`_ with tag-matching extension (superceeds: old-style tag-expressions) + * Use cucumber "gherkin-languages.json" now (simplify: Gherkin v6 aliases, language usage) + * Support emojis in ``*.feature`` files and steps ++* Select-by-location: Add support for "Scenario container" (Feature, Rule, ScenarioOutline) (related to: #391) + * issue #678: Scenario Outline: Support tags with commas and semicolons (provided by: lawnmowerlatte, pull #679) + * issue #675: Feature files cannot be found within symlink directories (provided by: smadness, pull #680) +-* Select-by-location: Add support for "Scenario container" (Feature, Rule, ScenarioOutline) (related to: #391) +- +-PARTIALLY FIXED: +- +-* issue #755: Failures with Python 3.8 (submitted by: hroncok) + + FIXED: + +-* pull #588: Steps-catalog argument should not break configured rerun settings (provided by: Lego3) ++* issue #755: Failures with Python 3.8 (submitted by: hroncok) + * issue #725: Scenario Outline description lines seem to be ignored (submitted by: nizwiz) + * issue #713: Background section doesn't support description (provided by: dgou) + * pull #657: Allow async steps with timeouts to fail when they raise exceptions (provided by: ALSchwalm) + * issue #631: ScenarioOutline variables not possible in table headings (provided by: mschnelle, pull #642) + * issue #619: Context __getattr__ should raise AttributeError instead of KeyError (submitted by: anxodio) ++* pull #588: Steps-catalog argument should not break configured rerun settings (provided by: Lego3) + + MINOR: + ++* pull #660: Fix minor typos (provided by: rrueth) + * pull #655: Use pytest instead of py.test per upstream recommendation (provided by: scop) + * issue #654: tox.ini: pypi.python.org -> pypi.org (submitted by: pradyunsg) +-* pull #660: Fix minor typos (provided by: rrueth) + + DOCUMENTATION: + diff --git a/meta-python/recipes-devtools/python/python3-behave/0046-UPDATE-Cucumber-gherkin-languages.json.patch b/meta-python/recipes-devtools/python/python3-behave/0046-UPDATE-Cucumber-gherkin-languages.json.patch new file mode 100644 index 000000000..05c33089a --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0046-UPDATE-Cucumber-gherkin-languages.json.patch @@ -0,0 +1,57 @@ +From 8c02ce2abbab65cfa4d5b0f306bf9e4735a7195d Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sun, 7 Jul 2019 23:35:45 +0200 +Subject: [PATCH] UPDATE: Cucumber gherkin-languages.json + +--- + behave/i18n.py | 5 ++++- + etc/gherkin/gherkin-languages.json | 6 +++++- + 2 files changed, 9 insertions(+), 2 deletions(-) + +diff --git a/behave/i18n.py b/behave/i18n.py +index 9eb9aab..721c4c3 100644 +--- a/behave/i18n.py ++++ b/behave/i18n.py +@@ -382,6 +382,9 @@ languages = \ + 'feature': ['Fonctionnalité'], + 'given': ['* ', + 'Soit ', ++ 'Sachant que ', ++ "Sachant qu'", ++ 'Sachant ', + 'Etant donné que ', + "Etant donné qu'", + 'Etant donné ', +@@ -399,7 +402,7 @@ languages = \ + 'rule': ['Règle'], + 'scenario': ['Exemple', 'Scénario'], + 'scenario_outline': ['Plan du scénario', 'Plan du Scénario'], +- 'then': ['* ', 'Alors '], ++ 'then': ['* ', 'Alors ', 'Donc '], + 'when': ['* ', 'Quand ', 'Lorsque ', "Lorsqu'"]}, + 'ga': {'and': ['* ', 'Agus'], + 'background': ['Cúlra'], +diff --git a/etc/gherkin/gherkin-languages.json b/etc/gherkin/gherkin-languages.json +index b08e0f5..913cfac 100644 +--- a/etc/gherkin/gherkin-languages.json ++++ b/etc/gherkin/gherkin-languages.json +@@ -1256,6 +1256,9 @@ + "given": [ + "* ", + "Soit ", ++ "Sachant que ", ++ "Sachant qu'", ++ "Sachant ", + "Etant donné que ", + "Etant donné qu'", + "Etant donné ", +@@ -1284,7 +1287,8 @@ + ], + "then": [ + "* ", +- "Alors " ++ "Alors ", ++ "Donc " + ], + "when": [ + "* ", diff --git a/meta-python/recipes-devtools/python/python3-behave/0047-gherkin-Adding-Rule-keyword-translation-in-portugues.patch b/meta-python/recipes-devtools/python/python3-behave/0047-gherkin-Adding-Rule-keyword-translation-in-portugues.patch new file mode 100644 index 000000000..ebbaa72ed --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0047-gherkin-Adding-Rule-keyword-translation-in-portugues.patch @@ -0,0 +1,202 @@ +From cf7b144c917f5bc067dd8fd2a4d98c859849dbf0 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Mon, 8 Jul 2019 00:38:26 +0200 +Subject: [PATCH] gherkin: Adding Rule keyword translation in portuguese and + spanish to gherkin-languages.json + +* Integrate changes based on merged cucumber pull-request #621 +* Update "gherkin-languages.json" +* Update "behave/i18n.py" (generated from: gherkin-languages.json) + +RELATED-TO: pull #751 (same; using the official way) +--- + CHANGES.rst | 1 + + behave/fixture.py | 1 - + behave/i18n.py | 4 +-- + etc/gherkin/gherkin-languages.json | 4 +-- + invoke.yaml | 4 +++ + tasks/__init__.py | 3 ++ + tasks/develop.py | 58 ++++++++++++++++++++++++++++++ + tasks/py.requirements.txt | 3 ++ + 8 files changed, 73 insertions(+), 5 deletions(-) + create mode 100644 tasks/develop.py + +diff --git a/CHANGES.rst b/CHANGES.rst +index 312cbba..15a4ef9 100644 +--- a/CHANGES.rst ++++ b/CHANGES.rst +@@ -43,6 +43,7 @@ FIXED: + + MINOR: + ++* pull #751: gherkin: Adding Rule keyword translation in portuguese and spanish to gherkin-languages.json (provided by: dunossauro) + * pull #660: Fix minor typos (provided by: rrueth) + * pull #655: Use pytest instead of py.test per upstream recommendation (provided by: scop) + * issue #654: tox.ini: pypi.python.org -> pypi.org (submitted by: pradyunsg) +diff --git a/behave/fixture.py b/behave/fixture.py +index 21093b0..3a9f1bc 100644 +--- a/behave/fixture.py ++++ b/behave/fixture.py +@@ -348,7 +348,6 @@ def use_composite_fixture_with(context, fixture_funcs_with_params): + return composite_fixture + + +- + # ------------------------------------------------------------------------------- + # DECORATORS: + # ------------------------------------------------------------------------------- +diff --git a/behave/i18n.py b/behave/i18n.py +index 721c4c3..2781afe 100644 +--- a/behave/i18n.py ++++ b/behave/i18n.py +@@ -331,7 +331,7 @@ languages = \ + 'given': ['* ', 'Dado ', 'Dada ', 'Dados ', 'Dadas '], + 'name': 'Spanish', + 'native': 'español', +- 'rule': ['Rule'], ++ 'rule': ['Regla'], + 'scenario': ['Ejemplo', 'Escenario'], + 'scenario_outline': ['Esquema del escenario'], + 'then': ['* ', 'Entonces '], +@@ -762,7 +762,7 @@ languages = \ + 'given': ['* ', 'Dado ', 'Dada ', 'Dados ', 'Dadas '], + 'name': 'Portuguese', + 'native': 'português', +- 'rule': ['Rule'], ++ 'rule': ['Regra'], + 'scenario': ['Exemplo', 'Cenário', 'Cenario'], + 'scenario_outline': ['Esquema do Cenário', + 'Esquema do Cenario', +diff --git a/etc/gherkin/gherkin-languages.json b/etc/gherkin/gherkin-languages.json +index 913cfac..29cbca1 100644 +--- a/etc/gherkin/gherkin-languages.json ++++ b/etc/gherkin/gherkin-languages.json +@@ -1084,7 +1084,7 @@ + "name": "Spanish", + "native": "español", + "rule": [ +- "Rule" ++ "Regla" + ], + "scenario": [ + "Ejemplo", +@@ -2553,7 +2553,7 @@ + "name": "Portuguese", + "native": "português", + "rule": [ +- "Rule" ++ "Regra" + ], + "scenario": [ + "Exemplo", +diff --git a/invoke.yaml b/invoke.yaml +index 3e93cfc..d6f141c 100644 +--- a/invoke.yaml ++++ b/invoke.yaml +@@ -38,6 +38,10 @@ cleanup: + - "__WORKDIR__" + - reports + ++ extra_files: ++ - "etc/gherkin/gherkin*.json.SAVED" ++ - "etc/gherkin/i18n.py" ++ + cleanup_all: + extra_directories: + - .hypothesis +diff --git a/tasks/__init__.py b/tasks/__init__.py +index 969a94a..a572465 100644 +--- a/tasks/__init__.py ++++ b/tasks/__init__.py +@@ -39,6 +39,8 @@ from . import _tasklet_cleanup as cleanup + from . import docs + from . import test + from . import release ++from . import develop ++ + + # ----------------------------------------------------------------------------- + # TASKS: +@@ -56,6 +58,7 @@ namespace.add_collection(Collection.from_module(cleanup), name="cleanup") + namespace.add_collection(Collection.from_module(docs)) + namespace.add_collection(Collection.from_module(test)) + namespace.add_collection(Collection.from_module(release)) ++namespace.add_collection(Collection.from_module(develop)) + cleanup.cleanup_tasks.add_task(cleanup.clean_python) + + cleanup.cleanup_tasks.add_task(cleanup.clean_python) +diff --git a/tasks/develop.py b/tasks/develop.py +new file mode 100644 +index 0000000..b08df0e +--- /dev/null ++++ b/tasks/develop.py +@@ -0,0 +1,58 @@ ++# -*- coding: UTF-8 -*- ++""" ++Development tasks ++""" ++ ++from __future__ import absolute_import, print_function ++from invoke import Collection, task ++from invoke.util import cd ++from path import Path ++import requests ++ ++# ----------------------------------------------------------------------------- ++# CONSTANTS: ++# ----------------------------------------------------------------------------- ++GHERKIN_LANGUAGES_URL = "https://raw.githubusercontent.com/cucumber/cucumber/master/gherkin/gherkin-languages.json" ++ ++ ++# ----------------------------------------------------------------------------- ++# TASKS: ++# ----------------------------------------------------------------------------- ++@task(name="update_gherkin") # TOO-LONGS: aliases=["update_gherkin_languages"]) ++def update_gherkin_languages(ctx): ++ """Update "gherkin-languages.json" file from cucumber-repo.""" ++ with cd("etc/gherkin"): ++ # -- BACKUP-FILE: ++ gherkin_languages_file = Path("gherkin-languages.json") ++ gherkin_languages_file.copy("gherkin-languages.json.SAVED") ++ ++ print('Downloading "gherkin-languages.json" from github:cucumber ...') ++ download_request = requests.get(GHERKIN_LANGUAGES_URL) ++ gherkin_languages_newfile = Path("gherkin-languages.json.NEW") ++ assert download_request.ok ++ print('Download finished: OK (size={0})'.format(len(download_request.content))) ++ with open(gherkin_languages_newfile, "wb") as f: ++ f.write(download_request.content) ++ gherkin_languages_newfile.rename("gherkin-languages.json") ++ ++ print('Generating "i18n.py" ...') ++ ctx.run("./convert_gherkin-languages.py") ++ ++ ++# ----------------------------------------------------------------------------- ++# TASK HELPERS: ++# ----------------------------------------------------------------------------- ++def print_packages(packages): ++ print("PACKAGES[%d]:" % len(packages)) ++ for package in packages: ++ package_size = package.stat().st_size ++ package_time = package.stat().st_mtime ++ print(" - %s (size=%s)" % (package, package_size)) ++ ++ ++# ----------------------------------------------------------------------------- ++# TASK CONFIGURATION: ++# ----------------------------------------------------------------------------- ++namespace = Collection() ++namespace.add_task(update_gherkin_languages) ++namespace.configure({}) +diff --git a/tasks/py.requirements.txt b/tasks/py.requirements.txt +index e772d5e..a77d3bc 100644 +--- a/tasks/py.requirements.txt ++++ b/tasks/py.requirements.txt +@@ -16,3 +16,6 @@ six >= 1.12.0 + # -- PYTHON2 BACKPORTS: + pathlib; python_version <= '3.4' + backports.shutil_which; python_version <= '3.3' ++ ++# -- SECTION: develop ++requests diff --git a/meta-python/recipes-devtools/python/python3-behave/0048-Tweaks-to-update-generate-from-gherkin-languages.jso.patch b/meta-python/recipes-devtools/python/python3-behave/0048-Tweaks-to-update-generate-from-gherkin-languages.jso.patch new file mode 100644 index 000000000..939a0901e --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0048-Tweaks-to-update-generate-from-gherkin-languages.jso.patch @@ -0,0 +1,141 @@ +From 084f823fda5939442f48eb34d99c0ed48d675097 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Mon, 8 Jul 2019 01:07:11 +0200 +Subject: [PATCH] Tweaks to update/generate from gherkin-languages.json + +--- + etc/gherkin/convert_gherkin-languages.py | 16 +++++++---- + tasks/develop.py | 34 +++++++++++------------- + 2 files changed, 27 insertions(+), 23 deletions(-) + +diff --git a/etc/gherkin/convert_gherkin-languages.py b/etc/gherkin/convert_gherkin-languages.py +index 1803ca6..9ef9b0c 100755 +--- a/etc/gherkin/convert_gherkin-languages.py ++++ b/etc/gherkin/convert_gherkin-languages.py +@@ -68,7 +68,7 @@ def yaml_normalize(data): + return data + + +-def data_normalize(data): ++def data_normalize(data, verbose=False): + """Normalize "gherkin-languages.json" data into internal format, + needed by behave." + +@@ -76,7 +76,8 @@ def data_normalize(data): + :return: Normalized data (as dictionary). + """ + for language in data: +- print("Language: %s ..." % language) ++ if verbose: ++ print("Language: %s ..." % language) + # -- STEP: Normalize attribute "scenarioOutline" => "scenario_outline" + lang_keywords = data[language] + lang_keywords[u"scenario_outline"] = lang_keywords[u"scenarioOutline"] +@@ -107,7 +108,7 @@ def data_normalize(data): + + + def gherkin_languages_to_python_module(gherkin_languages_path, output_file=None, +- encoding=None): ++ encoding=None, verbose=False): + """Workhorse. + Performs the conversion from "gherkin-languages.json" to "i18n.py". + Writes output to file or console (stdout). +@@ -115,6 +116,7 @@ def gherkin_languages_to_python_module(gherkin_languages_path, output_file=None, + :param gherkin_languages_path: File path for JSON file. + :param output_file: Output filename (or STDOUT for: None, "stdout", "-") + :param encoding: Optional output encoding to use (default: UTF-8). ++ :param verbose: Enable verbose mode (as bool; optional). + """ + if encoding is None: + encoding = "UTF-8" +@@ -122,7 +124,7 @@ def gherkin_languages_to_python_module(gherkin_languages_path, output_file=None, + # -- STEP 1: Load JSON data. + json_encoding = "UTF-8" + languages = json.load(open(gherkin_languages_path, encoding=json_encoding)) +- languages = data_normalize(languages) ++ languages = data_normalize(languages, verbose=verbose) + # languages = yaml_normalize(languages) + + # -- STEP 2: Generate python module with i18n data. +@@ -178,6 +180,9 @@ def main(args=None): + parser.add_argument("-e", "--encoding", dest="encoding", + default="UTF-8", + help="Output encoding.") ++ parser.add_argument("--verbose", dest="verbose", default=False, ++ action="store_true", ++ help="Enable verbose mode.") + parser.add_argument("output_file", default="i18n.py", nargs="?", + help="Filename of Python I18N module (as output).") + parser.add_argument("--version", action="version", version=__version__) +@@ -191,7 +196,8 @@ def main(args=None): + try: + print("Writing %s .." % options.output_file) + gherkin_languages_to_python_module(options.json_file, options.output_file, +- encoding=options.encoding) ++ encoding=options.encoding, ++ verbose=options.verbose) + except Exception as e: + message = "%s: %s" % (e.__class__.__name__, e) + sys.exit(message) +diff --git a/tasks/develop.py b/tasks/develop.py +index b08df0e..9a21363 100644 +--- a/tasks/develop.py ++++ b/tasks/develop.py +@@ -18,9 +18,15 @@ GHERKIN_LANGUAGES_URL = "https://raw.githubusercontent.com/cucumber/cucumber/mas + # ----------------------------------------------------------------------------- + # TASKS: + # ----------------------------------------------------------------------------- +-@task(name="update_gherkin") # TOO-LONGS: aliases=["update_gherkin_languages"]) +-def update_gherkin_languages(ctx): +- """Update "gherkin-languages.json" file from cucumber-repo.""" ++@task ++def update_gherkin(ctx, dry_run=False): ++ """Update "gherkin-languages.json" file from cucumber-repo. ++ ++ * Download "gherkin-languages.json" from cucumber repo ++ * Update "gherkin-languages.json" ++ * Generate "i18n.py" file from "gherkin-languages.json" ++ * Update "behave/i18n.py" file (optional; not in dry-run mode) ++ """ + with cd("etc/gherkin"): + # -- BACKUP-FILE: + gherkin_languages_file = Path("gherkin-languages.json") +@@ -28,31 +34,23 @@ def update_gherkin_languages(ctx): + + print('Downloading "gherkin-languages.json" from github:cucumber ...') + download_request = requests.get(GHERKIN_LANGUAGES_URL) +- gherkin_languages_newfile = Path("gherkin-languages.json.NEW") + assert download_request.ok + print('Download finished: OK (size={0})'.format(len(download_request.content))) +- with open(gherkin_languages_newfile, "wb") as f: ++ with open(gherkin_languages_file, "wb") as f: + f.write(download_request.content) +- gherkin_languages_newfile.rename("gherkin-languages.json") + + print('Generating "i18n.py" ...') + ctx.run("./convert_gherkin-languages.py") +- +- +-# ----------------------------------------------------------------------------- +-# TASK HELPERS: +-# ----------------------------------------------------------------------------- +-def print_packages(packages): +- print("PACKAGES[%d]:" % len(packages)) +- for package in packages: +- package_size = package.stat().st_size +- package_time = package.stat().st_mtime +- print(" - %s (size=%s)" % (package, package_size)) ++ ctx.run("diff i18n.py ../../behave/i18n.py") ++ if not dry_run: ++ print("Updating behave/i18n.py ...") ++ Path("i18n.py").move("../../behave/i18n.py") + + + # ----------------------------------------------------------------------------- + # TASK CONFIGURATION: + # ----------------------------------------------------------------------------- ++# TOO-LONG: aliases=["update_gherkin_languages"]) + namespace = Collection() +-namespace.add_task(update_gherkin_languages) ++namespace.add_task(update_gherkin) + namespace.configure({}) diff --git a/meta-python/recipes-devtools/python/python3-behave/0049-EXAMPLE-Tweak-naming-to-fixture.behave.no_background.patch b/meta-python/recipes-devtools/python/python3-behave/0049-EXAMPLE-Tweak-naming-to-fixture.behave.no_background.patch new file mode 100644 index 000000000..8d956cdd5 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0049-EXAMPLE-Tweak-naming-to-fixture.behave.no_background.patch @@ -0,0 +1,322 @@ +From d3ef06c2dfe44b2286b42b7ddcc66f7c9e6aa245 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Tue, 9 Jul 2019 08:10:26 +0200 +Subject: [PATCH] EXAMPLE: Tweak naming to @fixture.behave.no_background (was: + .override_background) + +--- + examples/fixture.no_background/README.rst | 110 ++++++++++++++++++ + .../behave_fixture_lib/__init__.py | 0 + .../behave_fixture_lib/no_background.py | 72 ++++++++++++ + .../features/environment.py | 35 ++++++ + .../features/example.feature | 18 +++ + .../features/steps/basic_steps.py | 13 +++ + .../features/steps/use_steplib_behave4cmd.py | 12 ++ + 7 files changed, 260 insertions(+) + create mode 100644 examples/fixture.no_background/README.rst + create mode 100644 examples/fixture.no_background/behave_fixture_lib/__init__.py + create mode 100644 examples/fixture.no_background/behave_fixture_lib/no_background.py + create mode 100644 examples/fixture.no_background/features/environment.py + create mode 100644 examples/fixture.no_background/features/example.feature + create mode 100644 examples/fixture.no_background/features/steps/basic_steps.py + create mode 100644 examples/fixture.no_background/features/steps/use_steplib_behave4cmd.py + +diff --git a/examples/fixture.no_background/README.rst b/examples/fixture.no_background/README.rst +new file mode 100644 +index 0000000..4243f10 +--- /dev/null ++++ b/examples/fixture.no_background/README.rst +@@ -0,0 +1,110 @@ ++EXAMPLE: Disable Background Inheritance Mechanism for Scenario ++=============================================================================== ++ ++:RELATED-TO: #756 ++ ++This example shows how the Background inheritance mechanism in Gherkin ++can be disabled in ``behave``. ++ ++Parts of the recipe: ++ ++* features/example.feature (Feature file as example) ++* features/environment.py (glue code and hooks for fixture-tag / fixture) ++* behave_fixture_lib/no_background.py (fixture implementation, workhorse) ++ ++ ++.. warning:: BEWARE: This shows you how can do it, not that you should do it ++ ++ BETTER: ++ ++ * Use Rules to group Scenarios, each with its own Background (in Gherkin v6) ++ * Split Feature aspects into multiple feature files (if needed) ++ * ... (see issue #756 above) ++ ++ ++Explanation ++------------------------------------------------------------------------ ++ ++Example code how to provide a behave fixture to disable the ++background inheritance mechanism by using a fixture / fixture-tag. ++The fixture-tag "@fixture.behave.no_background" marks the ++location in Gherkin (which Scenario) where the fixture should be used ++ ++.. code-block:: gherkin ++ ++ # -- FILE: features/example.feature ++ Feature: Show how @fixture.behave.no_background is used ++ ++ Background: ++ Given a background step ++ ++ Scenario: Alice ++ When a step passes ++ And note that "Background steps are executed here" ++ ++ @fixture.behave.no_background ++ Scenario: Bob ++ Given I need another scenario setup ++ When another step passes ++ And note that "NO-BACKGROUND STEPS are executed here" ++ ++When the feature is executed, you see that: ++ ++* First Scenario "Alice": Background steps are inherited and executed first. ++* Second Scenario "Bob": No Background step is executed. ++ ++.. code-block:: sh ++ ++ $ ../../bin/behave -f plain features/example.feature ++ Feature: Override the Background Inheritance Mechanism in some Scenarios ++ Background: ++ ++ Scenario: Alice ++ Given a background step passes ... passed ++ When a step passes ... passed ++ And note that "Background steps are executed here" ... passed ++ FIXTURE-HINT: DISABLE-BACKGROUND FOR: Bob ++ ++ Scenario: Bob ++ Given I need another scenario setup ... passed ++ When another step passes ... passed ++ And note that "NO-BACKGROUND STEPS are executed here" ... passed ++ ++ 1 feature passed, 0 failed, 0 skipped ++ 2 scenarios passed, 0 failed, 0 skipped ++ 6 steps passed, 0 failed, 0 skipped, 0 undefined ++ ++ ++The environment file provides the glue code that the fixture is called: ++ ++.. code-block:: python ++ ++ # -- FILE: features/environment.py ++ from behave_fixture_lib.no_background import behave_no_background ++ from behave.fixture import use_fixture_by_tag ++ ++ # -- FIXTURE REGISTRY: ++ fixture_registry = { ++ "fixture.behave.no_background": behave_no_background, ++ } ++ ++ # ----------------------------------------------------------------------------- ++ # HOOKS: ++ # ----------------------------------------------------------------------------- ++ def before_tag(context, tag): ++ if tag.startswith("fixture."): ++ return use_fixture_by_tag(tag, context, fixture_registry) ++ ++ ++.. code-block:: python ++ ++ # -- FILE: behave_fixture_lib/no_background.py (fixture implementation) ++ from behave import fixture ++ ++ @fixture(name="fixture.behave.no_background") ++ def behave_no_background(ctx): ++ # -- SETUP-PART-ONLY: Disable background inheritance (for scenarios only). ++ current_scenario = ctx.scenario ++ if current_scenario: ++ print("FIXTURE-HINT: DISABLE-BACKGROUND FOR: %s" % current_scenario.name) ++ current_scenario.use_background = False +diff --git a/examples/fixture.no_background/behave_fixture_lib/__init__.py b/examples/fixture.no_background/behave_fixture_lib/__init__.py +new file mode 100644 +index 0000000..e69de29 +diff --git a/examples/fixture.no_background/behave_fixture_lib/no_background.py b/examples/fixture.no_background/behave_fixture_lib/no_background.py +new file mode 100644 +index 0000000..47bd0b5 +--- /dev/null ++++ b/examples/fixture.no_background/behave_fixture_lib/no_background.py +@@ -0,0 +1,72 @@ ++# -*- coding: UTF-8 -*- ++# RELATED-TO: #756 ++""" ++Example code how to provide a behave fixture to disable the ++background inheritance mechanism. ++ ++.. code-block:: gherkin ++ ++ # -- FILE: features/example.feature ++ Feature: Show how @fixture.behave.override_background is used ++ ++ Background: ++ Given a background step ++ ++ Scenario: Alice ++ When a step passes ++ And note that "Background steps are executed here" ++ ++ @fixture.behave.no_background ++ Scenario: Bob ++ Given I need another scenario setup ++ When another step passes ++ And note that "NO-BACKGROUND STEPS are executed here" ++ ++.. code-block:: python ++ ++ # -- FILE: features/environment.py ++ from behave_fixture_lib.override_background import behave_override_background ++ from behave.fixture import use_fixture_by_tag ++ ++ # -- FIXTURE REGISTRY: ++ fixture_registry = { ++ "fixture.behave.no_background": behave_override_background, ++ } ++ ++ def before_tag(context, tag): ++ if tag.startswith("fixture."): ++ return use_fixture_by_tag(tag, context, fixture_registry) ++ ++""" ++ ++from __future__ import absolute_import, print_function ++from behave import fixture ++ ++ ++# ----------------------------------------------------------------------------- ++# BEHAVE FIXTURES: ++# ----------------------------------------------------------------------------- ++@fixture(name="fixture.behave.ono_background") ++def behave_no_background(ctx): ++ """Override the Background inherintance mechanism. ++ If a Feature / Rule Background exists in a Feature, ++ all contained Scenarios inherit the Background's steps. ++ ++ This fixture disables this mechanism. ++ The tagged Gherkin element will no longer inherit the background steps. ++ ++ :param ctx: Context object to use (during a test run). ++ """ ++ # -- SETUP-PART-ONLY: Disable background inheritance (for scenarios only). ++ current_scenario = ctx.scenario ++ if current_scenario: ++ print("FIXTURE-HINT: DISABLE-BACKGROUND FOR: %s" % current_scenario.name) ++ current_scenario.use_background = False ++ ++ ++# ----------------------------------------------------------------------------- ++# MODULE SPECIFIC: ++# ----------------------------------------------------------------------------- ++fixture_registry = { ++ "fixture.behave.no_background": behave_no_background, ++} +diff --git a/examples/fixture.no_background/features/environment.py b/examples/fixture.no_background/features/environment.py +new file mode 100644 +index 0000000..18857b9 +--- /dev/null ++++ b/examples/fixture.no_background/features/environment.py +@@ -0,0 +1,35 @@ ++# -*- coding: UTF-8 -*- ++# -- FILE: features/environment.py ++import os.path ++import sys ++ ++# ----------------------------------------------------------------------------- ++# PYTHON PATH SETUP: ++# ----------------------------------------------------------------------------- ++HERE = os.path.dirname(__file__) ++TOPA = os.path.abspath(os.path.join(HERE, "..")) ++ ++def setup_python_path(): ++ sys.path.insert(0, TOPA) ++ ++setup_python_path() ++ ++# ----------------------------------------------------------------------------- ++# NORMAL PART: ++# ----------------------------------------------------------------------------- ++from behave_fixture_lib.no_background import behave_no_background ++from behave.fixture import use_fixture_by_tag ++ ++# -- FIXTURE REGISTRY: ++fixture_registry = { ++ "fixture.behave.no_background": behave_no_background, ++} ++ ++ ++# ----------------------------------------------------------------------------- ++# HOOKS: ++# ----------------------------------------------------------------------------- ++def before_tag(context, tag): ++ if tag.startswith("fixture."): ++ return use_fixture_by_tag(tag, context, fixture_registry) ++ +diff --git a/examples/fixture.no_background/features/example.feature b/examples/fixture.no_background/features/example.feature +new file mode 100644 +index 0000000..2025716 +--- /dev/null ++++ b/examples/fixture.no_background/features/example.feature +@@ -0,0 +1,18 @@ ++Feature: Disable the Background Inheritance Mechanism in some Scenarios ++ ++ . BEWARE: ++ . This is only an example how this can be done (PROOF-OF-CONCEPT). ++ . This is not an example that you should do this !!! ++ ++ Background: ++ Given a background step passes ++ ++ Scenario: Alice ++ When a step passes ++ And note that "BACKGROUND STEPS are executed here" ++ ++ @fixture.behave.no_background ++ Scenario: Bob ++ Given I need another scenario setup ++ When another step passes ++ And note that "NO-BACKGROUND STEPS are executed here" +diff --git a/examples/fixture.no_background/features/steps/basic_steps.py b/examples/fixture.no_background/features/steps/basic_steps.py +new file mode 100644 +index 0000000..34f2107 +--- /dev/null ++++ b/examples/fixture.no_background/features/steps/basic_steps.py +@@ -0,0 +1,13 @@ ++from behave import given, step ++ ++# @step(u'{word} step passes') ++# def step_passes_with_word(context, word): ++# pass ++ ++@step(u'{word} background step passes') ++def step_background_step_passes(context, word): ++ pass ++ ++@given(u'I need {word} scenario setup') ++def step_given_i_need_scenario_setup(context, word): ++ pass +diff --git a/examples/fixture.no_background/features/steps/use_steplib_behave4cmd.py b/examples/fixture.no_background/features/steps/use_steplib_behave4cmd.py +new file mode 100644 +index 0000000..bc32a32 +--- /dev/null ++++ b/examples/fixture.no_background/features/steps/use_steplib_behave4cmd.py +@@ -0,0 +1,12 @@ ++# -*- coding: utf-8 -*- ++""" ++Use behave4cmd0 step library (predecessor of behave4cmd). ++""" ++ ++from __future__ import absolute_import ++ ++# -- REGISTER-STEPS FROM STEP-LIBRARY: ++# import behave4cmd0.__all_steps__ ++# import behave4cmd0.failing_steps ++import behave4cmd0.passing_steps ++import behave4cmd0.note_steps diff --git a/meta-python/recipes-devtools/python/python3-behave/0050-EXAMPLE-Cleanup-Gherkin-v6-README.patch b/meta-python/recipes-devtools/python/python3-behave/0050-EXAMPLE-Cleanup-Gherkin-v6-README.patch new file mode 100644 index 000000000..b630d48e7 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0050-EXAMPLE-Cleanup-Gherkin-v6-README.patch @@ -0,0 +1,64 @@ +From b52d7c5d0141971f2e745ec806f829e2c7c897cf Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Tue, 9 Jul 2019 08:20:25 +0200 +Subject: [PATCH] EXAMPLE: Cleanup Gherkin v6 README + +--- + examples/gherkin_v6/README.rst | 23 +++++++++++-------- + .../features/steps/passing_steps.py | 11 +++++++++ + 2 files changed, 25 insertions(+), 9 deletions(-) + create mode 100644 examples/gherkin_v6/features/steps/passing_steps.py + +diff --git a/examples/gherkin_v6/README.rst b/examples/gherkin_v6/README.rst +index 58199dd..99af1c5 100644 +--- a/examples/gherkin_v6/README.rst ++++ b/examples/gherkin_v6/README.rst +@@ -2,17 +2,22 @@ Gherkin v6 Examples + ============================================================================= + + +-SCRATCHPAD: Problems +------------------------------------------------------------------------------ ++Provides example(s) of Gherkin v6 additions: + +-- SummaryReporter: Shows wrong counts when Rules are present:: ++* Rule concept ++* New aliases for Gherkin keywords (Scenario, ScenarioOutline) + +- ... +- 0 features passed, 0 failed, 1 skipped XXX +- 3 rules passed, 0 failed, 0 skipped +- 5 scenarios passed, 0 failed, 0 skipped +- 13 steps passed, 0 failed, 0 skipped, 0 undefined ++Rule functionality: + ++* A Rule is a scenario container similar to a Feature ++* A Feature may contain many Rules ++* A Rule may not contain other Rules ++* A Rule may contain a Background (and inherits its Feature Background) ++* A Rule inherits its Feature Background if it has no Background ++* A Rule may contain many Scenarios and/or ScenarioOutlines ++* A Rule may have tags + +-- Formatters: PrettyFormatter, PlainFormatter (at least) need Rule support ++New keyword aliases: + ++* "Scenario Template" for "Scenario Outline" ++* "Example" for "Scenario" +diff --git a/examples/gherkin_v6/features/steps/passing_steps.py b/examples/gherkin_v6/features/steps/passing_steps.py +new file mode 100644 +index 0000000..2714cb1 +--- /dev/null ++++ b/examples/gherkin_v6/features/steps/passing_steps.py +@@ -0,0 +1,11 @@ ++# -*- coding: UTF-8 -*- ++ ++from behave import step ++ ++@step(u'{word} step passes') ++def step_passes(ctx, word): ++ pass ++ ++@step(u'{word} step fails') ++def step_fails(ctx, word): ++ assert False, "XFAIL-STEP: {0} step fails".format(word) diff --git a/meta-python/recipes-devtools/python/python3-behave/0051-Improve-support-for-feature.background-inheritance-f.patch b/meta-python/recipes-devtools/python/python3-behave/0051-Improve-support-for-feature.background-inheritance-f.patch new file mode 100644 index 000000000..764ff7869 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0051-Improve-support-for-feature.background-inheritance-f.patch @@ -0,0 +1,1510 @@ +From 4f013b949f6528ec70105c1fb8e292f23e3a13c8 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Wed, 10 Jul 2019 22:38:13 +0200 +Subject: [PATCH] Improve support for feature.background inheritance for + rule.background. + +--- + .gitignore | 3 + + behave/model.py | 230 ++++++++++-- + behave/parser.py | 9 +- + .../fixture.override_background/README.rst | 116 ------ + .../behave_fixture_lib/__init__.py | 0 + .../behave_fixture_lib/override_background.py | 80 ----- + .../features/environment.py | 35 -- + .../features/example.feature | 18 - + .../features/steps/basic_steps.py | 13 - + .../features/steps/use_steplib_behave4cmd.py | 12 - + setup.py | 1 + + tests/unit/test_model.py | 117 +----- + tests/unit/test_model2.py | 4 - + tests/unit/test_model_core.py | 116 +++++- + tests/unit/test_parser_gherkin_v6.py | 339 +++++++++++++++++- + 15 files changed, 645 insertions(+), 448 deletions(-) + delete mode 100644 examples/fixture.override_background/README.rst + delete mode 100644 examples/fixture.override_background/behave_fixture_lib/__init__.py + delete mode 100644 examples/fixture.override_background/behave_fixture_lib/override_background.py + delete mode 100644 examples/fixture.override_background/features/environment.py + delete mode 100644 examples/fixture.override_background/features/example.feature + delete mode 100644 examples/fixture.override_background/features/steps/basic_steps.py + delete mode 100644 examples/fixture.override_background/features/steps/use_steplib_behave4cmd.py + +diff --git a/.gitignore b/.gitignore +index 6196a6d..9c5c33d 100644 +--- a/.gitignore ++++ b/.gitignore +@@ -7,6 +7,9 @@ build/ + dist/ + __pycache__/ + __WORKDIR__/ ++__*/ ++__*.txt ++__*.rst + _build/ + _WORKSPACE/ + reports/ +diff --git a/behave/model.py b/behave/model.py +index 7fc534a..69f38ab 100644 +--- a/behave/model.py ++++ b/behave/model.py +@@ -29,6 +29,36 @@ else: + import traceback + + ++# --------------------------------------------------------------------------- ++# MODEL UTILITIES: ++# --------------------------------------------------------------------------- ++def reset_steps(steps): ++ for step in steps: ++ step.reset() ++ return steps ++ ++ ++def copy_steps(steps): ++ """Copy steps; needed if steps should be used in multiple run contexts. ++ ++ :param steps: List of steps to copy. ++ :return: List of copied steps. ++ """ ++ return [copy.copy(step) for step in steps] ++ ++ ++def copy_and_reset_steps(steps): ++ """Copy steps and reset each step (status, duration, etc.) ++ ++ :param steps: List of steps to copy. ++ :return: List of copied steps. ++ """ ++ return reset_steps(copy_steps(steps)) ++ ++ ++# --------------------------------------------------------------------------- ++# MODEL CLASSES: ++# --------------------------------------------------------------------------- + class ScenarioContainer(TagAndStatusStatement, Replayable): + """Abstract base class for model elements + that contains the following structure: +@@ -198,8 +228,8 @@ class ScenarioContainer(TagAndStatusStatement, Replayable): + + if skipped: + return Status.skipped +- else: +- return Status.passed ++ # -- OTHERWISE: ++ return Status.passed + + @property + def duration(self): +@@ -230,7 +260,8 @@ class ScenarioContainer(TagAndStatusStatement, Replayable): + rule = run_item + if with_rules: + all_scenarios.append(rule) +- all_scenarios.extend(rule.walk_scenarios(with_outlines=with_outlines)) ++ scenarios = rule.walk_scenarios(with_outlines=with_outlines) ++ all_scenarios.extend(scenarios) + elif isinstance(run_item, ScenarioOutline): + scenario_outline = run_item + if with_outlines: +@@ -241,6 +272,16 @@ class ScenarioContainer(TagAndStatusStatement, Replayable): + all_scenarios.append(run_item) + return all_scenarios + ++ def iter_scenarios(self): ++ return iter(self.walk_scenarios()) ++ ++ def iter_scenario_outlines(self): ++ return iter([x for x in self.walk_scenarios(with_outlines=True) ++ if isinstance(x, ScenarioOutline)]) ++ ++ def iter_rules(self): ++ return iter([x for x in self.run_items if isinstance(x, Rule)]) ++ + def should_run(self, config=None): + """ + Determines if this Feature (and its scenarios) should run. +@@ -312,7 +353,7 @@ class ScenarioContainer(TagAndStatusStatement, Replayable): + :param runner: Runner to use. + :return: True, if test-run failed. + """ +- # pylint: disable=too-many-branches ++ # pylint: disable=too-many-branches, too-many-locals, too-many-statements + # MAYBE: self.reset() + self.clear_status() + self.hook_failed = False +@@ -387,7 +428,7 @@ class ScenarioContainer(TagAndStatusStatement, Replayable): + # -- PERFORM CONTEXT CLEANUP: May raise cleanup errors. + try: + runner.context._pop() # pylint: disable=protected-access +- except Exception: ++ except Exception: # pylint: disable=broad-except + # -- CLEANUP-ERROR: + self.set_status(Status.failed) + +@@ -509,16 +550,28 @@ class Feature(ScenarioContainer): + def _setup_context_for_run(self, context): + context.feature = self + ++ def add_background(self, background): ++ self.background = background ++ self.background.parent = self ++ + def add_rule(self, rule): +- """Add a rule to this feature.""" ++ """Add a rule to this feature (supported in: Gherkin v6). ++ ++ .. versionadded: 1.2.7 ++ """ + feature = self + rule.parent = feature + rule.feature = feature +- if not rule.background: +- # -- MAYBE: Inherit feature.background if the rule has no background. +- rule.background = self.background + self.rules.append(rule) + self.run_items.append(rule) ++ if self.background: ++ # -- ENSURE: Rule inherits feature.background. ++ if not rule.background: ++ # -- ENSURE: Rule has a default background. ++ # Necessary to inherit feature.background (or disable it). ++ rule_default_background = Background(rule.filename, rule.line) ++ rule.add_background(rule_default_background) ++ rule.background.inherited_background = self.background + + + class Rule(ScenarioContainer): +@@ -630,6 +683,7 @@ class Rule(ScenarioContainer): + description, scenarios, background) + self.parent = parent + self.feature = parent ++ self._use_background_inheritance = True + + def _setup_context_for_run(self, context): + context.rule = self +@@ -638,10 +692,43 @@ class Rule(ScenarioContainer): + return '' % \ + (self.name, len(self.scenarios)) + ++ def add_background(self, background, inherited=None): ++ if inherited is None: ++ feature = self.feature or self.parent ++ inherited = feature.background ++ ++ self.background = background ++ self.background.inherited_background = inherited ++ self.background.use_inheritance = self.use_background_inheritance ++ self.background.parent = self ++ # -- ENSURE: Normally background is added before scenarios. ++ for scenario in self.walk_scenarios(): ++ scenario.background = self.background ++ ++ @property ++ def use_background_inheritance(self): ++ return self._use_background_inheritance ++ ++ @use_background_inheritance.setter ++ def use_background_inheritance(self, value): ++ self._use_background_inheritance = value ++ if self.background: ++ self.background.use_inheritance = value ++ + + class Background(BasicStatement, Replayable): + """A `background`_ parsed from a *feature file*. + ++ Behaviour: ++ ++ * Each scenario of a scenario container (Feature, Rule) ++ inherits the Background of its scenario container ++ * Background steps in a scenario are executed before scenario steps ++ * Rule Background inherits the Feature Background (outer background) if any ++ * Inherited Background steps are used/executed first ++ * Optionally, background inheritance can be disabled ++ (normally: by using a fixture/fixture-tag) ++ + The attributes are: + + .. attribute:: keyword +@@ -679,23 +766,65 @@ class Background(BasicStatement, Replayable): + + .. _`background`: gherkin.html#backgrounds + """ +- # TODO: Background inheritance +- # Rule.background should inherit its Feature.background steps (if available) +- # Rule.background = Feature.background iff not Rule.background exists (ALREADY-SOLVED) +- # Rule may override background inheritance mechanism + type = "background" + +- def __init__(self, filename, line, keyword, name, steps=None, description=None): ++ def __init__(self, filename, line, keyword=u"Background", name=u"", ++ steps=None, description=None): + super(Background, self).__init__(filename, line, keyword, name) + self.description = description or [] + self.steps = steps or [] ++ self.inherited_background = None ++ self._inherited_steps = None ++ self._use_inheritance = True + +- def __repr__(self): +- return '' % self.name ++ @property ++ def use_inheritance(self): ++ """Indicates if this Background should inherit from an outer Background. ++ Background inheritance mechanism is enabled (per default). ++ Optionally, this mechanism can be disabled (or overridden). + +- def __iter__(self): ++ :return: Current background inheritance state (as bool). ++ ++ .. versionadded:: 1.2.7 ++ """ ++ return self._use_inheritance ++ ++ @use_inheritance.setter ++ def use_inheritance(self, value): ++ """Enable/disable background inheritance mechanism for this Background. ++ ++ :param value: New value (as bool). ++ ++ .. versionadded:: 1.2.7 ++ """ ++ # -- ENSURE: inherited_steps are reinitialized (later). ++ self._use_inheritance = bool(value) ++ self._inherited_steps = None ++ ++ @property ++ def inherited_steps(self): ++ # versionadded:: 1.2.7 ++ if self._inherited_steps is None: ++ # -- LAZY-INIT: Support enable/disable the inheritance mechanism. ++ steps = [] ++ if self.inherited_background and self._use_inheritance: ++ steps = copy_and_reset_steps(self.inherited_background.steps) ++ self._inherited_steps = steps ++ return self._inherited_steps ++ ++ def iter_steps(self): ++ """Returns iterator to all steps, including inherited steps (if any). ++ ++ .. versionadded:: 1.2.7 ++ """ ++ if self.inherited_steps: ++ return itertools.chain(self.inherited_steps, self.steps) + return iter(self.steps) + ++ @property ++ def all_steps(self): ++ return self.iter_steps() ++ + @property + def duration(self): + duration = 0 +@@ -703,6 +832,12 @@ class Background(BasicStatement, Replayable): + duration += step.duration + return duration + ++ def __repr__(self): ++ return '' % self.name ++ ++ def __iter__(self): ++ return self.iter_steps() ++ + + class Scenario(TagAndStatusStatement, Replayable): + """A `scenario`_ parsed from a *feature file*. +@@ -799,6 +934,7 @@ class Scenario(TagAndStatusStatement, Replayable): + self.feature = None # REFER-TO: owner=Feature + self.hook_failed = False + self._background_steps = None ++ self._use_background = True + self._row = None + self.was_dry_run = False + +@@ -813,6 +949,27 @@ class Scenario(TagAndStatusStatement, Replayable): + for step in self.all_steps: + step.reset() + ++ @property ++ def use_background(self): ++ """Indicates if the background is/would be used (if any exists). ++ NOTE: The Background (steps) are normally used. ++ ++ .. versionadded:: 1.2.7 ++ """ ++ return self._use_background ++ ++ @use_background.setter ++ def use_background(self, value): ++ """Enable/disable the usage of the background (steps). ++ ++ :param value: New value (as bool). ++ ++ .. versionadded:: 1.2.7 ++ """ ++ # -- ENSURE: background_steps are reinitialized. ++ self._use_background = value ++ self._background_steps = None ++ + @property + def background_steps(self): + """Provide background steps if feature/rule has a background. +@@ -828,24 +985,29 @@ class Scenario(TagAndStatusStatement, Replayable): + # Each scenario needs own background.steps. + # Otherwise, background step status of the last-run scenario is used. + steps = [] +- if self.background: +- steps = [copy.copy(step) for step in self.background.steps] ++ if self.background and self.use_background: ++ steps = copy_and_reset_steps(self.background.all_steps) + self._background_steps = steps + return self._background_steps + +- @property +- def all_steps(self): +- """Returns iterator to all steps, including background steps if any.""" ++ def iter_steps(self): ++ """Returns iterator to all steps, including background steps if any. ++ ++ .. versionadded:: 1.2.7 ++ """ + if self.background is not None: + return itertools.chain(self.background_steps, self.steps) +- else: +- return iter(self.steps) ++ return iter(self.steps) ++ ++ @property ++ def all_steps(self): ++ return self.iter_steps() + + def __repr__(self): + return '' % self.name + + def __iter__(self): +- return self.all_steps ++ return self.iter_steps() + + def compute_status(self): + """Compute the status of the scenario from its steps +@@ -862,9 +1024,8 @@ class Scenario(TagAndStatusStatement, Replayable): + # -- SPECIAL CASE: In dry-run with undefined-step discovery + # Undefined steps should not cause failed scenario. + return Status.untested +- else: +- # -- NORMALLY: Undefined steps cause failed scenario. +- return Status.failed ++ # -- NORMALLY: Undefined steps cause failed scenario. ++ return Status.failed + elif step.status != Status.passed: + # pylint: disable=line-too-long + assert step.status in (Status.failed, Status.skipped, Status.untested) +@@ -1029,7 +1190,6 @@ class Scenario(TagAndStatusStatement, Replayable): + # BUT: Detect all remaining undefined steps. + step.status = Status.skipped + if dry_run_scenario: +- # pylint: disable=redefined-variable-type + step.status = Status.untested + found_step_match = runner.step_registry.find_match(step) + if not found_step_match: +@@ -1067,7 +1227,7 @@ class Scenario(TagAndStatusStatement, Replayable): + # -- PERFORM CONTEXT-CLEANUP: May raise cleanup errors. + try: + runner.context._pop() # pylint: disable=protected-access +- except Exception: ++ except Exception: # pylint: disable=broad-except + self.set_status(Status.failed) + failed = True + +@@ -1176,9 +1336,9 @@ class ScenarioOutlineBuilder(object): + placeholder = u"<%s>" % name + for i, cell in enumerate(new_step.table.headings): + new_step.table.headings[i] = cell.replace(placeholder, value) +- for row in new_step.table: +- for i, cell in enumerate(row.cells): +- row.cells[i] = cell.replace(placeholder, value) ++ for step_row in new_step.table: ++ for i, cell in enumerate(step_row.cells): ++ step_row.cells[i] = cell.replace(placeholder, value) + return new_step + + def build_scenarios(self, scenario_outline): +@@ -1640,7 +1800,6 @@ class Step(BasicStatement, Replayable): + match.run(runner.context) + if self.status == Status.untested: + # -- NOTE: Executed step may have skipped scenario and itself. +- # pylint: disable=redefined-variable-type + self.status = Status.passed + except KeyboardInterrupt as e: + runner.aborted = True +@@ -1815,8 +1974,7 @@ class Table(Replayable): + """ + if self.has_column(column_name): + return self.get_column_index(column_name) +- else: +- return self.add_column(column_name) ++ return self.add_column(column_name) + + def __repr__(self): + return "" % (len(self.headings), len(self.rows)) +diff --git a/behave/parser.py b/behave/parser.py +index 993c9dc..520f678 100644 +--- a/behave/parser.py ++++ b/behave/parser.py +@@ -249,7 +249,6 @@ class Parser(object): + self.rule = rule + self.scenario_container = rule + self.statement = rule +- # MAYBE: self.background = None + self.feature.add_rule(self.statement) + # -- RESET STATE: + self.tags = [] +@@ -258,11 +257,15 @@ class Parser(object): + if self.tags: + msg = u"Background supports no tags: @%s" % (u" @".join(self.tags)) + raise ParserError(msg, self.line, self.filename, line) ++ elif self.scenario_container and self.scenario_container.background: ++ if self.scenario_container.background.steps: ++ # -- HINT: Rule may have default background w/o steps. ++ msg = u"Second Background (can have only one)" ++ raise ParserError(msg, self.line, self.filename, line) + name = line[len(keyword) + 1:].strip() + background = model.Background(self.filename, self.line, keyword, name) ++ self.scenario_container.add_background(background) + self.statement = background +- self.scenario_container.background = background +- # OLD: self.feature.background = self.statement + + def _build_scenario_statement(self, keyword, line): + name = line[len(keyword) + 1:].strip() +diff --git a/examples/fixture.override_background/README.rst b/examples/fixture.override_background/README.rst +deleted file mode 100644 +index 9c150cc..0000000 +--- a/examples/fixture.override_background/README.rst ++++ /dev/null +@@ -1,116 +0,0 @@ +-EXAMPLE: Override / Disable Background Inheritance Mechanism for Scenario +-=============================================================================== +- +-:RELATED-TO: #756 +- +-This example shows how the Background inheritance mechanism in Gherkin +-can be disabled in ``behave``. +- +-Parts of the recipe: +- +-* features/example.feature (Feature file as example) +-* features/environment.py (glue code and hooks for fixture-tag / fixture) +-* behave_fixture_lib/override_background.py (fixture implementation, workhorse) +- +- +-.. warning:: BEWARE: This shows you how can do it, not that you should do it +- +- BETTER: +- +- * Use Rules to group Scenarios, each with its own Background (in Gherkin v6) +- * Split Feature aspects into multiple feature files (if needed) +- * ... (see issue #756 above) +- +- +-Explanation +------------------------------------------------------------------------- +- +-Example code how to provide a behave fixture to disable the +-background inheritance mechanism by using a fixture / fixture-tag. +-The fixture-tag "@ixture.behave.override_background" marks the +-location in Gherkin (which Scenario) where the fixture should be used +- +-.. code-block:: gherkin +- +- # -- FILE: features/example.feature +- Feature: Show how @fixture.behave.override_background is used +- +- Background: +- Given a background step +- +- Scenario: Alice +- When a step passes +- And note that "Background steps are executed here" +- +- @fixture.behave.overide_background +- Scenario: Bob +- Given I need another scenario setup +- When another step passes +- And note that "NO-BACKGROUND STEPS are executed here" +- +-When the feature is executed, you see that: +- +-* First Scenario "Alice": Background steps are inherited and executed first. +-* Second Scenario "Bob": No Background step is executed. +- +-.. code-block:: sh +- +- $ ../../bin/behave -f plain features/example.feature +- Feature: Override the Background Inheritance Mechanism in some Scenarios +- Background: +- +- Scenario: Alice +- Given a background step passes ... passed +- When a step passes ... passed +- And note that "Background steps are executed here" ... passed +- FIXTURE-HINT: DISABLE-BACKGROUND FOR: Bob +- +- Scenario: Bob +- Given I need another scenario setup ... passed +- When another step passes ... passed +- And note that "NO-BACKGROUND STEPS are executed here" ... passed +- +- 1 feature passed, 0 failed, 0 skipped +- 2 scenarios passed, 0 failed, 0 skipped +- 6 steps passed, 0 failed, 0 skipped, 0 undefined +- +- +-The environment file provides the glue code that the fixture is called: +- +-.. code-block:: python +- +- # -- FILE: features/environment.py +- from behave_fixture_lib.override_background import behave_override_background +- from behave.fixture import use_fixture_by_tag +- +- # -- FIXTURE REGISTRY: +- fixture_registry = { +- "fixture.behave.overide_background": behave_override_background, +- } +- +- # ----------------------------------------------------------------------------- +- # HOOKS: +- # ----------------------------------------------------------------------------- +- def before_tag(context, tag): +- if tag.startswith("fixture."): +- return use_fixture_by_tag(tag, context, fixture_registry) +- +- +-.. code-block:: python +- +- # -- FILE: behave_fixture_lib/override_background.py (fixture implementation) +- from behave import fixture +- +- @fixture(name="fixture.behave.override_background") +- def behave_override_background(ctx): +- # -- SETUP-PART-ONLY: Disable background inheritance (for scenarios only). +- current_scenario = ctx.scenario +- if current_scenario: +- behave_disable_background_inheritance_for_scenario(current_scenario) +- +- # ----------------------------------------------------------------------------- +- # BEHAVE UTILITY: +- # ----------------------------------------------------------------------------- +- def behave_disable_background_inheritance_for_scenario(scenario): +- print("FIXTURE-HINT: DISABLE-BACKGROUND FOR: %s" % scenario.name) +- scenario.background = None +diff --git a/examples/fixture.override_background/behave_fixture_lib/__init__.py b/examples/fixture.override_background/behave_fixture_lib/__init__.py +deleted file mode 100644 +index e69de29..0000000 +diff --git a/examples/fixture.override_background/behave_fixture_lib/override_background.py b/examples/fixture.override_background/behave_fixture_lib/override_background.py +deleted file mode 100644 +index 6c572cf..0000000 +--- a/examples/fixture.override_background/behave_fixture_lib/override_background.py ++++ /dev/null +@@ -1,80 +0,0 @@ +-# -*- coding: UTF-8 -*- +-# RELATED-TO: #756 +-""" +-Example code how to provide a behave fixture to disable the +-background inheritance mechanism. +- +-.. code-block:: gherkin +- +- # -- FILE: features/example.feature +- Feature: Show how @fixture.behave.override_background is used +- +- Background: +- Given a background step +- +- Scenario: Alice +- When a step passes +- And note that "Background steps are executed here" +- +- @fixture.behave.overide_background +- Scenario: Bob +- Given I need another scenario setup +- When another step passes +- And note that "NO-BACKGROUND STEPS are executed here" +- +-.. code-block:: python +- +- # -- FILE: features/environment.py +- from behave_fixture_lib.override_background import behave_override_background +- from behave.fixture import use_fixture_by_tag +- +- # -- FIXTURE REGISTRY: +- fixture_registry = { +- "fixture.behave.overide_background": behave_override_background, +- } +- +- def before_tag(context, tag): +- if tag.startswith("fixture."): +- return use_fixture_by_tag(tag, context, fixture_registry) +- +-""" +- +-from __future__ import absolute_import, print_function +-from behave import fixture +- +- +-# ----------------------------------------------------------------------------- +-# BEHAVE FIXTURES: +-# ----------------------------------------------------------------------------- +-@fixture(name="fixture.behave.override_background") +-def behave_override_background(ctx): +- """Override the Background inherintance mechanism. +- If a Feature / Rule Background exists in a Feature, +- all contained Scenarios inherit the Background's steps. +- +- This fixture disables this mechanism. +- The tagged Gherkin element will no longer inherit the background steps. +- +- :param ctx: Context object to use (during a test run). +- """ +- # -- SETUP-PART-ONLY: Disable background inheritance (for scenarios only). +- current_scenario = ctx.scenario +- if current_scenario: +- behave_disable_background_inheritance_for_scenario(current_scenario) +- +- +-# ----------------------------------------------------------------------------- +-# BEHAVE UTILITY: +-# ----------------------------------------------------------------------------- +-def behave_disable_background_inheritance_for_scenario(scenario): +- print("FIXTURE-HINT: DISABLE-BACKGROUND FOR: %s" % scenario.name) +- scenario.background = None +- # scenario._background_steps = [] +- +- +-# ----------------------------------------------------------------------------- +-# MODULE SPECIFIC: +-# ----------------------------------------------------------------------------- +-fixture_registry = { +- "fixture.behave.overide_background": behave_override_background, +-} +diff --git a/examples/fixture.override_background/features/environment.py b/examples/fixture.override_background/features/environment.py +deleted file mode 100644 +index 7a4b735..0000000 +--- a/examples/fixture.override_background/features/environment.py ++++ /dev/null +@@ -1,35 +0,0 @@ +-# -*- coding: UTF-8 -*- +-# -- FILE: features/environment.py +-import os.path +-import sys +- +-# ----------------------------------------------------------------------------- +-# PYTHON PATH SETUP: +-# ----------------------------------------------------------------------------- +-HERE = os.path.dirname(__file__) +-TOPA = os.path.abspath(os.path.join(HERE, "..")) +- +-def setup_python_path(): +- sys.path.insert(0, TOPA) +- +-setup_python_path() +- +-# ----------------------------------------------------------------------------- +-# NORMAL PART: +-# ----------------------------------------------------------------------------- +-from behave_fixture_lib.override_background import behave_override_background +-from behave.fixture import use_fixture_by_tag +- +-# -- FIXTURE REGISTRY: +-fixture_registry = { +- "fixture.behave.overide_background": behave_override_background, +-} +- +- +-# ----------------------------------------------------------------------------- +-# HOOKS: +-# ----------------------------------------------------------------------------- +-def before_tag(context, tag): +- if tag.startswith("fixture."): +- return use_fixture_by_tag(tag, context, fixture_registry) +- +diff --git a/examples/fixture.override_background/features/example.feature b/examples/fixture.override_background/features/example.feature +deleted file mode 100644 +index 5ddd874..0000000 +--- a/examples/fixture.override_background/features/example.feature ++++ /dev/null +@@ -1,18 +0,0 @@ +-Feature: Override the Background Inheritance Mechanism in some Scenarios +- +- . BEWARE: +- . This is only an example how this can be done (PROOF-OF-CONCEPT). +- . This is not an example that you should do this !!! +- +- Background: +- Given a background step passes +- +- Scenario: Alice +- When a step passes +- And note that "Background steps are executed here" +- +- @fixture.behave.overide_background +- Scenario: Bob +- Given I need another scenario setup +- When another step passes +- And note that "NO-BACKGROUND STEPS are executed here" +diff --git a/examples/fixture.override_background/features/steps/basic_steps.py b/examples/fixture.override_background/features/steps/basic_steps.py +deleted file mode 100644 +index 34f2107..0000000 +--- a/examples/fixture.override_background/features/steps/basic_steps.py ++++ /dev/null +@@ -1,13 +0,0 @@ +-from behave import given, step +- +-# @step(u'{word} step passes') +-# def step_passes_with_word(context, word): +-# pass +- +-@step(u'{word} background step passes') +-def step_background_step_passes(context, word): +- pass +- +-@given(u'I need {word} scenario setup') +-def step_given_i_need_scenario_setup(context, word): +- pass +diff --git a/examples/fixture.override_background/features/steps/use_steplib_behave4cmd.py b/examples/fixture.override_background/features/steps/use_steplib_behave4cmd.py +deleted file mode 100644 +index bc32a32..0000000 +--- a/examples/fixture.override_background/features/steps/use_steplib_behave4cmd.py ++++ /dev/null +@@ -1,12 +0,0 @@ +-# -*- coding: utf-8 -*- +-""" +-Use behave4cmd0 step library (predecessor of behave4cmd). +-""" +- +-from __future__ import absolute_import +- +-# -- REGISTER-STEPS FROM STEP-LIBRARY: +-# import behave4cmd0.__all_steps__ +-# import behave4cmd0.failing_steps +-import behave4cmd0.passing_steps +-import behave4cmd0.note_steps +diff --git a/setup.py b/setup.py +index cea4392..8de3ec0 100644 +--- a/setup.py ++++ b/setup.py +@@ -131,6 +131,7 @@ setup( + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", ++ "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: Jython", + "Programming Language :: Python :: Implementation :: PyPy", +diff --git a/tests/unit/test_model.py b/tests/unit/test_model.py +index c1fc424..21d6c27 100644 +--- a/tests/unit/test_model.py ++++ b/tests/unit/test_model.py +@@ -8,7 +8,7 @@ from mock import Mock, patch + import six + from six.moves import range # pylint: disable=redefined-builtin + from six.moves import zip # pylint: disable=redefined-builtin +-from behave.model_core import FileLocation, Status ++from behave.model_core import Status + from behave.model import Feature, Scenario, ScenarioOutline, Step + from behave.model import Table, Row + from behave.matchers import NoMatch +@@ -20,19 +20,12 @@ from behave import step_registry + + if six.PY2: + # pylint: disable=unused-import +- import traceback2 as traceback + traceback_modname = "traceback2" + else: + # pylint: disable=unused-import +- import traceback + traceback_modname = "traceback" + + +- +-# -- CONVENIENCE-ALIAS: +-_text = six.text_type +- +- + class TestFeatureRun(unittest.TestCase): + # pylint: disable=invalid-name + +@@ -769,111 +762,3 @@ class TestModelRow(unittest.TestCase): + assert data1["name"] == u"Alice" + assert data1["sex"] == u"female" + assert data1["age"] == u"12" +- +- +-class TestFileLocation(unittest.TestCase): +- # pylint: disable=invalid-name +- ordered_locations1 = [ +- FileLocation("features/alice.feature", 1), +- FileLocation("features/alice.feature", 5), +- FileLocation("features/alice.feature", 10), +- FileLocation("features/alice.feature", 11), +- FileLocation("features/alice.feature", 100), +- ] +- ordered_locations2 = [ +- FileLocation("features/alice.feature", 1), +- FileLocation("features/alice.feature", 10), +- FileLocation("features/bob.feature", 5), +- FileLocation("features/charly.feature", None), +- FileLocation("features/charly.feature", 0), +- FileLocation("features/charly.feature", 100), +- ] +- same_locations = [ +- (FileLocation("alice.feature"), +- FileLocation("alice.feature", None), +- ), +- (FileLocation("alice.feature", 10), +- FileLocation("alice.feature", 10), +- ), +- (FileLocation("features/bob.feature", 11), +- FileLocation("features/bob.feature", 11), +- ), +- ] +- +- def test_compare_equal(self): +- for value1, value2 in self.same_locations: +- assert value1 == value2 +- +- def test_compare_equal_with_string(self): +- for location in self.ordered_locations2: +- assert location == location.filename +- assert location.filename == location +- +- def test_compare_not_equal(self): +- for value1, value2 in self.same_locations: +- assert not(value1 != value2) # pylint: disable=unneeded-not, superfluous-parens +- +- for locations in [self.ordered_locations1, self.ordered_locations2]: +- for value1, value2 in zip(locations, locations[1:]): +- assert value1 != value2 +- +- def test_compare_less_than(self): +- for locations in [self.ordered_locations1, self.ordered_locations2]: +- for value1, value2 in zip(locations, locations[1:]): +- assert value1 < value2, "FAILED: %s < %s" % (_text(value1), _text(value2)) +- assert value1 != value2 +- +- def test_compare_less_than_with_string(self): +- locations = self.ordered_locations2 +- for value1, value2 in zip(locations, locations[1:]): +- if value1.filename == value2.filename: +- continue +- assert value1 < value2.filename, \ +- "FAILED: %s < %s" % (_text(value1), _text(value2.filename)) +- assert value1.filename < value2, \ +- "FAILED: %s < %s" % (_text(value1.filename), _text(value2)) +- +- def test_compare_greater_than(self): +- for locations in [self.ordered_locations1, self.ordered_locations2]: +- for value1, value2 in zip(locations, locations[1:]): +- assert value2 > value1, "FAILED: %s > %s" % (_text(value2), _text(value1)) +- assert value2 != value1 +- +- def test_compare_less_or_equal(self): +- for value1, value2 in self.same_locations: +- assert value1 <= value2, "FAILED: %s <= %s" % (_text(value1), _text(value2)) +- assert value1 == value2 +- +- for locations in [self.ordered_locations1, self.ordered_locations2]: +- for value1, value2 in zip(locations, locations[1:]): +- assert value1 <= value2, "FAILED: %s <= %s" % (_text(value1), _text(value2)) +- assert value1 != value2 +- +- def test_compare_greater_or_equal(self): +- for value1, value2 in self.same_locations: +- assert value2 >= value1, "FAILED: %s >= %s" % (_text(value2), _text(value1)) +- assert value2 == value1 +- +- for locations in [self.ordered_locations1, self.ordered_locations2]: +- for value1, value2 in zip(locations, locations[1:]): +- assert value2 >= value1, "FAILED: %s >= %s" % (_text(value2), _text(value1)) +- assert value2 != value1 +- +- def test_filename_should_be_same_as_self(self): +- for location in self.ordered_locations2: +- assert location == location.filename +- assert location.filename == location +- +- def test_string_conversion(self): +- for location in self.ordered_locations2: +- expected = u"%s:%s" % (location.filename, location.line) +- if location.line is None: +- expected = location.filename +- assert six.text_type(location) == expected +- +- def test_repr_conversion(self): +- for location in self.ordered_locations2: +- expected = u'' % \ +- (location.filename, location.line) +- actual = repr(location) +- assert actual == expected, "FAILED: %s == %s" % (actual, expected) +diff --git a/tests/unit/test_model2.py b/tests/unit/test_model2.py +index 7884b90..a86b80e 100644 +--- a/tests/unit/test_model2.py ++++ b/tests/unit/test_model2.py +@@ -35,10 +35,6 @@ def step_to_text(step, indentation=" "): + return step_text.rstrip() + + +-# -- PYTEST MARKERS/ANNOTATIONS: +-not_implemented_yet = pytest.mark.skip("NOT-IMPLEMENTED-YET") +- +- + # ---------------------------------------------------------------------------- + # TEST SUITE: + # ---------------------------------------------------------------------------- +diff --git a/tests/unit/test_model_core.py b/tests/unit/test_model_core.py +index b5f20c4..3cb5efa 100644 +--- a/tests/unit/test_model_core.py ++++ b/tests/unit/test_model_core.py +@@ -4,10 +4,16 @@ + """ + + from __future__ import print_function +-from behave.model_core import Status ++import six ++from behave.model_core import Status, FileLocation + import pytest + + ++# -- CONVENIENCE-ALIAS: ++_text = six.text_type ++ ++ ++ + # ----------------------------------------------------------------------------- + # TESTS: + # ----------------------------------------------------------------------------- +@@ -54,3 +60,111 @@ class TestStatus(object): + def test_from_name__with_unknown_name_raises_lookuperror(self, unknown_name): + with pytest.raises(LookupError): + Status.from_name(unknown_name) ++ ++ ++class TestFileLocation(object): ++ # pylint: disable=invalid-name ++ ordered_locations1 = [ ++ FileLocation("features/alice.feature", 1), ++ FileLocation("features/alice.feature", 5), ++ FileLocation("features/alice.feature", 10), ++ FileLocation("features/alice.feature", 11), ++ FileLocation("features/alice.feature", 100), ++ ] ++ ordered_locations2 = [ ++ FileLocation("features/alice.feature", 1), ++ FileLocation("features/alice.feature", 10), ++ FileLocation("features/bob.feature", 5), ++ FileLocation("features/charly.feature", None), ++ FileLocation("features/charly.feature", 0), ++ FileLocation("features/charly.feature", 100), ++ ] ++ same_locations = [ ++ (FileLocation("alice.feature"), ++ FileLocation("alice.feature", None), ++ ), ++ (FileLocation("alice.feature", 10), ++ FileLocation("alice.feature", 10), ++ ), ++ (FileLocation("features/bob.feature", 11), ++ FileLocation("features/bob.feature", 11), ++ ), ++ ] ++ ++ def test_compare_equal(self): ++ for value1, value2 in self.same_locations: ++ assert value1 == value2 ++ ++ def test_compare_equal_with_string(self): ++ for location in self.ordered_locations2: ++ assert location == location.filename ++ assert location.filename == location ++ ++ def test_compare_not_equal(self): ++ for value1, value2 in self.same_locations: ++ assert not(value1 != value2) # pylint: disable=unneeded-not, superfluous-parens ++ ++ for locations in [self.ordered_locations1, self.ordered_locations2]: ++ for value1, value2 in zip(locations, locations[1:]): ++ assert value1 != value2 ++ ++ def test_compare_less_than(self): ++ for locations in [self.ordered_locations1, self.ordered_locations2]: ++ for value1, value2 in zip(locations, locations[1:]): ++ assert value1 < value2, "FAILED: %s < %s" % (_text(value1), _text(value2)) ++ assert value1 != value2 ++ ++ def test_compare_less_than_with_string(self): ++ locations = self.ordered_locations2 ++ for value1, value2 in zip(locations, locations[1:]): ++ if value1.filename == value2.filename: ++ continue ++ assert value1 < value2.filename, \ ++ "FAILED: %s < %s" % (_text(value1), _text(value2.filename)) ++ assert value1.filename < value2, \ ++ "FAILED: %s < %s" % (_text(value1.filename), _text(value2)) ++ ++ def test_compare_greater_than(self): ++ for locations in [self.ordered_locations1, self.ordered_locations2]: ++ for value1, value2 in zip(locations, locations[1:]): ++ assert value2 > value1, "FAILED: %s > %s" % (_text(value2), _text(value1)) ++ assert value2 != value1 ++ ++ def test_compare_less_or_equal(self): ++ for value1, value2 in self.same_locations: ++ assert value1 <= value2, "FAILED: %s <= %s" % (_text(value1), _text(value2)) ++ assert value1 == value2 ++ ++ for locations in [self.ordered_locations1, self.ordered_locations2]: ++ for value1, value2 in zip(locations, locations[1:]): ++ assert value1 <= value2, "FAILED: %s <= %s" % (_text(value1), _text(value2)) ++ assert value1 != value2 ++ ++ def test_compare_greater_or_equal(self): ++ for value1, value2 in self.same_locations: ++ assert value2 >= value1, "FAILED: %s >= %s" % (_text(value2), _text(value1)) ++ assert value2 == value1 ++ ++ for locations in [self.ordered_locations1, self.ordered_locations2]: ++ for value1, value2 in zip(locations, locations[1:]): ++ assert value2 >= value1, "FAILED: %s >= %s" % (_text(value2), _text(value1)) ++ assert value2 != value1 ++ ++ def test_filename_should_be_same_as_self(self): ++ for location in self.ordered_locations2: ++ assert location == location.filename ++ assert location.filename == location ++ ++ def test_string_conversion(self): ++ for location in self.ordered_locations2: ++ expected = u"%s:%s" % (location.filename, location.line) ++ if location.line is None: ++ expected = location.filename ++ assert six.text_type(location) == expected ++ ++ def test_repr_conversion(self): ++ for location in self.ordered_locations2: ++ expected = u'' % \ ++ (location.filename, location.line) ++ actual = repr(location) ++ assert actual == expected, "FAILED: %s == %s" % (actual, expected) +diff --git a/tests/unit/test_parser_gherkin_v6.py b/tests/unit/test_parser_gherkin_v6.py +index 991a57d..43e3d41 100644 +--- a/tests/unit/test_parser_gherkin_v6.py ++++ b/tests/unit/test_parser_gherkin_v6.py +@@ -227,7 +227,9 @@ Feature: With Rule + assert rule1.description == [] + assert rule1.tags == [] + assert len(rule1.scenarios) == 1 +- assert rule1.background is feature.background ++ assert rule1.background is not feature.background ++ assert rule1.background.inherited_steps == feature.background.steps ++ assert list(rule1.background.all_steps) == feature.background.steps + assert_compare_steps(rule1.scenarios[0].all_steps, [ + ("given", "Given", "feature background step 1", None, None), + ("when", "When", "feature background step 2", None, None), +@@ -235,7 +237,7 @@ Feature: With Rule + ("when", "When", "scenario step 2", None, None), + ]) + +- def test_parses_rule_with_background_should_not_inherit_feature_background(self): ++ def test_parses_rule_with_background_inherits_feature_background(self): + """If a Rule has no Background, + it inherits the Feature's Background (if one exists). + """ +@@ -269,13 +271,15 @@ Feature: With Rule + assert rule1.background is not None + assert rule1.background is not feature.background + assert_compare_steps(rule1.scenarios[0].all_steps, [ ++ ("given", "Given", "feature background step 1", None, None), ++ ("when", "When", "feature background step 2", None, None), + ("given", "Given", "rule background step 1", None, None), +- ("when", "When", "rule background step 2", None, None), ++ ("when", "When", "rule background step 2", None, None), + ("given", "Given", "scenario step 1", None, None), +- ("when", "When", "scenario step 2", None, None), ++ ("when", "When", "scenario step 2", None, None), + ]) + +- def test_parses_rule_with_empty_background_prevents_inheriting_feature_background(self): ++ def test_parses_rule_with_empty_background_inherits_feature_background(self): + """A Rule has empty Background (without any steps) prevents that + Feature Background is inherited (if one exists). + """ +@@ -308,8 +312,10 @@ Feature: With Rule + assert rule1.background is not feature.background + assert rule1.background.name == "Rule_R3C.Empty_Background" + assert_compare_steps(rule1.scenarios[0].all_steps, [ ++ ("given", "Given", "feature background step 1", None, None), ++ ("when", "When", "feature background step 2", None, None), + ("given", "Given", "scenario step 1", None, None), +- ("when", "When", "scenario step 2", None, None), ++ ("when", "When", "scenario step 2", None, None), + ]) + + def test_parses_rule_with_scenario(self): +@@ -558,6 +564,7 @@ Feature: With Rule + ("when", "When", 'step uses "2"', None, None), + ]) + ++ # @check.duplicated + def test_parse_background_scenario_and_rules(self): + """HINT: Some Scenarios may exist before the first Rule.""" + text = u''' +@@ -606,10 +613,10 @@ Feature: With Scenarios and Rules + assert scenario1.tags == [] + assert scenario1.description == [] + assert_compare_steps(scenario1.all_steps, [ +- ("given", "Given", 'feature background step_1', None, None), +- ("when", "When", 'feature background step_2', None, None), +- ("given", "Given", 'scenario_1 step_1', None, None), +- ("when", "When", 'scenario_1 step_2', None, None), ++ (u"given", u"Given", u'feature background step_1', None, None), ++ (u"when", u"When", u'feature background step_2', None, None), ++ (u"given", u"Given", u'scenario_1 step_1', None, None), ++ (u"when", u"When", u'scenario_1 step_2', None, None), + ]) + + assert rule1.name == "R1" +@@ -623,9 +630,11 @@ Feature: With Scenarios and Rules + assert rule1_scenario1.parent is rule1 + assert rule1_scenario1.feature is feature + assert_compare_steps(rule1_scenario1.all_steps, [ ++ ("given", "Given", 'feature background step_1', None, None), ++ ("when", "When", 'feature background step_2', None, None), + ("given", "Given", 'rule R1 background step_1', None, None), + ("given", "Given", 'rule R1 scenario_1 step_1', None, None), +- ("when", "When", 'rule R1 scenario_1 step_2', None, None), ++ ("when", "When", 'rule R1 scenario_1 step_2', None, None), + ]) + + assert rule2.name == "R2" +@@ -633,16 +642,318 @@ Feature: With Scenarios and Rules + assert rule2.feature is feature + assert rule2.description == [] + assert rule2.tags == [] +- assert rule2.background is feature.background ++ assert rule2.background is not feature.background ++ assert list(rule2.background.inherited_steps) == list(feature.background.steps) ++ assert list(rule2.background.all_steps) == list(feature.background.steps) + assert len(rule2.scenarios) == 1 + assert rule2_scenario1.name == "R2.Scenario_1" + assert rule2_scenario1.parent is rule2 + assert rule2_scenario1.feature is feature + assert_compare_steps(rule2_scenario1.all_steps, [ + ("given", "Given", 'feature background step_1', None, None), +- ("when", "When", 'feature background step_2', None, None), ++ ("when", "When", 'feature background step_2', None, None), + ("given", "Given", 'rule R2 scenario_1 step_1', None, None), +- ("when", "When", 'rule R2 scenario_1 step_2', None, None), ++ ("when", "When", 'rule R2 scenario_1 step_2', None, None), ++ ]) ++ ++ ++# --------------------------------------------------------------------------- ++# TEST SUITE: Verify Feature Background to Rule Background Inheritance ++# --------------------------------------------------------------------------- ++class TestParser4Background(object): ++ """Verify feature.background to rule.background inheritance, etc.""" ++ ++ def test_parse__norule_scenarios_use_feature_background(self): ++ """AFFECTED: Scenarios outside of rules (before first rule).""" ++ text = u''' ++ Feature: With Scenarios and Rules ++ ++ Background: Feature.Background ++ Given feature background step_1 ++ ++ Scenario: Scenario_1 ++ Given scenario_1 step_1 ++ ++ Rule: R1 ++ Scenario: R1.Scenario_1 ++ Given rule R1 scenario_1 step_1 ++ '''.lstrip() ++ feature = parse_feature(text) ++ assert feature.name == "With Scenarios and Rules" ++ assert feature.background is not None ++ assert len(feature.scenarios) == 1 ++ assert len(feature.rules) == 1 ++ assert len(feature.run_items) == 2 ++ ++ scenario1 = feature.scenarios[0] ++ rule1 = feature.rules[0] ++ assert feature.run_items == [scenario1, rule1] ++ ++ assert scenario1.name == "Scenario_1" ++ assert scenario1.background is feature.background ++ assert scenario1.background_steps == feature.background.steps ++ assert_compare_steps(scenario1.all_steps, [ ++ (u"given", u"Given", u'feature background step_1', None, None), ++ (u"given", u"Given", u'scenario_1 step_1', None, None), ++ ]) ++ ++ def test_parse__norule_scenarios_with_disabled_background(self): ++ """AFFECTED: Scenarios outside of rules (before first rule).""" ++ text = u''' ++ Feature: Scenario with disabled background ++ ++ Background: Feature.Background ++ Given feature background step_1 ++ ++ @fixture.behave.disable_background ++ Scenario: Scenario_1 ++ Given scenario_1 step_1 ++ ++ Scenario: Scenario_2 ++ Given scenario_2 step_1 ++ '''.lstrip() ++ feature = parse_feature(text) ++ assert feature.name == "Scenario with disabled background" ++ assert feature.background is not None ++ assert len(feature.scenarios) == 2 ++ assert len(feature.run_items) == 2 ++ ++ scenario1 = feature.scenarios[0] ++ scenario2 = feature.scenarios[1] ++ assert feature.run_items == [scenario1, scenario2] ++ ++ scenario1.use_background = False # -- FIXTURE-EFFECT (simulated) ++ assert scenario1.name == "Scenario_1" ++ assert scenario1.background is feature.background ++ assert scenario1.background_steps != feature.background.steps ++ assert scenario1.background_steps == [] ++ assert_compare_steps(scenario1.all_steps, [ ++ (u"given", u"Given", u'scenario_1 step_1', None, None), ++ ]) ++ ++ # -- ENSURE: Disabling of background has no effect on other scenarios. ++ assert scenario2.name == "Scenario_2" ++ assert scenario2.background is feature.background ++ assert scenario2.background_steps == feature.background.steps ++ assert_compare_steps(scenario2.all_steps, [ ++ (u"given", u"Given", u'feature background step_1', None, None), ++ (u"given", u"Given", u'scenario_2 step_1', None, None), ++ ]) ++ ++ def test_parse__rule_scenarios_inherit_feature_background_without_rule_background(self): ++ text = u''' ++ Feature: With Background and Rule ++ ++ Background: Feature.Background ++ Given feature background step_1 ++ ++ Rule: R1 ++ Scenario: R1.Scenario_1 ++ Given rule R1 scenario_1 step_1 ++ '''.lstrip() ++ feature = parse_feature(text) ++ assert feature.name == "With Background and Rule" ++ assert feature.background is not None ++ assert len(feature.scenarios) == 0 ++ assert len(feature.rules) == 1 ++ assert len(feature.run_items) == 1 ++ ++ rule1 = feature.rules[0] ++ rule1_scenario1 = rule1.scenarios[0] ++ assert feature.run_items == [rule1] ++ ++ assert rule1_scenario1.name == "R1.Scenario_1" ++ assert rule1_scenario1.background is not None ++ # assert rule1_scenario1.background is not feature.background ++ assert rule1_scenario1.background_steps == feature.background.steps ++ assert_compare_steps(rule1_scenario1.all_steps, [ ++ (u"given", u"Given", u'feature background step_1', None, None), ++ (u"given", u"Given", u'rule R1 scenario_1 step_1', None, None), ++ ]) ++ ++ def test_parse__rule_scenarios_inherit_feature_background_with_rule_background(self): ++ text = u''' ++ Feature: With Feature.Background and Rule.Background ++ ++ Background: Feature.Background ++ Given feature background step_1 ++ ++ Rule: R1 ++ Background: R1.Background ++ Given rule R1 background step_1 ++ ++ Scenario: R1.Scenario_1 ++ Given rule R1 scenario_1 step_1 ++ '''.lstrip() ++ feature = parse_feature(text) ++ assert feature.name == "With Feature.Background and Rule.Background" ++ assert feature.background is not None ++ assert len(feature.scenarios) == 0 ++ assert len(feature.rules) == 1 ++ assert len(feature.run_items) == 1 ++ ++ rule1 = feature.rules[0] ++ rule1_scenario1 = rule1.scenarios[0] ++ assert feature.run_items == [rule1] ++ ++ assert rule1.background is not None ++ assert rule1.background is not feature.background ++ assert rule1.background.inherited_steps == feature.background.steps ++ assert list(rule1.background.all_steps) != feature.background.steps ++ ++ assert rule1_scenario1.name == "R1.Scenario_1" ++ assert rule1_scenario1.background is rule1.background ++ assert rule1_scenario1.background_steps == list(rule1.background.all_steps) ++ assert_compare_steps(rule1_scenario1.all_steps, [ ++ (u"given", u"Given", u'feature background step_1', None, None), ++ (u"given", u"Given", u'rule R1 background step_1', None, None), ++ (u"given", u"Given", u'rule R1 scenario_1 step_1', None, None), ++ ]) ++ ++ def test_parse__rule_scenarios_with_rule_background_when_background_inheritance_is_disabled(self): ++ # -- HINT: Background inheritance is enabled (by default). ++ text = u''' ++ Feature: With Feature Background Inheritance disabled ++ ++ Background: Feature.Background ++ Given feature background step_1 ++ ++ @fixture.behave.override_background ++ Rule: R1 ++ Background: R1.Background ++ Given rule R1 background step_1 ++ ++ Scenario: R1.Scenario_1 ++ Given rule R1 scenario_1 step_1 ++ '''.lstrip() ++ feature = parse_feature(text) ++ assert feature.name == "With Feature Background Inheritance disabled" ++ assert feature.background is not None ++ assert len(feature.scenarios) == 0 ++ assert len(feature.rules) == 1 ++ assert len(feature.run_items) == 1 ++ ++ rule1 = feature.rules[0] ++ rule1_scenario1 = rule1.scenarios[0] ++ assert feature.run_items == [rule1] ++ ++ rule1.use_background_inheritance = False # FIXTURE-EFFECT (simulated) ++ assert rule1.background is not None ++ assert rule1.background.use_inheritance is False ++ assert rule1.background is not feature.background ++ assert rule1.background.inherited_steps == [] ++ assert rule1.background.inherited_steps != feature.background.steps ++ assert list(rule1.background.all_steps) != feature.background.steps ++ ++ assert rule1_scenario1.name == "R1.Scenario_1" ++ assert rule1_scenario1.background is rule1.background ++ assert rule1_scenario1.background_steps == rule1.background.steps ++ assert rule1_scenario1.background_steps == list(rule1.background.all_steps) ++ assert_compare_steps(rule1_scenario1.all_steps, [ ++ (u"given", u"Given", u'rule R1 background step_1', None, None), ++ (u"given", u"Given", u'rule R1 scenario_1 step_1', None, None), ++ ]) ++ ++ def test_parse__rule_scenarios_without_rule_background_when_background_inheritance_is_disabled_without(self): ++ # -- HINT: Background inheritance is enabled (by default). ++ text = u''' ++ Feature: With Feature Background Inheritance disabled ++ ++ Background: Feature.Background ++ Given feature background step_1 ++ ++ @fixture.behave.override_background ++ Rule: R1 ++ Scenario: R1.Scenario_1 ++ Given rule R1 scenario_1 step_1 ++ '''.lstrip() ++ feature = parse_feature(text) ++ assert feature.name == "With Feature Background Inheritance disabled" ++ assert feature.background is not None ++ assert len(feature.scenarios) == 0 ++ assert len(feature.rules) == 1 ++ assert len(feature.run_items) == 1 ++ ++ rule1 = feature.rules[0] ++ rule1_scenario1 = rule1.scenarios[0] ++ assert feature.run_items == [rule1] ++ ++ rule1.use_background_inheritance = False # FIXTURE-EFFECT (simulated) ++ assert rule1.background is not None ++ assert rule1.background.use_inheritance is False ++ assert rule1.background is not feature.background ++ assert rule1.background.inherited_steps == [] ++ ++ assert rule1_scenario1.name == "R1.Scenario_1" ++ assert rule1_scenario1.background is rule1.background ++ assert rule1_scenario1.background_steps == rule1.background.steps ++ assert rule1_scenario1.background_steps == list(rule1.background.all_steps) ++ assert_compare_steps(rule1_scenario1.all_steps, [ ++ (u"given", u"Given", u'rule R1 scenario_1 step_1', None, None), ++ ]) ++ ++ def test_parse__rule_scenarios_without_feature_background_and_with_rule_background(self): ++ text = u''' ++ Feature: Without Feature.Background and with Rule.Background ++ ++ Rule: R1 ++ Background: R1.Background ++ Given rule R1 background step_1 ++ ++ Scenario: R1.Scenario_1 ++ Given rule R1 scenario_1 step_1 ++ '''.lstrip() ++ feature = parse_feature(text) ++ assert feature.name == "Without Feature.Background and with Rule.Background" ++ assert feature.background is None ++ assert len(feature.scenarios) == 0 ++ assert len(feature.rules) == 1 ++ assert len(feature.run_items) == 1 ++ ++ rule1 = feature.rules[0] ++ rule1_scenario1 = rule1.scenarios[0] ++ assert feature.run_items == [rule1] ++ ++ assert rule1.background is not None ++ assert rule1.background is not feature.background ++ assert rule1.background.inherited_steps == [] ++ ++ assert rule1_scenario1.name == "R1.Scenario_1" ++ assert rule1_scenario1.background is rule1.background ++ assert rule1_scenario1.background_steps == rule1.background.steps ++ assert rule1_scenario1.background_steps == list(rule1.background.all_steps) ++ assert_compare_steps(rule1_scenario1.all_steps, [ ++ (u"given", u"Given", u'rule R1 background step_1', None, None), ++ (u"given", u"Given", u'rule R1 scenario_1 step_1', None, None), ++ ]) ++ ++ def test_parse__rule_scenarios_without_feature_and_rule_background(self): ++ text = u''' ++ Feature: Without Feature.Background and Rule.Background ++ ++ Rule: R1 ++ Scenario: R1.Scenario_1 ++ Given rule R1 scenario_1 step_1 ++ '''.lstrip() ++ feature = parse_feature(text) ++ assert feature.name == "Without Feature.Background and Rule.Background" ++ assert feature.background is None ++ assert len(feature.scenarios) == 0 ++ assert len(feature.rules) == 1 ++ assert len(feature.run_items) == 1 ++ ++ rule1 = feature.rules[0] ++ rule1_scenario1 = rule1.scenarios[0] ++ assert feature.run_items == [rule1] ++ ++ assert rule1.background is None ++ ++ assert rule1_scenario1.name == "R1.Scenario_1" ++ assert rule1_scenario1.background is None ++ assert rule1_scenario1.background is rule1.background ++ assert rule1_scenario1.background_steps == [] ++ assert_compare_steps(rule1_scenario1.all_steps, [ ++ (u"given", u"Given", u'rule R1 scenario_1 step_1', None, None), + ]) + + diff --git a/meta-python/recipes-devtools/python/python3-behave/0052-Add-support-for-runtime-constraints.patch b/meta-python/recipes-devtools/python/python3-behave/0052-Add-support-for-runtime-constraints.patch new file mode 100644 index 000000000..964b83e69 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0052-Add-support-for-runtime-constraints.patch @@ -0,0 +1,269 @@ +From 26e431c821c260a981ca8ed28f21330c8ef17af6 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sat, 13 Jul 2019 13:18:02 +0200 +Subject: [PATCH] Add support for runtime constraints. + +--- + .bumpversion.cfg | 2 +- + behave/__init__.py | 2 +- + behave/__main__.py | 13 +++++---- + behave/api/runtime_constraint.py | 45 ++++++++++++++++++++++++++++++++ + behave/configuration.py | 4 --- + behave/exception.py | 40 ++++++++++++++++++++++++++++ + behave/runner.py | 2 +- + behave/runner_util.py | 17 ++---------- + behave/version.py | 2 ++ + tests/unit/test_runner.py | 2 +- + 10 files changed, 101 insertions(+), 28 deletions(-) + create mode 100644 behave/api/runtime_constraint.py + create mode 100644 behave/exception.py + create mode 100644 behave/version.py + +diff --git a/.bumpversion.cfg b/.bumpversion.cfg +index ac913c2..a5d3d2f 100644 +--- a/.bumpversion.cfg ++++ b/.bumpversion.cfg +@@ -1,6 +1,6 @@ + [bumpversion] + current_version = 1.2.7.dev1 +-files = behave/__init__.py setup.py VERSION.txt pytest.ini .bumpversion.cfg ++files = behave/version.py setup.py VERSION.txt pytest.ini .bumpversion.cfg + parse = (?P\d+)\.(?P\d+)\.(?P\d+)(?P\w*) + serialize = {major}.{minor}.{patch}{drop} + commit = False +diff --git a/behave/__init__.py b/behave/__init__.py +index 31e4e55..53a5337 100644 +--- a/behave/__init__.py ++++ b/behave/__init__.py +@@ -20,6 +20,7 @@ from __future__ import absolute_import + from behave.step_registry import * # pylint: disable=wildcard-import + from behave.matchers import use_step_matcher, step_matcher, register_type + from behave.fixture import fixture, use_fixture ++from behave.version import VERSION as __version__ + + # pylint: disable=undefined-all-variable + __all__ = [ +@@ -29,4 +30,3 @@ __all__ = [ + # -- DEPRECATING: + "step_matcher" + ] +-__version__ = "1.2.7.dev1" +diff --git a/behave/__main__.py b/behave/__main__.py +index c340b25..3cae36d 100644 +--- a/behave/__main__.py ++++ b/behave/__main__.py +@@ -4,12 +4,13 @@ from __future__ import absolute_import, print_function + import codecs + import sys + import six +-from behave import __version__ +-from behave.configuration import Configuration, ConfigError ++from behave.version import VERSION as BEHAVE_VERSION ++from behave.configuration import Configuration ++from behave.exception import ConstraintError, ConfigError, \ ++ FileNotFoundError, InvalidFileLocationError, InvalidFilenameError + from behave.parser import ParserError + from behave.runner import Runner +-from behave.runner_util import print_undefined_step_snippets, reset_runtime, \ +- InvalidFileLocationError, InvalidFilenameError, FileNotFoundError ++from behave.runner_util import print_undefined_step_snippets, reset_runtime + from behave.textutil import compute_words_maxsize, text as _text + + +@@ -62,7 +63,7 @@ def run_behave(config, runner_class=None): + runner_class = Runner + + if config.version: +- print("behave " + __version__) ++ print("behave " + BEHAVE_VERSION) + return 0 + + if config.tags_help: +@@ -110,6 +111,8 @@ def run_behave(config, runner_class=None): + print(u"InvalidFileLocationError: %s" % e) + except InvalidFilenameError as e: + print(u"InvalidFilenameError: %s" % e) ++ except ConstraintError as e: ++ print(u"ConstraintError: %s" % e) + except Exception as e: + # -- DIAGNOSTICS: + text = _text(e) +diff --git a/behave/api/runtime_constraint.py b/behave/api/runtime_constraint.py +new file mode 100644 +index 0000000..310e529 +--- /dev/null ++++ b/behave/api/runtime_constraint.py +@@ -0,0 +1,45 @@ ++# -*- coding: UTF-8 -*- ++""" ++Simplifies to specify runtime constraints in ++ ++* features/environment.py file ++* features/steps/*.py" files ++""" ++ ++from __future__ import absolute_import ++from behave.exception import ConstraintError ++ ++ ++# --------------------------------------------------------------------------- ++# UTILITY FUNCTIONS: ++# --------------------------------------------------------------------------- ++def require_min_python_version(minimal_version): ++ """Simplifies to specify the minimal python version that is required. ++ ++ :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] ++ elif not isinstance(minimal_version, tuple): ++ raise TypeError("string or tuple (was: %s)" % type(minimal_version)) ++ ++ if python_version < minimal_version: ++ raise ConstraintError("python >= %s expected (was: %s)" % \ ++ (minimal_version, python_version)) ++ ++ ++def require_min_behave_version(minimal_version): ++ """Simplifies to specify the minimal behave version that is required. ++ ++ :param minimal_version: Minimum version (as string, tuple) ++ :raises: behave.exception.ConstraintError ++ """ ++ # -- SIMPLISTIC IMPLEMENTATION: ++ from behave.version import VERSION as behave_version ++ if behave_version < minimal_version: ++ raise ConstraintError("behave >= %s expected (was: %s)" % \ ++ (minimal_version, behave_version)) +diff --git a/behave/configuration.py b/behave/configuration.py +index 861f89f..bd8b039 100644 +--- a/behave/configuration.py ++++ b/behave/configuration.py +@@ -62,10 +62,6 @@ class LogLevel(object): + return logging.getLevelName(level) + + +-class ConfigError(Exception): +- pass +- +- + # ----------------------------------------------------------------------------- + # CONFIGURATION SCHEMA: + # ----------------------------------------------------------------------------- +diff --git a/behave/exception.py b/behave/exception.py +new file mode 100644 +index 0000000..ba21206 +--- /dev/null ++++ b/behave/exception.py +@@ -0,0 +1,40 @@ ++# -*- coding: UTF-8 -*- ++""" ++Behave exception classes. ++ ++.. versionadded:: 1.2.7 ++""" ++ ++ ++# --------------------------------------------------------------------------- ++# EXCEPTION/ERROR CLASSES: ++# --------------------------------------------------------------------------- ++class ConstraintError(RuntimeError): ++ """Used if a constraint/precondition is not fulfilled at runtime. ++ ++ .. versionadded:: 1.2.7 ++ """ ++ ++ ++class ConfigError(Exception): ++ """Used if the configuration is (partially) invalid.""" ++ ++ ++# --------------------------------------------------------------------------- ++# EXCEPTION/ERROR CLASSES: Related to File Handling ++# --------------------------------------------------------------------------- ++class FileNotFoundError(LookupError): ++ """Used if a specified file was not found.""" ++ ++ ++class InvalidFileLocationError(LookupError): ++ """Used if a :class:`behave.model_core.FileLocation` is invalid. ++ This occurs if the file location is no exactly correct and ++ strict checking is enabled. ++ """ ++ ++ ++class InvalidFilenameError(ValueError): ++ """Used if a filename does not have the expected file extension, etc.""" ++ ++ +diff --git a/behave/runner.py b/behave/runner.py +index f209cb0..cbedb5a 100644 +--- a/behave/runner.py ++++ b/behave/runner.py +@@ -15,7 +15,7 @@ import six + + from behave._types import ExceptionUtil + from behave.capture import CaptureController +-from behave.configuration import ConfigError ++from behave.exception import ConfigError + from behave.formatter._registry import make_formatters + from behave.runner_util import \ + collect_feature_locations, parse_features, \ +diff --git a/behave/runner_util.py b/behave/runner_util.py +index 7e0807f..80b99a0 100644 +--- a/behave/runner_util.py ++++ b/behave/runner_util.py +@@ -11,26 +11,13 @@ import re + import sys + from six import string_types + from behave import parser ++from behave.exception import \ ++ FileNotFoundError, InvalidFileLocationError, InvalidFilenameError + from behave.model_core import FileLocation + from behave.textutil import ensure_stream_with_encoder + # LAZY: from behave.step_registry import setup_step_decorators + + +-# ----------------------------------------------------------------------------- +-# EXCEPTIONS: +-# ----------------------------------------------------------------------------- +-class FileNotFoundError(LookupError): +- pass +- +- +-class InvalidFileLocationError(LookupError): +- pass +- +- +-class InvalidFilenameError(ValueError): +- pass +- +- + # ----------------------------------------------------------------------------- + # CLASS: FileLocationParser + # ----------------------------------------------------------------------------- +diff --git a/behave/version.py b/behave/version.py +new file mode 100644 +index 0000000..b19cb5e +--- /dev/null ++++ b/behave/version.py +@@ -0,0 +1,2 @@ ++# -- BEHAVE-VERSION: ++VERSION = "1.2.7.dev1" +diff --git a/tests/unit/test_runner.py b/tests/unit/test_runner.py +index 030dffa..f0d03cd 100644 +--- a/tests/unit/test_runner.py ++++ b/tests/unit/test_runner.py +@@ -17,7 +17,7 @@ from behave import runner_util + from behave.model import Table + from behave.step_registry import StepRegistry + from behave import parser, runner +-from behave.configuration import ConfigError ++from behave.exception import ConfigError + from behave.formatter.base import StreamOpener + + diff --git a/meta-python/recipes-devtools/python/python3-behave/0053-Use-runtime-constraints.patch b/meta-python/recipes-devtools/python/python3-behave/0053-Use-runtime-constraints.patch new file mode 100644 index 000000000..8a07505bf --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0053-Use-runtime-constraints.patch @@ -0,0 +1,196 @@ +From 5917914e415c728e130d828cc1ed4e2199b1fcdc Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sat, 13 Jul 2019 13:19:13 +0200 +Subject: [PATCH] Use runtime constraints + +--- + .../features/async_dispatch.feature | 2 ++ + .../async_step/features/async_run.feature | 2 ++ + examples/async_step/features/environment.py | 13 +++++++++ + .../{async_steps34.py => _async_steps34.py} | 6 +++- + .../{async_steps35.py => _async_steps35.py} | 3 +- + .../features/steps/async_dispatch_steps.py | 29 +++++++++++++++---- + .../async_step/features/steps/async_steps.py | 12 ++++++++ + 7 files changed, 59 insertions(+), 8 deletions(-) + rename examples/async_step/features/steps/{async_steps34.py => _async_steps34.py} (58%) + rename examples/async_step/features/steps/{async_steps35.py => _async_steps35.py} (95%) + create mode 100644 examples/async_step/features/steps/async_steps.py + +diff --git a/examples/async_step/features/async_dispatch.feature b/examples/async_step/features/async_dispatch.feature +index 416d3d1..18e9869 100644 +--- a/examples/async_step/features/async_dispatch.feature ++++ b/examples/async_step/features/async_dispatch.feature +@@ -1,6 +1,8 @@ + @use.with_python.version=3.4 + @use.with_python.version=3.5 + @use.with_python.version=3.6 ++@use.with_python.version=3.7 ++@use.with_python.version=3.8 + Feature: + Scenario: + Given I dispatch an async-call with param "Alice" +diff --git a/examples/async_step/features/async_run.feature b/examples/async_step/features/async_run.feature +index 9f506b4..29b8fa7 100644 +--- a/examples/async_step/features/async_run.feature ++++ b/examples/async_step/features/async_run.feature +@@ -1,6 +1,8 @@ + @use.with_python.version=3.4 + @use.with_python.version=3.5 + @use.with_python.version=3.6 ++@use.with_python.version=3.7 ++@use.with_python.version=3.8 + Feature: + Scenario: + Given an async-step waits 0.3 seconds +diff --git a/examples/async_step/features/environment.py b/examples/async_step/features/environment.py +index 9d4302b..02c4d92 100644 +--- a/examples/async_step/features/environment.py ++++ b/examples/async_step/features/environment.py +@@ -1,8 +1,18 @@ + # -*- coding: UTF-8 -*- + + from behave.tag_matcher import ActiveTagMatcher, setup_active_tag_values ++from behave.api.runtime_constraint import require_min_python_version + import sys + ++# ----------------------------------------------------------------------------- ++# REQUIRE: python >= 3.4 ++# ----------------------------------------------------------------------------- ++require_min_python_version("3.4") ++ ++ ++# ----------------------------------------------------------------------------- ++# SUPPORT: Active-tags ++# ----------------------------------------------------------------------------- + # -- MATCHES ANY TAGS: @use.with_{category}={value} + # NOTE: active_tag_value_provider provides category values for active tags. + python_version = "%s.%s" % sys.version_info[:2] +@@ -11,6 +21,7 @@ active_tag_value_provider = { + } + active_tag_matcher = ActiveTagMatcher(active_tag_value_provider) + ++ + # ----------------------------------------------------------------------------- + # HOOKS: + # ----------------------------------------------------------------------------- +@@ -18,10 +29,12 @@ def before_all(context): + # -- SETUP ACTIVE-TAG MATCHER (with userdata): + setup_active_tag_values(active_tag_value_provider, context.config.userdata) + ++ + def before_feature(context, feature): + if active_tag_matcher.should_exclude_with(feature.tags): + feature.skip(reason=active_tag_matcher.exclude_reason) + ++ + def before_scenario(context, scenario): + if active_tag_matcher.should_exclude_with(scenario.effective_tags): + scenario.skip(reason=active_tag_matcher.exclude_reason) +diff --git a/examples/async_step/features/steps/async_steps34.py b/examples/async_step/features/steps/_async_steps34.py +similarity index 58% +rename from examples/async_step/features/steps/async_steps34.py +rename to examples/async_step/features/steps/_async_steps34.py +index c4962ab..556500f 100644 +--- a/examples/async_step/features/steps/async_steps34.py ++++ b/examples/async_step/features/steps/_async_steps34.py +@@ -1,8 +1,12 @@ +-# -- REQUIRES: Python >= 3.4 ++# -- REQUIRES: Python >= 3.4 and Python < 3.8 ++# HINT: Decorator @asyncio.coroutine is prohibited in python 3.8 ++# USE: Async generator/coroutine instead. ++ + from behave import step + from behave.api.async_step import async_run_until_complete + import asyncio + ++# -- USABLE FOR: "3.4" <= python_version < "3.8" + @step('an async-step waits {duration:f} seconds') + @async_run_until_complete + @asyncio.coroutine +diff --git a/examples/async_step/features/steps/async_steps35.py b/examples/async_step/features/steps/_async_steps35.py +similarity index 95% +rename from examples/async_step/features/steps/async_steps35.py +rename to examples/async_step/features/steps/_async_steps35.py +index 018d5ef..edcbe0e 100644 +--- a/examples/async_step/features/steps/async_steps35.py ++++ b/examples/async_step/features/steps/_async_steps35.py +@@ -1,4 +1,5 @@ + # -- REQUIRES: Python >= 3.5 ++ + from behave import step + from behave.api.async_step import async_run_until_complete + import asyncio +@@ -6,5 +7,5 @@ import asyncio + @step('an async-step waits {duration:f} seconds') + @async_run_until_complete + async def step_async_step_waits_seconds_py35(context, duration): +- """Simple example of a coroutine as async-step (in Python 3.5)""" ++ """Simple example of a coroutine as async-step (in Python 3.5 or newer)""" + await asyncio.sleep(duration) +diff --git a/examples/async_step/features/steps/async_dispatch_steps.py b/examples/async_step/features/steps/async_dispatch_steps.py +index b9b6e15..222e54d 100644 +--- a/examples/async_step/features/steps/async_dispatch_steps.py ++++ b/examples/async_step/features/steps/async_dispatch_steps.py +@@ -1,21 +1,38 @@ + # -*- coding: UTF-8 -*- +-# REQUIRES: Python >= 3.5 ++# REQUIRES: Python >= 3.4/3.5 ++import sys + from behave import given, then, step +-from behave.api.async_step import use_or_create_async_context, AsyncContext ++from behave.api.async_step import use_or_create_async_context + from hamcrest import assert_that, equal_to, empty + import asyncio + +-@asyncio.coroutine +-def async_func(param): +- yield from asyncio.sleep(0.2) +- return str(param).upper() + ++# --------------------------------------------------------------------------- ++# ASYNC EXAMPLE FUNCTION: ++# --------------------------------------------------------------------------- ++python_version = "%s.%s" % sys.version_info[:2] ++if python_version >= "3.5": ++ async def async_func(param): ++ await asyncio.sleep(0.2) ++ return str(param).upper() ++else: ++ # -- HINT: Decorator @asyncio.coroutine is prohibited in python 3.8 ++ @asyncio.coroutine ++ def async_func(param): ++ yield from asyncio.sleep(0.2) ++ return str(param).upper() ++ ++ ++# --------------------------------------------------------------------------- ++# STEPS: ++# --------------------------------------------------------------------------- + @given('I dispatch an async-call with param "{param}"') + def step_dispatch_async_call(context, param): + async_context = use_or_create_async_context(context, "async_context1") + task = async_context.loop.create_task(async_func(param)) + async_context.tasks.append(task) + ++ + @then('the collected result of the async-calls is "{expected}"') + def step_collected_async_call_result_is(context, expected): + async_context = context.async_context1 +diff --git a/examples/async_step/features/steps/async_steps.py b/examples/async_step/features/steps/async_steps.py +new file mode 100644 +index 0000000..dc03c72 +--- /dev/null ++++ b/examples/async_step/features/steps/async_steps.py +@@ -0,0 +1,12 @@ ++# -*- coding: UTF-8 -*- ++# REQUIRES: Python >= 3.4/3.5 ++"""Python import-barrier for python2 or python < 3.4.""" ++ ++from __future__ import absolute_import ++import sys ++ ++python_version = "%s.%s" % sys.version_info[:2] ++if python_version >= "3.5": ++ import _async_steps35 ++elif python_version == "3.4": ++ import _async_steps34 diff --git a/meta-python/recipes-devtools/python/python3-behave/0054-CLEANUP-Remove-deprecated-parts.patch b/meta-python/recipes-devtools/python/python3-behave/0054-CLEANUP-Remove-deprecated-parts.patch new file mode 100644 index 000000000..aebb68589 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0054-CLEANUP-Remove-deprecated-parts.patch @@ -0,0 +1,3937 @@ +From 484af9f2be3868f90ae736c040332dcdc04b5d02 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sat, 13 Jul 2019 13:20:38 +0200 +Subject: [PATCH] CLEANUP: Remove deprecated parts. + +--- + .attic/convert_i18n_yaml.py | 77 + + .attic/i18n.yml | 635 +++++++ + bin/gherkin-languages.json | 3193 ----------------------------------- + 3 files changed, 712 insertions(+), 3193 deletions(-) + create mode 100755 .attic/convert_i18n_yaml.py + create mode 100644 .attic/i18n.yml + delete mode 100644 bin/gherkin-languages.json + +diff --git a/.attic/convert_i18n_yaml.py b/.attic/convert_i18n_yaml.py +new file mode 100755 +index 0000000..d6a6713 +--- /dev/null ++++ b/.attic/convert_i18n_yaml.py +@@ -0,0 +1,77 @@ ++#!/usr/bin/env python ++# -*- coding: UTF-8 -*- ++# USAGE: convert_i18n_yaml.py [--data=i18n.yml] behave/i18n.py ++""" ++Generates I18N python module based on YAML description (i18n.yml). ++ ++REQUIRES: ++ * argparse ++ * six ++ * PyYAML ++""" ++ ++from __future__ import absolute_import, print_function ++import argparse ++import os.path ++import six ++import sys ++import pprint ++import yaml ++ ++HERE = os.path.dirname(__file__) ++NAME = os.path.basename(__file__) ++__version__ = "1.0" ++ ++def yaml_normalize(data): ++ for part in data: ++ keywords = data[part] ++ for k in keywords: ++ v = keywords[k] ++ # bloody YAML parser returns a mixture of unicode and str ++ if not isinstance(v, six.text_type): ++ v = v.decode("UTF-8") ++ keywords[k] = v.split("|") ++ return data ++ ++def main(args=None): ++ if args is None: ++ args = sys.argv[1:] ++ parser = argparse.ArgumentParser(prog=NAME, ++ description="Generate python module i18n from YAML based data") ++ parser.add_argument("-d", "--data", dest="yaml_file", ++ default=os.path.join(HERE, "i18n.yml"), ++ help="Path to i18n.yml file (YAML file).") ++ parser.add_argument("output_file", default="stdout", ++ help="Filename of Python I18N module (as output).") ++ parser.add_argument("--version", action="version", version=__version__) ++ ++ options = parser.parse_args(args) ++ if not os.path.isfile(options.yaml_file): ++ parser.error("YAML file not found: %s" % options.yaml_file) ++ ++ # -- STEP 1: Load YAML data. ++ languages = yaml.load(open(options.yaml_file)) ++ languages = yaml_normalize(languages) ++ ++ # -- STEP 2: Generate python module with i18n data. ++ contents = u"""# -*- coding: UTF-8 -*- ++# -- FILE GENERATED BY: convert_i18n_yaml.py with i18n.yml ++# pylint: disable=line-too-long ++ ++languages = \\ ++""" ++ if options.output_file in ("-", "stdout"): ++ i18n_py = sys.stdout ++ should_close = False ++ else: ++ i18n_py = open(options.output_file, "w") ++ should_close = True ++ i18n_py.write(contents.encode("UTF-8")) ++ i18n_py.write(pprint.pformat(languages).encode("UTF-8")) ++ i18n_py.write(u"\n") ++ if should_close: ++ i18n_py.close() ++ return 0 ++ ++if __name__ == "__main__": ++ sys.exit(main()) +diff --git a/.attic/i18n.yml b/.attic/i18n.yml +new file mode 100644 +index 0000000..82345a4 +--- /dev/null ++++ b/.attic/i18n.yml +@@ -0,0 +1,635 @@ ++# encoding: UTF-8 ++# ++# We use ISO 639-1 (language) and ISO 3166 alpha-2 (region - if applicable): ++# http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes ++# http://en.wikipedia.org/wiki/ISO_3166-1 ++# ++# If you want several aliases for a keyword, just separate them ++# with a | character. The * is a step keyword alias for all translations. ++# ++# If you do *not* want a trailing space after a keyword, end it with a < character. ++# (See Chinese for examples). ++# ++# This file copyright (c) 2009-2011 Mike Sassak, Gregory Hnatiuk, Aslak Hellesøy ++# ++# Permission is hereby granted, free of charge, to any person obtaining ++# a copy of this software and associated documentation files (the ++# "Software"), to deal in the Software without restriction, including ++# without limitation the rights to use, copy, modify, merge, publish, ++# distribute, sublicense, and/or sell copies of the Software, and to ++# permit persons to whom the Software is furnished to do so, subject to ++# the following conditions: ++# ++# The above copyright notice and this permission notice shall be ++# included in all copies or substantial portions of the Software. ++# ++# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, ++# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF ++# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND ++# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE ++# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION ++# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION ++# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ++ ++"en": ++ name: English ++ native: English ++ feature: Feature ++ background: Background ++ scenario: Scenario ++ scenario_outline: Scenario Outline|Scenario Template ++ examples: Examples|Scenarios ++ given: "*|Given" ++ when: "*|When" ++ then: "*|Then" ++ and: "*|And" ++ but: "*|But" ++ ++# Please keep the grammars in alphabetical order by name from here and down. ++ ++"ar": ++ name: Arabic ++ native: العربية ++ feature: خاصية ++ background: الخلفية ++ scenario: سيناريو ++ scenario_outline: سيناريو مخطط ++ examples: امثلة ++ given: "*|بفرض" ++ when: "*|متى|عندما" ++ then: "*|اذاً|ثم" ++ and: "*|و" ++ but: "*|لكن" ++"bg": ++ name: Bulgarian ++ native: български ++ feature: Функционалност ++ background: Предистория ++ scenario: Сценарий ++ scenario_outline: Рамка на сценарий ++ examples: Примери ++ given: "*|Дадено" ++ when: "*|Когато" ++ then: "*|То" ++ and: "*|И" ++ but: "*|Но" ++"ca": ++ name: Catalan ++ native: català ++ background: Rerefons|Antecedents ++ feature: Característica|Funcionalitat ++ scenario: Escenari ++ scenario_outline: Esquema de l'escenari ++ examples: Exemples ++ given: "*|Donat|Donada|Atès|Atesa" ++ when: "*|Quan" ++ then: "*|Aleshores|Cal" ++ and: "*|I" ++ but: "*|Però" ++"cy-GB": ++ name: Welsh ++ native: Cymraeg ++ background: Cefndir ++ feature: Arwedd ++ scenario: Scenario ++ scenario_outline: Scenario Amlinellol ++ examples: Enghreifftiau ++ given: "*|Anrhegedig a" ++ when: "*|Pryd" ++ then: "*|Yna" ++ and: "*|A" ++ but: "*|Ond" ++"cs": ++ name: Czech ++ native: Česky ++ feature: Požadavek ++ background: Pozadí|Kontext ++ scenario: Scénář ++ scenario_outline: Náčrt Scénáře|Osnova scénáře ++ examples: Příklady ++ given: "*|Pokud|Za předpokladu" ++ when: "*|Když" ++ then: "*|Pak" ++ and: "*|A|A také" ++ but: "*|Ale" ++"da": ++ name: Danish ++ native: dansk ++ feature: Egenskab ++ background: Baggrund ++ scenario: Scenarie ++ scenario_outline: Abstrakt Scenario ++ examples: Eksempler ++ given: "*|Givet" ++ when: "*|Når" ++ then: "*|Så" ++ and: "*|Og" ++ but: "*|Men" ++"de": ++ name: German ++ native: Deutsch ++ feature: Funktionalität ++ background: Grundlage ++ scenario: Szenario ++ scenario_outline: Szenariogrundriss ++ examples: Beispiele ++ given: "*|Angenommen|Gegeben sei" ++ when: "*|Wenn" ++ then: "*|Dann" ++ and: "*|Und" ++ but: "*|Aber" ++"en-au": ++ name: Australian ++ native: Australian ++ feature: Crikey ++ background: Background ++ scenario: Mate ++ scenario_outline: Blokes ++ examples: Cobber ++ given: "*|Ya know how" ++ when: "*|When" ++ then: "*|Ya gotta" ++ and: "*|N" ++ but: "*|Cept" ++"en-lol": ++ name: LOLCAT ++ native: LOLCAT ++ feature: OH HAI ++ background: B4 ++ scenario: MISHUN ++ scenario_outline: MISHUN SRSLY ++ examples: EXAMPLZ ++ given: "*|I CAN HAZ" ++ when: "*|WEN" ++ then: "*|DEN" ++ and: "*|AN" ++ but: "*|BUT" ++"en-pirate": ++ name: Pirate ++ native: Pirate ++ feature: Ahoy matey! ++ background: Yo-ho-ho ++ scenario: Heave to ++ scenario_outline: Shiver me timbers ++ examples: Dead men tell no tales ++ given: "*|Gangway!" ++ when: "*|Blimey!" ++ then: "*|Let go and haul" ++ and: "*|Aye" ++ but: "*|Avast!" ++"en-Scouse": ++ name: Scouse ++ native: Scouse ++ feature: Feature ++ background: "Dis is what went down" ++ scenario: "The thing of it is" ++ scenario_outline: "Wharrimean is" ++ examples: Examples ++ given: "*|Givun|Youse know when youse got" ++ when: "*|Wun|Youse know like when" ++ then: "*|Dun|Den youse gotta" ++ and: "*|An" ++ but: "*|Buh" ++"en-tx": ++ name: Texan ++ native: Texan ++ feature: Feature ++ background: Background ++ scenario: Scenario ++ scenario_outline: All y'all ++ examples: Examples ++ given: "*|Given y'all" ++ when: "*|When y'all" ++ then: "*|Then y'all" ++ and: "*|And y'all" ++ but: "*|But y'all" ++"eo": ++ name: Esperanto ++ native: Esperanto ++ feature: Trajto ++ background: Fono ++ scenario: Scenaro ++ scenario_outline: Konturo de la scenaro ++ examples: Ekzemploj ++ given: "*|Donitaĵo" ++ when: "*|Se" ++ then: "*|Do" ++ and: "*|Kaj" ++ but: "*|Sed" ++"es": ++ name: Spanish ++ native: español ++ background: Antecedentes ++ feature: Característica ++ scenario: Escenario ++ scenario_outline: Esquema del escenario ++ examples: Ejemplos ++ given: "*|Dado|Dada|Dados|Dadas" ++ when: "*|Cuando" ++ then: "*|Entonces" ++ and: "*|Y" ++ but: "*|Pero" ++"et": ++ name: Estonian ++ native: eesti keel ++ feature: Omadus ++ background: Taust ++ scenario: Stsenaarium ++ scenario_outline: Raamstsenaarium ++ examples: Juhtumid ++ given: "*|Eeldades" ++ when: "*|Kui" ++ then: "*|Siis" ++ and: "*|Ja" ++ but: "*|Kuid" ++"fi": ++ name: Finnish ++ native: suomi ++ feature: Ominaisuus ++ background: Tausta ++ scenario: Tapaus ++ scenario_outline: Tapausaihio ++ examples: Tapaukset ++ given: "*|Oletetaan" ++ when: "*|Kun" ++ then: "*|Niin" ++ and: "*|Ja" ++ but: "*|Mutta" ++"fr": ++ name: French ++ native: français ++ feature: Fonctionnalité ++ background: Contexte ++ scenario: Scénario ++ scenario_outline: Plan du scénario|Plan du Scénario ++ examples: Exemples ++ given: "*|Soit|Etant donné|Etant donnée|Etant donnés|Etant données|Étant donné|Étant donnée|Étant donnés|Étant données" ++ when: "*|Quand|Lorsque|Lorsqu'<" ++ then: "*|Alors" ++ and: "*|Et" ++ but: "*|Mais" ++"gl": ++ name: Galician ++ native: galego ++ feature: Característica ++ background: Contexto ++ scenario: Escenario ++ scenario_outline: "Esbozo do escenario" ++ examples: Exemplos ++ given: "*|Dado|Dada|Dados|Dadas" ++ when: "*|Cando" ++ then: "*|Entón|Logo" ++ and: "*|E" ++ but: "*|Mais|Pero" ++ ++"he": ++ name: Hebrew ++ native: עברית ++ feature: תכונה ++ background: רקע ++ scenario: תרחיש ++ scenario_outline: תבנית תרחיש ++ examples: דוגמאות ++ given: "*|בהינתן" ++ when: "*|כאשר" ++ then: "*|אז|אזי" ++ and: "*|וגם" ++ but: "*|אבל" ++"hr": ++ name: Croatian ++ native: hrvatski ++ feature: Osobina|Mogućnost|Mogucnost ++ background: Pozadina ++ scenario: Scenarij ++ scenario_outline: Skica|Koncept ++ examples: Primjeri|Scenariji ++ given: "*|Zadan|Zadani|Zadano" ++ when: "*|Kada|Kad" ++ then: "*|Onda" ++ and: "*|I" ++ but: "*|Ali" ++"hu": ++ name: Hungarian ++ native: magyar ++ feature: Jellemző ++ background: Háttér ++ scenario: Forgatókönyv ++ scenario_outline: Forgatókönyv vázlat ++ examples: Példák ++ given: "*|Amennyiben|Adott" ++ when: "*|Majd|Ha|Amikor" ++ then: "*|Akkor" ++ and: "*|És" ++ but: "*|De" ++"id": ++ name: Indonesian ++ native: Bahasa Indonesia ++ feature: Fitur ++ background: Dasar ++ scenario: Skenario ++ scenario_outline: Skenario konsep ++ examples: Contoh ++ given: "*|Dengan" ++ when: "*|Ketika" ++ then: "*|Maka" ++ and: "*|Dan" ++ but: "*|Tapi" ++"is": ++ name: Icelandic ++ native: Íslenska ++ feature: Eiginleiki ++ background: Bakgrunnur ++ scenario: Atburðarás ++ scenario_outline: Lýsing Atburðarásar|Lýsing Dæma ++ examples: Dæmi|Atburðarásir ++ given: "*|Ef" ++ when: "*|Þegar" ++ then: "*|Þá" ++ and: "*|Og" ++ but: "*|En" ++"it": ++ name: Italian ++ native: italiano ++ feature: Funzionalità ++ background: Contesto ++ scenario: Scenario ++ scenario_outline: Schema dello scenario ++ examples: Esempi ++ given: "*|Dato|Data|Dati|Date" ++ when: "*|Quando" ++ then: "*|Allora" ++ and: "*|E" ++ but: "*|Ma" ++"ja": ++ name: Japanese ++ native: 日本語 ++ feature: フィーチャ|機能 ++ background: 背景 ++ scenario: シナリオ ++ scenario_outline: シナリオアウトライン|シナリオテンプレート|テンプレ|シナリオテンプレ ++ examples: 例|サンプル ++ given: "*|前提<" ++ when: "*|もし<" ++ then: "*|ならば<" ++ and: "*|かつ<" ++ but: "*|しかし<|但し<|ただし<" ++"ko": ++ name: Korean ++ native: 한국어 ++ background: 배경 ++ feature: 기능 ++ scenario: 시나리오 ++ scenario_outline: 시나리오 개요 ++ examples: 예 ++ given: "*|조건<|먼저<" ++ when: "*|만일<|만약<" ++ then: "*|그러면<" ++ and: "*|그리고<" ++ but: "*|하지만<|단<" ++"lt": ++ name: Lithuanian ++ native: lietuvių kalba ++ feature: Savybė ++ background: Kontekstas ++ scenario: Scenarijus ++ scenario_outline: Scenarijaus šablonas ++ examples: Pavyzdžiai|Scenarijai|Variantai ++ given: "*|Duota" ++ when: "*|Kai" ++ then: "*|Tada" ++ and: "*|Ir" ++ but: "*|Bet" ++"lu": ++ name: Luxemburgish ++ native: Lëtzebuergesch ++ feature: Funktionalitéit ++ background: Hannergrond ++ scenario: Szenario ++ scenario_outline: Plang vum Szenario ++ examples: Beispiller ++ given: "*|ugeholl" ++ when: "*|wann" ++ then: "*|dann" ++ and: "*|an|a" ++ but: "*|awer|mä" ++"lv": ++ name: Latvian ++ native: latviešu ++ feature: Funkcionalitāte|Fīča ++ background: Konteksts|Situācija ++ scenario: Scenārijs ++ scenario_outline: Scenārijs pēc parauga ++ examples: Piemēri|Paraugs ++ given: "*|Kad" ++ when: "*|Ja" ++ then: "*|Tad" ++ and: "*|Un" ++ but: "*|Bet" ++"nl": ++ name: Dutch ++ native: Nederlands ++ feature: Functionaliteit ++ background: Achtergrond ++ scenario: Scenario ++ scenario_outline: Abstract Scenario ++ examples: Voorbeelden ++ given: "*|Gegeven|Stel" ++ when: "*|Als" ++ then: "*|Dan" ++ and: "*|En" ++ but: "*|Maar" ++"no": ++ name: Norwegian ++ native: norsk ++ feature: Egenskap ++ background: Bakgrunn ++ scenario: Scenario ++ scenario_outline: Scenariomal|Abstrakt Scenario ++ examples: Eksempler ++ given: "*|Gitt" ++ when: "*|Når" ++ then: "*|Så" ++ and: "*|Og" ++ but: "*|Men" ++"pl": ++ name: Polish ++ native: polski ++ feature: Właściwość ++ background: Założenia ++ scenario: Scenariusz ++ scenario_outline: Szablon scenariusza ++ examples: Przykłady ++ given: "*|Zakładając|Mając" ++ when: "*|Jeżeli|Jeśli" ++ then: "*|Wtedy" ++ and: "*|Oraz|I" ++ but: "*|Ale" ++"pt": ++ name: Portuguese ++ native: português ++ background: Contexto ++ feature: Funcionalidade ++ scenario: Cenário|Cenario ++ scenario_outline: Esquema do Cenário|Esquema do Cenario ++ examples: Exemplos ++ given: "*|Dado|Dada|Dados|Dadas" ++ when: "*|Quando" ++ then: "*|Então|Entao" ++ and: "*|E" ++ but: "*|Mas" ++"ro": ++ name: Romanian ++ native: română ++ background: Context ++ feature: Functionalitate|Funcționalitate|Funcţionalitate ++ scenario: Scenariu ++ scenario_outline: Structura scenariu|Structură scenariu ++ examples: Exemple ++ given: "*|Date fiind|Dat fiind|Dati fiind|Dați fiind|Daţi fiind" ++ when: "*|Cand|Când" ++ then: "*|Atunci" ++ and: "*|Si|Și|Şi" ++ but: "*|Dar" ++"ru": ++ name: Russian ++ native: русский ++ feature: Функция|Функционал|Свойство ++ background: Предыстория|Контекст ++ scenario: Сценарий ++ scenario_outline: Структура сценария ++ examples: Примеры ++ given: "*|Допустим|Дано|Пусть" ++ when: "*|Если|Когда" ++ then: "*|То|Тогда" ++ and: "*|И|К тому же" ++ but: "*|Но|А" ++"sv": ++ name: Swedish ++ native: Svenska ++ feature: Egenskap ++ background: Bakgrund ++ scenario: Scenario ++ scenario_outline: Abstrakt Scenario|Scenariomall ++ examples: Exempel ++ given: "*|Givet" ++ when: "*|När" ++ then: "*|Så" ++ and: "*|Och" ++ but: "*|Men" ++"sk": ++ name: Slovak ++ native: Slovensky ++ feature: Požiadavka ++ background: Pozadie ++ scenario: Scenár ++ scenario_outline: Náčrt Scenáru ++ examples: Príklady ++ given: "*|Pokiaľ" ++ when: "*|Keď" ++ then: "*|Tak" ++ and: "*|A" ++ but: "*|Ale" ++"sr-Latn": ++ name: Serbian (Latin) ++ native: Srpski (Latinica) ++ feature: Funkcionalnost|Mogućnost|Mogucnost|Osobina ++ background: Kontekst|Osnova|Pozadina ++ scenario: Scenario|Primer ++ scenario_outline: Struktura scenarija|Skica|Koncept ++ examples: Primeri|Scenariji ++ given: "*|Zadato|Zadate|Zatati" ++ when: "*|Kada|Kad" ++ then: "*|Onda" ++ and: "*|I" ++ but: "*|Ali" ++"sr-Cyrl": ++ name: Serbian ++ native: Српски ++ feature: Функционалност|Могућност|Особина ++ background: Контекст|Основа|Позадина ++ scenario: Сценарио|Пример ++ scenario_outline: Структура сценарија|Скица|Концепт ++ examples: Примери|Сценарији ++ given: "*|Задато|Задате|Задати" ++ when: "*|Када|Кад" ++ then: "*|Онда" ++ and: "*|И" ++ but: "*|Али" ++"tr": ++ name: Turkish ++ native: Türkçe ++ feature: Özellik ++ background: Geçmiş ++ scenario: Senaryo ++ scenario_outline: Senaryo taslağı ++ examples: Örnekler ++ given: "*|Diyelim ki" ++ when: "*|Eğer ki" ++ then: "*|O zaman" ++ and: "*|Ve" ++ but: "*|Fakat|Ama" ++"uk": ++ name: Ukrainian ++ native: Українська ++ feature: Функціонал ++ background: Передумова ++ scenario: Сценарій ++ scenario_outline: Структура сценарію ++ examples: Приклади ++ given: "*|Припустимо|Припустимо, що|Нехай|Дано" ++ when: "*|Якщо|Коли" ++ then: "*|То|Тоді" ++ and: "*|І|А також|Та" ++ but: "*|Але" ++"uz": ++ name: Uzbek ++ native: Узбекча ++ feature: Функционал ++ background: Тарих ++ scenario: Сценарий ++ scenario_outline: Сценарий структураси ++ examples: Мисоллар ++ given: "*|Агар" ++ when: "*|Агар" ++ then: "*|Унда" ++ and: "*|Ва" ++ but: "*|Лекин|Бирок|Аммо" ++"vi": ++ name: Vietnamese ++ native: Tiếng Việt ++ feature: Tính năng ++ background: Bối cảnh ++ scenario: Tình huống|Kịch bản ++ scenario_outline: Khung tình huống|Khung kịch bản ++ examples: Dữ liệu ++ given: "*|Biết|Cho" ++ when: "*|Khi" ++ then: "*|Thì" ++ and: "*|Và" ++ but: "*|Nhưng" ++"zh-CN": ++ name: Chinese simplified ++ native: 简体中文 ++ feature: 功能 ++ background: 背景 ++ scenario: 场景 ++ scenario_outline: 场景大纲 ++ examples: 例子 ++ given: "*|假如<" ++ when: "*|当<" ++ then: "*|那么<" ++ and: "*|而且<" ++ but: "*|但是<" ++"zh-TW": ++ name: Chinese traditional ++ native: 繁體中文 ++ feature: 功能 ++ background: 背景 ++ scenario: 場景|劇本 ++ scenario_outline: 場景大綱|劇本大綱 ++ examples: 例子 ++ given: "*|假設<" ++ when: "*|當<" ++ then: "*|那麼<" ++ and: "*|而且<|並且<" ++ but: "*|但是<" +diff --git a/bin/gherkin-languages.json b/bin/gherkin-languages.json +deleted file mode 100644 +index d2d5e93..0000000 +--- a/bin/gherkin-languages.json ++++ /dev/null +@@ -1,3193 +0,0 @@ +-{ +- "af": { +- "and": [ +- "* ", +- "En " +- ], +- "background": [ +- "Agtergrond" +- ], +- "but": [ +- "* ", +- "Maar " +- ], +- "examples": [ +- "Voorbeelde" +- ], +- "feature": [ +- "Funksie", +- "Besigheid Behoefte", +- "Vermoë" +- ], +- "given": [ +- "* ", +- "Gegewe " +- ], +- "name": "Afrikaans", +- "native": "Afrikaans", +- "scenario": [ +- "Situasie" +- ], +- "scenarioOutline": [ +- "Situasie Uiteensetting" +- ], +- "then": [ +- "* ", +- "Dan " +- ], +- "when": [ +- "* ", +- "Wanneer " +- ] +- }, +- "am": { +- "and": [ +- "* ", +- "Եվ " +- ], +- "background": [ +- "Կոնտեքստ" +- ], +- "but": [ +- "* ", +- "Բայց " +- ], +- "examples": [ +- "Օրինակներ" +- ], +- "feature": [ +- "Ֆունկցիոնալություն", +- "Հատկություն" +- ], +- "given": [ +- "* ", +- "Դիցուք " +- ], +- "name": "Armenian", +- "native": "հայերեն", +- "scenario": [ +- "Սցենար" +- ], +- "scenarioOutline": [ +- "Սցենարի կառուցվացքը" +- ], +- "then": [ +- "* ", +- "Ապա " +- ], +- "when": [ +- "* ", +- "Եթե ", +- "Երբ " +- ] +- }, +- "ar": { +- "and": [ +- "* ", +- "و " +- ], +- "background": [ +- "الخلفية" +- ], +- "but": [ +- "* ", +- "لكن " +- ], +- "examples": [ +- "امثلة" +- ], +- "feature": [ +- "خاصية" +- ], +- "given": [ +- "* ", +- "بفرض " +- ], +- "name": "Arabic", +- "native": "العربية", +- "scenario": [ +- "سيناريو" +- ], +- "scenarioOutline": [ +- "سيناريو مخطط" +- ], +- "then": [ +- "* ", +- "اذاً ", +- "ثم " +- ], +- "when": [ +- "* ", +- "متى ", +- "عندما " +- ] +- }, +- "ast": { +- "and": [ +- "* ", +- "Y ", +- "Ya " +- ], +- "background": [ +- "Antecedentes" +- ], +- "but": [ +- "* ", +- "Peru " +- ], +- "examples": [ +- "Exemplos" +- ], +- "feature": [ +- "Carauterística" +- ], +- "given": [ +- "* ", +- "Dáu ", +- "Dada ", +- "Daos ", +- "Daes " +- ], +- "name": "Asturian", +- "native": "asturianu", +- "scenario": [ +- "Casu" +- ], +- "scenarioOutline": [ +- "Esbozu del casu" +- ], +- "then": [ +- "* ", +- "Entós " +- ], +- "when": [ +- "* ", +- "Cuando " +- ] +- }, +- "az": { +- "and": [ +- "* ", +- "Və ", +- "Həm " +- ], +- "background": [ +- "Keçmiş", +- "Kontekst" +- ], +- "but": [ +- "* ", +- "Amma ", +- "Ancaq " +- ], +- "examples": [ +- "Nümunələr" +- ], +- "feature": [ +- "Özəllik" +- ], +- "given": [ +- "* ", +- "Tutaq ki ", +- "Verilir " +- ], +- "name": "Azerbaijani", +- "native": "Azərbaycanca", +- "scenario": [ +- "Ssenari" +- ], +- "scenarioOutline": [ +- "Ssenarinin strukturu" +- ], +- "then": [ +- "* ", +- "O halda " +- ], +- "when": [ +- "* ", +- "Əgər ", +- "Nə vaxt ki " +- ] +- }, +- "bg": { +- "and": [ +- "* ", +- "И " +- ], +- "background": [ +- "Предистория" +- ], +- "but": [ +- "* ", +- "Но " +- ], +- "examples": [ +- "Примери" +- ], +- "feature": [ +- "Функционалност" +- ], +- "given": [ +- "* ", +- "Дадено " +- ], +- "name": "Bulgarian", +- "native": "български", +- "scenario": [ +- "Сценарий" +- ], +- "scenarioOutline": [ +- "Рамка на сценарий" +- ], +- "then": [ +- "* ", +- "То " +- ], +- "when": [ +- "* ", +- "Когато " +- ] +- }, +- "bm": { +- "and": [ +- "* ", +- "Dan " +- ], +- "background": [ +- "Latar Belakang" +- ], +- "but": [ +- "* ", +- "Tetapi ", +- "Tapi " +- ], +- "examples": [ +- "Contoh" +- ], +- "feature": [ +- "Fungsi" +- ], +- "given": [ +- "* ", +- "Diberi ", +- "Bagi " +- ], +- "name": "Malay", +- "native": "Bahasa Melayu", +- "scenario": [ +- "Senario", +- "Situasi", +- "Keadaan" +- ], +- "scenarioOutline": [ +- "Kerangka Senario", +- "Kerangka Situasi", +- "Kerangka Keadaan", +- "Garis Panduan Senario" +- ], +- "then": [ +- "* ", +- "Maka ", +- "Kemudian " +- ], +- "when": [ +- "* ", +- "Apabila " +- ] +- }, +- "bs": { +- "and": [ +- "* ", +- "I ", +- "A " +- ], +- "background": [ +- "Pozadina" +- ], +- "but": [ +- "* ", +- "Ali " +- ], +- "examples": [ +- "Primjeri" +- ], +- "feature": [ +- "Karakteristika" +- ], +- "given": [ +- "* ", +- "Dato " +- ], +- "name": "Bosnian", +- "native": "Bosanski", +- "scenario": [ +- "Scenariju", +- "Scenario" +- ], +- "scenarioOutline": [ +- "Scenariju-obris", +- "Scenario-outline" +- ], +- "then": [ +- "* ", +- "Zatim " +- ], +- "when": [ +- "* ", +- "Kada " +- ] +- }, +- "ca": { +- "and": [ +- "* ", +- "I " +- ], +- "background": [ +- "Rerefons", +- "Antecedents" +- ], +- "but": [ +- "* ", +- "Però " +- ], +- "examples": [ +- "Exemples" +- ], +- "feature": [ +- "Característica", +- "Funcionalitat" +- ], +- "given": [ +- "* ", +- "Donat ", +- "Donada ", +- "Atès ", +- "Atesa " +- ], +- "name": "Catalan", +- "native": "català", +- "scenario": [ +- "Escenari" +- ], +- "scenarioOutline": [ +- "Esquema de l'escenari" +- ], +- "then": [ +- "* ", +- "Aleshores ", +- "Cal " +- ], +- "when": [ +- "* ", +- "Quan " +- ] +- }, +- "cs": { +- "and": [ +- "* ", +- "A také ", +- "A " +- ], +- "background": [ +- "Pozadí", +- "Kontext" +- ], +- "but": [ +- "* ", +- "Ale " +- ], +- "examples": [ +- "Příklady" +- ], +- "feature": [ +- "Požadavek" +- ], +- "given": [ +- "* ", +- "Pokud ", +- "Za předpokladu " +- ], +- "name": "Czech", +- "native": "Česky", +- "scenario": [ +- "Scénář" +- ], +- "scenarioOutline": [ +- "Náčrt Scénáře", +- "Osnova scénáře" +- ], +- "then": [ +- "* ", +- "Pak " +- ], +- "when": [ +- "* ", +- "Když " +- ] +- }, +- "cy-GB": { +- "and": [ +- "* ", +- "A " +- ], +- "background": [ +- "Cefndir" +- ], +- "but": [ +- "* ", +- "Ond " +- ], +- "examples": [ +- "Enghreifftiau" +- ], +- "feature": [ +- "Arwedd" +- ], +- "given": [ +- "* ", +- "Anrhegedig a " +- ], +- "name": "Welsh", +- "native": "Cymraeg", +- "scenario": [ +- "Scenario" +- ], +- "scenarioOutline": [ +- "Scenario Amlinellol" +- ], +- "then": [ +- "* ", +- "Yna " +- ], +- "when": [ +- "* ", +- "Pryd " +- ] +- }, +- "da": { +- "and": [ +- "* ", +- "Og " +- ], +- "background": [ +- "Baggrund" +- ], +- "but": [ +- "* ", +- "Men " +- ], +- "examples": [ +- "Eksempler" +- ], +- "feature": [ +- "Egenskab" +- ], +- "given": [ +- "* ", +- "Givet " +- ], +- "name": "Danish", +- "native": "dansk", +- "scenario": [ +- "Scenarie" +- ], +- "scenarioOutline": [ +- "Abstrakt Scenario" +- ], +- "then": [ +- "* ", +- "Så " +- ], +- "when": [ +- "* ", +- "Når " +- ] +- }, +- "de": { +- "and": [ +- "* ", +- "Und " +- ], +- "background": [ +- "Grundlage" +- ], +- "but": [ +- "* ", +- "Aber " +- ], +- "examples": [ +- "Beispiele" +- ], +- "feature": [ +- "Funktionalität" +- ], +- "given": [ +- "* ", +- "Angenommen ", +- "Gegeben sei ", +- "Gegeben seien " +- ], +- "name": "German", +- "native": "Deutsch", +- "scenario": [ +- "Szenario" +- ], +- "scenarioOutline": [ +- "Szenariogrundriss" +- ], +- "then": [ +- "* ", +- "Dann " +- ], +- "when": [ +- "* ", +- "Wenn " +- ] +- }, +- "el": { +- "and": [ +- "* ", +- "Και " +- ], +- "background": [ +- "Υπόβαθρο" +- ], +- "but": [ +- "* ", +- "Αλλά " +- ], +- "examples": [ +- "Παραδείγματα", +- "Σενάρια" +- ], +- "feature": [ +- "Δυνατότητα", +- "Λειτουργία" +- ], +- "given": [ +- "* ", +- "Δεδομένου " +- ], +- "name": "Greek", +- "native": "Ελληνικά", +- "scenario": [ +- "Σενάριο" +- ], +- "scenarioOutline": [ +- "Περιγραφή Σεναρίου", +- "Περίγραμμα Σεναρίου" +- ], +- "then": [ +- "* ", +- "Τότε " +- ], +- "when": [ +- "* ", +- "Όταν " +- ] +- }, +- "em": { +- "and": [ +- "* ", +- "😂" +- ], +- "background": [ +- "💤" +- ], +- "but": [ +- "* ", +- "😔" +- ], +- "examples": [ +- "📓" +- ], +- "feature": [ +- "📚" +- ], +- "given": [ +- "* ", +- "😐" +- ], +- "name": "Emoji", +- "native": "😀", +- "scenario": [ +- "📕" +- ], +- "scenarioOutline": [ +- "📖" +- ], +- "then": [ +- "* ", +- "🙏" +- ], +- "when": [ +- "* ", +- "🎬" +- ] +- }, +- "en": { +- "and": [ +- "* ", +- "And " +- ], +- "background": [ +- "Background" +- ], +- "but": [ +- "* ", +- "But " +- ], +- "examples": [ +- "Examples", +- "Scenarios" +- ], +- "feature": [ +- "Feature", +- "Business Need", +- "Ability" +- ], +- "given": [ +- "* ", +- "Given " +- ], +- "name": "English", +- "native": "English", +- "scenario": [ +- "Scenario" +- ], +- "scenarioOutline": [ +- "Scenario Outline", +- "Scenario Template" +- ], +- "then": [ +- "* ", +- "Then " +- ], +- "when": [ +- "* ", +- "When " +- ] +- }, +- "en-Scouse": { +- "and": [ +- "* ", +- "An " +- ], +- "background": [ +- "Dis is what went down" +- ], +- "but": [ +- "* ", +- "Buh " +- ], +- "examples": [ +- "Examples" +- ], +- "feature": [ +- "Feature" +- ], +- "given": [ +- "* ", +- "Givun ", +- "Youse know when youse got " +- ], +- "name": "Scouse", +- "native": "Scouse", +- "scenario": [ +- "The thing of it is" +- ], +- "scenarioOutline": [ +- "Wharrimean is" +- ], +- "then": [ +- "* ", +- "Dun ", +- "Den youse gotta " +- ], +- "when": [ +- "* ", +- "Wun ", +- "Youse know like when " +- ] +- }, +- "en-au": { +- "and": [ +- "* ", +- "Too right " +- ], +- "background": [ +- "First off" +- ], +- "but": [ +- "* ", +- "Yeah nah " +- ], +- "examples": [ +- "You'll wanna" +- ], +- "feature": [ +- "Pretty much" +- ], +- "given": [ +- "* ", +- "Y'know " +- ], +- "name": "Australian", +- "native": "Australian", +- "scenario": [ +- "Awww, look mate" +- ], +- "scenarioOutline": [ +- "Reckon it's like" +- ], +- "then": [ +- "* ", +- "But at the end of the day I reckon " +- ], +- "when": [ +- "* ", +- "It's just unbelievable " +- ] +- }, +- "en-lol": { +- "and": [ +- "* ", +- "AN " +- ], +- "background": [ +- "B4" +- ], +- "but": [ +- "* ", +- "BUT " +- ], +- "examples": [ +- "EXAMPLZ" +- ], +- "feature": [ +- "OH HAI" +- ], +- "given": [ +- "* ", +- "I CAN HAZ " +- ], +- "name": "LOLCAT", +- "native": "LOLCAT", +- "scenario": [ +- "MISHUN" +- ], +- "scenarioOutline": [ +- "MISHUN SRSLY" +- ], +- "then": [ +- "* ", +- "DEN " +- ], +- "when": [ +- "* ", +- "WEN " +- ] +- }, +- "en-old": { +- "and": [ +- "* ", +- "Ond ", +- "7 " +- ], +- "background": [ +- "Aer", +- "Ær" +- ], +- "but": [ +- "* ", +- "Ac " +- ], +- "examples": [ +- "Se the", +- "Se þe", +- "Se ðe" +- ], +- "feature": [ +- "Hwaet", +- "Hwæt" +- ], +- "given": [ +- "* ", +- "Thurh ", +- "Þurh ", +- "Ðurh " +- ], +- "name": "Old English", +- "native": "Englisc", +- "scenario": [ +- "Swa" +- ], +- "scenarioOutline": [ +- "Swa hwaer swa", +- "Swa hwær swa" +- ], +- "then": [ +- "* ", +- "Tha ", +- "Þa ", +- "Ða ", +- "Tha the ", +- "Þa þe ", +- "Ða ðe " +- ], +- "when": [ +- "* ", +- "Tha ", +- "Þa ", +- "Ða " +- ] +- }, +- "en-pirate": { +- "and": [ +- "* ", +- "Aye " +- ], +- "background": [ +- "Yo-ho-ho" +- ], +- "but": [ +- "* ", +- "Avast! " +- ], +- "examples": [ +- "Dead men tell no tales" +- ], +- "feature": [ +- "Ahoy matey!" +- ], +- "given": [ +- "* ", +- "Gangway! " +- ], +- "name": "Pirate", +- "native": "Pirate", +- "scenario": [ +- "Heave to" +- ], +- "scenarioOutline": [ +- "Shiver me timbers" +- ], +- "then": [ +- "* ", +- "Let go and haul " +- ], +- "when": [ +- "* ", +- "Blimey! " +- ] +- }, +- "eo": { +- "and": [ +- "* ", +- "Kaj " +- ], +- "background": [ +- "Fono" +- ], +- "but": [ +- "* ", +- "Sed " +- ], +- "examples": [ +- "Ekzemploj" +- ], +- "feature": [ +- "Trajto" +- ], +- "given": [ +- "* ", +- "Donitaĵo ", +- "Komence " +- ], +- "name": "Esperanto", +- "native": "Esperanto", +- "scenario": [ +- "Scenaro", +- "Kazo" +- ], +- "scenarioOutline": [ +- "Konturo de la scenaro", +- "Skizo", +- "Kazo-skizo" +- ], +- "then": [ +- "* ", +- "Do " +- ], +- "when": [ +- "* ", +- "Se " +- ] +- }, +- "es": { +- "and": [ +- "* ", +- "Y ", +- "E " +- ], +- "background": [ +- "Antecedentes" +- ], +- "but": [ +- "* ", +- "Pero " +- ], +- "examples": [ +- "Ejemplos" +- ], +- "feature": [ +- "Característica" +- ], +- "given": [ +- "* ", +- "Dado ", +- "Dada ", +- "Dados ", +- "Dadas " +- ], +- "name": "Spanish", +- "native": "español", +- "scenario": [ +- "Escenario" +- ], +- "scenarioOutline": [ +- "Esquema del escenario" +- ], +- "then": [ +- "* ", +- "Entonces " +- ], +- "when": [ +- "* ", +- "Cuando " +- ] +- }, +- "et": { +- "and": [ +- "* ", +- "Ja " +- ], +- "background": [ +- "Taust" +- ], +- "but": [ +- "* ", +- "Kuid " +- ], +- "examples": [ +- "Juhtumid" +- ], +- "feature": [ +- "Omadus" +- ], +- "given": [ +- "* ", +- "Eeldades " +- ], +- "name": "Estonian", +- "native": "eesti keel", +- "scenario": [ +- "Stsenaarium" +- ], +- "scenarioOutline": [ +- "Raamstsenaarium" +- ], +- "then": [ +- "* ", +- "Siis " +- ], +- "when": [ +- "* ", +- "Kui " +- ] +- }, +- "fa": { +- "and": [ +- "* ", +- "و " +- ], +- "background": [ +- "زمینه" +- ], +- "but": [ +- "* ", +- "اما " +- ], +- "examples": [ +- "نمونه ها" +- ], +- "feature": [ +- "وِیژگی" +- ], +- "given": [ +- "* ", +- "با فرض " +- ], +- "name": "Persian", +- "native": "فارسی", +- "scenario": [ +- "سناریو" +- ], +- "scenarioOutline": [ +- "الگوی سناریو" +- ], +- "then": [ +- "* ", +- "آنگاه " +- ], +- "when": [ +- "* ", +- "هنگامی " +- ] +- }, +- "fi": { +- "and": [ +- "* ", +- "Ja " +- ], +- "background": [ +- "Tausta" +- ], +- "but": [ +- "* ", +- "Mutta " +- ], +- "examples": [ +- "Tapaukset" +- ], +- "feature": [ +- "Ominaisuus" +- ], +- "given": [ +- "* ", +- "Oletetaan " +- ], +- "name": "Finnish", +- "native": "suomi", +- "scenario": [ +- "Tapaus" +- ], +- "scenarioOutline": [ +- "Tapausaihio" +- ], +- "then": [ +- "* ", +- "Niin " +- ], +- "when": [ +- "* ", +- "Kun " +- ] +- }, +- "fr": { +- "and": [ +- "* ", +- "Et que ", +- "Et qu'", +- "Et " +- ], +- "background": [ +- "Contexte" +- ], +- "but": [ +- "* ", +- "Mais que ", +- "Mais qu'", +- "Mais " +- ], +- "examples": [ +- "Exemples" +- ], +- "feature": [ +- "Fonctionnalité" +- ], +- "given": [ +- "* ", +- "Soit ", +- "Etant donné que ", +- "Etant donné qu'", +- "Etant donné ", +- "Etant donnée ", +- "Etant donnés ", +- "Etant données ", +- "Étant donné que ", +- "Étant donné qu'", +- "Étant donné ", +- "Étant donnée ", +- "Étant donnés ", +- "Étant données " +- ], +- "name": "French", +- "native": "français", +- "scenario": [ +- "Scénario" +- ], +- "scenarioOutline": [ +- "Plan du scénario", +- "Plan du Scénario" +- ], +- "then": [ +- "* ", +- "Alors " +- ], +- "when": [ +- "* ", +- "Quand ", +- "Lorsque ", +- "Lorsqu'" +- ] +- }, +- "ga": { +- "and": [ +- "* ", +- "Agus" +- ], +- "background": [ +- "Cúlra" +- ], +- "but": [ +- "* ", +- "Ach" +- ], +- "examples": [ +- "Samplaí" +- ], +- "feature": [ +- "Gné" +- ], +- "given": [ +- "* ", +- "Cuir i gcás go", +- "Cuir i gcás nach", +- "Cuir i gcás gur", +- "Cuir i gcás nár" +- ], +- "name": "Irish", +- "native": "Gaeilge", +- "scenario": [ +- "Cás" +- ], +- "scenarioOutline": [ +- "Cás Achomair" +- ], +- "then": [ +- "* ", +- "Ansin" +- ], +- "when": [ +- "* ", +- "Nuair a", +- "Nuair nach", +- "Nuair ba", +- "Nuair nár" +- ] +- }, +- "gj": { +- "and": [ +- "* ", +- "અને " +- ], +- "background": [ +- "બેકગ્રાઉન્ડ" +- ], +- "but": [ +- "* ", +- "પણ " +- ], +- "examples": [ +- "ઉદાહરણો" +- ], +- "feature": [ +- "લક્ષણ", +- "વ્યાપાર જરૂર", +- "ક્ષમતા" +- ], +- "given": [ +- "* ", +- "આપેલ છે " +- ], +- "name": "Gujarati", +- "native": "ગુજરાતી", +- "scenario": [ +- "સ્થિતિ" +- ], +- "scenarioOutline": [ +- "પરિદ્દશ્ય રૂપરેખા", +- "પરિદ્દશ્ય ઢાંચો" +- ], +- "then": [ +- "* ", +- "પછી " +- ], +- "when": [ +- "* ", +- "ક્યારે " +- ] +- }, +- "gl": { +- "and": [ +- "* ", +- "E " +- ], +- "background": [ +- "Contexto" +- ], +- "but": [ +- "* ", +- "Mais ", +- "Pero " +- ], +- "examples": [ +- "Exemplos" +- ], +- "feature": [ +- "Característica" +- ], +- "given": [ +- "* ", +- "Dado ", +- "Dada ", +- "Dados ", +- "Dadas " +- ], +- "name": "Galician", +- "native": "galego", +- "scenario": [ +- "Escenario" +- ], +- "scenarioOutline": [ +- "Esbozo do escenario" +- ], +- "then": [ +- "* ", +- "Entón ", +- "Logo " +- ], +- "when": [ +- "* ", +- "Cando " +- ] +- }, +- "he": { +- "and": [ +- "* ", +- "וגם " +- ], +- "background": [ +- "רקע" +- ], +- "but": [ +- "* ", +- "אבל " +- ], +- "examples": [ +- "דוגמאות" +- ], +- "feature": [ +- "תכונה" +- ], +- "given": [ +- "* ", +- "בהינתן " +- ], +- "name": "Hebrew", +- "native": "עברית", +- "scenario": [ +- "תרחיש" +- ], +- "scenarioOutline": [ +- "תבנית תרחיש" +- ], +- "then": [ +- "* ", +- "אז ", +- "אזי " +- ], +- "when": [ +- "* ", +- "כאשר " +- ] +- }, +- "hi": { +- "and": [ +- "* ", +- "और ", +- "तथा " +- ], +- "background": [ +- "पृष्ठभूमि" +- ], +- "but": [ +- "* ", +- "पर ", +- "परन्तु ", +- "किन्तु " +- ], +- "examples": [ +- "उदाहरण" +- ], +- "feature": [ +- "रूप लेख" +- ], +- "given": [ +- "* ", +- "अगर ", +- "यदि ", +- "चूंकि " +- ], +- "name": "Hindi", +- "native": "हिंदी", +- "scenario": [ +- "परिदृश्य" +- ], +- "scenarioOutline": [ +- "परिदृश्य रूपरेखा" +- ], +- "then": [ +- "* ", +- "तब ", +- "तदा " +- ], +- "when": [ +- "* ", +- "जब ", +- "कदा " +- ] +- }, +- "hr": { +- "and": [ +- "* ", +- "I " +- ], +- "background": [ +- "Pozadina" +- ], +- "but": [ +- "* ", +- "Ali " +- ], +- "examples": [ +- "Primjeri", +- "Scenariji" +- ], +- "feature": [ +- "Osobina", +- "Mogućnost", +- "Mogucnost" +- ], +- "given": [ +- "* ", +- "Zadan ", +- "Zadani ", +- "Zadano " +- ], +- "name": "Croatian", +- "native": "hrvatski", +- "scenario": [ +- "Scenarij" +- ], +- "scenarioOutline": [ +- "Skica", +- "Koncept" +- ], +- "then": [ +- "* ", +- "Onda " +- ], +- "when": [ +- "* ", +- "Kada ", +- "Kad " +- ] +- }, +- "ht": { +- "and": [ +- "* ", +- "Ak ", +- "Epi ", +- "E " +- ], +- "background": [ +- "Kontèks", +- "Istorik" +- ], +- "but": [ +- "* ", +- "Men " +- ], +- "examples": [ +- "Egzanp" +- ], +- "feature": [ +- "Karakteristik", +- "Mak", +- "Fonksyonalite" +- ], +- "given": [ +- "* ", +- "Sipoze ", +- "Sipoze ke ", +- "Sipoze Ke " +- ], +- "name": "Creole", +- "native": "kreyòl", +- "scenario": [ +- "Senaryo" +- ], +- "scenarioOutline": [ +- "Plan senaryo", +- "Plan Senaryo", +- "Senaryo deskripsyon", +- "Senaryo Deskripsyon", +- "Dyagram senaryo", +- "Dyagram Senaryo" +- ], +- "then": [ +- "* ", +- "Lè sa a ", +- "Le sa a " +- ], +- "when": [ +- "* ", +- "Lè ", +- "Le " +- ] +- }, +- "hu": { +- "and": [ +- "* ", +- "És " +- ], +- "background": [ +- "Háttér" +- ], +- "but": [ +- "* ", +- "De " +- ], +- "examples": [ +- "Példák" +- ], +- "feature": [ +- "Jellemző" +- ], +- "given": [ +- "* ", +- "Amennyiben ", +- "Adott " +- ], +- "name": "Hungarian", +- "native": "magyar", +- "scenario": [ +- "Forgatókönyv" +- ], +- "scenarioOutline": [ +- "Forgatókönyv vázlat" +- ], +- "then": [ +- "* ", +- "Akkor " +- ], +- "when": [ +- "* ", +- "Majd ", +- "Ha ", +- "Amikor " +- ] +- }, +- "id": { +- "and": [ +- "* ", +- "Dan " +- ], +- "background": [ +- "Dasar" +- ], +- "but": [ +- "* ", +- "Tapi " +- ], +- "examples": [ +- "Contoh" +- ], +- "feature": [ +- "Fitur" +- ], +- "given": [ +- "* ", +- "Dengan " +- ], +- "name": "Indonesian", +- "native": "Bahasa Indonesia", +- "scenario": [ +- "Skenario" +- ], +- "scenarioOutline": [ +- "Skenario konsep" +- ], +- "then": [ +- "* ", +- "Maka " +- ], +- "when": [ +- "* ", +- "Ketika " +- ] +- }, +- "is": { +- "and": [ +- "* ", +- "Og " +- ], +- "background": [ +- "Bakgrunnur" +- ], +- "but": [ +- "* ", +- "En " +- ], +- "examples": [ +- "Dæmi", +- "Atburðarásir" +- ], +- "feature": [ +- "Eiginleiki" +- ], +- "given": [ +- "* ", +- "Ef " +- ], +- "name": "Icelandic", +- "native": "Íslenska", +- "scenario": [ +- "Atburðarás" +- ], +- "scenarioOutline": [ +- "Lýsing Atburðarásar", +- "Lýsing Dæma" +- ], +- "then": [ +- "* ", +- "Þá " +- ], +- "when": [ +- "* ", +- "Þegar " +- ] +- }, +- "it": { +- "and": [ +- "* ", +- "E " +- ], +- "background": [ +- "Contesto" +- ], +- "but": [ +- "* ", +- "Ma " +- ], +- "examples": [ +- "Esempi" +- ], +- "feature": [ +- "Funzionalità" +- ], +- "given": [ +- "* ", +- "Dato ", +- "Data ", +- "Dati ", +- "Date " +- ], +- "name": "Italian", +- "native": "italiano", +- "scenario": [ +- "Scenario" +- ], +- "scenarioOutline": [ +- "Schema dello scenario" +- ], +- "then": [ +- "* ", +- "Allora " +- ], +- "when": [ +- "* ", +- "Quando " +- ] +- }, +- "ja": { +- "and": [ +- "* ", +- "かつ" +- ], +- "background": [ +- "背景" +- ], +- "but": [ +- "* ", +- "しかし", +- "但し", +- "ただし" +- ], +- "examples": [ +- "例", +- "サンプル" +- ], +- "feature": [ +- "フィーチャ", +- "機能" +- ], +- "given": [ +- "* ", +- "前提" +- ], +- "name": "Japanese", +- "native": "日本語", +- "scenario": [ +- "シナリオ" +- ], +- "scenarioOutline": [ +- "シナリオアウトライン", +- "シナリオテンプレート", +- "テンプレ", +- "シナリオテンプレ" +- ], +- "then": [ +- "* ", +- "ならば" +- ], +- "when": [ +- "* ", +- "もし" +- ] +- }, +- "jv": { +- "and": [ +- "* ", +- "Lan " +- ], +- "background": [ +- "Dasar" +- ], +- "but": [ +- "* ", +- "Tapi ", +- "Nanging ", +- "Ananging " +- ], +- "examples": [ +- "Conto", +- "Contone" +- ], +- "feature": [ +- "Fitur" +- ], +- "given": [ +- "* ", +- "Nalika ", +- "Nalikaning " +- ], +- "name": "Javanese", +- "native": "Basa Jawa", +- "scenario": [ +- "Skenario" +- ], +- "scenarioOutline": [ +- "Konsep skenario" +- ], +- "then": [ +- "* ", +- "Njuk ", +- "Banjur " +- ], +- "when": [ +- "* ", +- "Manawa ", +- "Menawa " +- ] +- }, +- "ka": { +- "and": [ +- "* ", +- "და" +- ], +- "background": [ +- "კონტექსტი" +- ], +- "but": [ +- "* ", +- "მაგ­რამ" +- ], +- "examples": [ +- "მაგალითები" +- ], +- "feature": [ +- "თვისება" +- ], +- "given": [ +- "* ", +- "მოცემული" +- ], +- "name": "Georgian", +- "native": "ქართველი", +- "scenario": [ +- "სცენარის" +- ], +- "scenarioOutline": [ +- "სცენარის ნიმუში" +- ], +- "then": [ +- "* ", +- "მაშინ" +- ], +- "when": [ +- "* ", +- "როდესაც" +- ] +- }, +- "kn": { +- "and": [ +- "* ", +- "ಮತ್ತು " +- ], +- "background": [ +- "ಹಿನ್ನೆಲೆ" +- ], +- "but": [ +- "* ", +- "ಆದರೆ " +- ], +- "examples": [ +- "ಉದಾಹರಣೆಗಳು" +- ], +- "feature": [ +- "ಹೆಚ್ಚಳ" +- ], +- "given": [ +- "* ", +- "ನೀಡಿದ " +- ], +- "name": "Kannada", +- "native": "ಕನ್ನಡ", +- "scenario": [ +- "ಕಥಾಸಾರಾಂಶ" +- ], +- "scenarioOutline": [ +- "ವಿವರಣೆ" +- ], +- "then": [ +- "* ", +- "ನಂತರ " +- ], +- "when": [ +- "* ", +- "ಸ್ಥಿತಿಯನ್ನು " +- ] +- }, +- "ko": { +- "and": [ +- "* ", +- "그리고" +- ], +- "background": [ +- "배경" +- ], +- "but": [ +- "* ", +- "하지만", +- "단" +- ], +- "examples": [ +- "예" +- ], +- "feature": [ +- "기능" +- ], +- "given": [ +- "* ", +- "조건", +- "먼저" +- ], +- "name": "Korean", +- "native": "한국어", +- "scenario": [ +- "시나리오" +- ], +- "scenarioOutline": [ +- "시나리오 개요" +- ], +- "then": [ +- "* ", +- "그러면" +- ], +- "when": [ +- "* ", +- "만일", +- "만약" +- ] +- }, +- "lt": { +- "and": [ +- "* ", +- "Ir " +- ], +- "background": [ +- "Kontekstas" +- ], +- "but": [ +- "* ", +- "Bet " +- ], +- "examples": [ +- "Pavyzdžiai", +- "Scenarijai", +- "Variantai" +- ], +- "feature": [ +- "Savybė" +- ], +- "given": [ +- "* ", +- "Duota " +- ], +- "name": "Lithuanian", +- "native": "lietuvių kalba", +- "scenario": [ +- "Scenarijus" +- ], +- "scenarioOutline": [ +- "Scenarijaus šablonas" +- ], +- "then": [ +- "* ", +- "Tada " +- ], +- "when": [ +- "* ", +- "Kai " +- ] +- }, +- "lu": { +- "and": [ +- "* ", +- "an ", +- "a " +- ], +- "background": [ +- "Hannergrond" +- ], +- "but": [ +- "* ", +- "awer ", +- "mä " +- ], +- "examples": [ +- "Beispiller" +- ], +- "feature": [ +- "Funktionalitéit" +- ], +- "given": [ +- "* ", +- "ugeholl " +- ], +- "name": "Luxemburgish", +- "native": "Lëtzebuergesch", +- "scenario": [ +- "Szenario" +- ], +- "scenarioOutline": [ +- "Plang vum Szenario" +- ], +- "then": [ +- "* ", +- "dann " +- ], +- "when": [ +- "* ", +- "wann " +- ] +- }, +- "lv": { +- "and": [ +- "* ", +- "Un " +- ], +- "background": [ +- "Konteksts", +- "Situācija" +- ], +- "but": [ +- "* ", +- "Bet " +- ], +- "examples": [ +- "Piemēri", +- "Paraugs" +- ], +- "feature": [ +- "Funkcionalitāte", +- "Fīča" +- ], +- "given": [ +- "* ", +- "Kad " +- ], +- "name": "Latvian", +- "native": "latviešu", +- "scenario": [ +- "Scenārijs" +- ], +- "scenarioOutline": [ +- "Scenārijs pēc parauga" +- ], +- "then": [ +- "* ", +- "Tad " +- ], +- "when": [ +- "* ", +- "Ja " +- ] +- }, +- "mk-Cyrl": { +- "and": [ +- "* ", +- "И " +- ], +- "background": [ +- "Контекст", +- "Содржина" +- ], +- "but": [ +- "* ", +- "Но " +- ], +- "examples": [ +- "Примери", +- "Сценарија" +- ], +- "feature": [ +- "Функционалност", +- "Бизнис потреба", +- "Можност" +- ], +- "given": [ +- "* ", +- "Дадено ", +- "Дадена " +- ], +- "name": "Macedonian", +- "native": "Македонски", +- "scenario": [ +- "Сценарио", +- "На пример" +- ], +- "scenarioOutline": [ +- "Преглед на сценарија", +- "Скица", +- "Концепт" +- ], +- "then": [ +- "* ", +- "Тогаш " +- ], +- "when": [ +- "* ", +- "Кога " +- ] +- }, +- "mk-Latn": { +- "and": [ +- "* ", +- "I " +- ], +- "background": [ +- "Kontekst", +- "Sodrzhina" +- ], +- "but": [ +- "* ", +- "No " +- ], +- "examples": [ +- "Primeri", +- "Scenaria" +- ], +- "feature": [ +- "Funkcionalnost", +- "Biznis potreba", +- "Mozhnost" +- ], +- "given": [ +- "* ", +- "Dadeno ", +- "Dadena " +- ], +- "name": "Macedonian (Latin)", +- "native": "Makedonski (Latinica)", +- "scenario": [ +- "Scenario", +- "Na primer" +- ], +- "scenarioOutline": [ +- "Pregled na scenarija", +- "Skica", +- "Koncept" +- ], +- "then": [ +- "* ", +- "Togash " +- ], +- "when": [ +- "* ", +- "Koga " +- ] +- }, +- "mn": { +- "and": [ +- "* ", +- "Мөн ", +- "Тэгээд " +- ], +- "background": [ +- "Агуулга" +- ], +- "but": [ +- "* ", +- "Гэхдээ ", +- "Харин " +- ], +- "examples": [ +- "Тухайлбал" +- ], +- "feature": [ +- "Функц", +- "Функционал" +- ], +- "given": [ +- "* ", +- "Өгөгдсөн нь ", +- "Анх " +- ], +- "name": "Mongolian", +- "native": "монгол", +- "scenario": [ +- "Сценар" +- ], +- "scenarioOutline": [ +- "Сценарын төлөвлөгөө" +- ], +- "then": [ +- "* ", +- "Тэгэхэд ", +- "Үүний дараа " +- ], +- "when": [ +- "* ", +- "Хэрэв " +- ] +- }, +- "nl": { +- "and": [ +- "* ", +- "En " +- ], +- "background": [ +- "Achtergrond" +- ], +- "but": [ +- "* ", +- "Maar " +- ], +- "examples": [ +- "Voorbeelden" +- ], +- "feature": [ +- "Functionaliteit" +- ], +- "given": [ +- "* ", +- "Gegeven ", +- "Stel " +- ], +- "name": "Dutch", +- "native": "Nederlands", +- "scenario": [ +- "Scenario" +- ], +- "scenarioOutline": [ +- "Abstract Scenario" +- ], +- "then": [ +- "* ", +- "Dan " +- ], +- "when": [ +- "* ", +- "Als ", +- "Wanneer " +- ] +- }, +- "no": { +- "and": [ +- "* ", +- "Og " +- ], +- "background": [ +- "Bakgrunn" +- ], +- "but": [ +- "* ", +- "Men " +- ], +- "examples": [ +- "Eksempler" +- ], +- "feature": [ +- "Egenskap" +- ], +- "given": [ +- "* ", +- "Gitt " +- ], +- "name": "Norwegian", +- "native": "norsk", +- "scenario": [ +- "Scenario" +- ], +- "scenarioOutline": [ +- "Scenariomal", +- "Abstrakt Scenario" +- ], +- "then": [ +- "* ", +- "Så " +- ], +- "when": [ +- "* ", +- "Når " +- ] +- }, +- "pa": { +- "and": [ +- "* ", +- "ਅਤੇ " +- ], +- "background": [ +- "ਪਿਛੋਕੜ" +- ], +- "but": [ +- "* ", +- "ਪਰ " +- ], +- "examples": [ +- "ਉਦਾਹਰਨਾਂ" +- ], +- "feature": [ +- "ਖਾਸੀਅਤ", +- "ਮੁਹਾਂਦਰਾ", +- "ਨਕਸ਼ ਨੁਹਾਰ" +- ], +- "given": [ +- "* ", +- "ਜੇਕਰ ", +- "ਜਿਵੇਂ ਕਿ " +- ], +- "name": "Panjabi", +- "native": "ਪੰਜਾਬੀ", +- "scenario": [ +- "ਪਟਕਥਾ" +- ], +- "scenarioOutline": [ +- "ਪਟਕਥਾ ਢਾਂਚਾ", +- "ਪਟਕਥਾ ਰੂਪ ਰੇਖਾ" +- ], +- "then": [ +- "* ", +- "ਤਦ " +- ], +- "when": [ +- "* ", +- "ਜਦੋਂ " +- ] +- }, +- "pl": { +- "and": [ +- "* ", +- "Oraz ", +- "I " +- ], +- "background": [ +- "Założenia" +- ], +- "but": [ +- "* ", +- "Ale " +- ], +- "examples": [ +- "Przykłady" +- ], +- "feature": [ +- "Właściwość", +- "Funkcja", +- "Aspekt", +- "Potrzeba biznesowa" +- ], +- "given": [ +- "* ", +- "Zakładając ", +- "Mając ", +- "Zakładając, że " +- ], +- "name": "Polish", +- "native": "polski", +- "scenario": [ +- "Scenariusz" +- ], +- "scenarioOutline": [ +- "Szablon scenariusza" +- ], +- "then": [ +- "* ", +- "Wtedy " +- ], +- "when": [ +- "* ", +- "Jeżeli ", +- "Jeśli ", +- "Gdy ", +- "Kiedy " +- ] +- }, +- "pt": { +- "and": [ +- "* ", +- "E " +- ], +- "background": [ +- "Contexto", +- "Cenário de Fundo", +- "Cenario de Fundo", +- "Fundo" +- ], +- "but": [ +- "* ", +- "Mas " +- ], +- "examples": [ +- "Exemplos", +- "Cenários", +- "Cenarios" +- ], +- "feature": [ +- "Funcionalidade", +- "Característica", +- "Caracteristica" +- ], +- "given": [ +- "* ", +- "Dado ", +- "Dada ", +- "Dados ", +- "Dadas " +- ], +- "name": "Portuguese", +- "native": "português", +- "scenario": [ +- "Cenário", +- "Cenario" +- ], +- "scenarioOutline": [ +- "Esquema do Cenário", +- "Esquema do Cenario", +- "Delineação do Cenário", +- "Delineacao do Cenario" +- ], +- "then": [ +- "* ", +- "Então ", +- "Entao " +- ], +- "when": [ +- "* ", +- "Quando " +- ] +- }, +- "ro": { +- "and": [ +- "* ", +- "Si ", +- "Și ", +- "Şi " +- ], +- "background": [ +- "Context" +- ], +- "but": [ +- "* ", +- "Dar " +- ], +- "examples": [ +- "Exemple" +- ], +- "feature": [ +- "Functionalitate", +- "Funcționalitate", +- "Funcţionalitate" +- ], +- "given": [ +- "* ", +- "Date fiind ", +- "Dat fiind ", +- "Dată fiind", +- "Dati fiind ", +- "Dați fiind ", +- "Daţi fiind " +- ], +- "name": "Romanian", +- "native": "română", +- "scenario": [ +- "Scenariu" +- ], +- "scenarioOutline": [ +- "Structura scenariu", +- "Structură scenariu" +- ], +- "then": [ +- "* ", +- "Atunci " +- ], +- "when": [ +- "* ", +- "Cand ", +- "Când " +- ] +- }, +- "ru": { +- "and": [ +- "* ", +- "И ", +- "К тому же ", +- "Также " +- ], +- "background": [ +- "Предыстория", +- "Контекст" +- ], +- "but": [ +- "* ", +- "Но ", +- "А " +- ], +- "examples": [ +- "Примеры" +- ], +- "feature": [ +- "Функция", +- "Функциональность", +- "Функционал", +- "Свойство" +- ], +- "given": [ +- "* ", +- "Допустим ", +- "Дано ", +- "Пусть ", +- "Если " +- ], +- "name": "Russian", +- "native": "русский", +- "scenario": [ +- "Сценарий" +- ], +- "scenarioOutline": [ +- "Структура сценария" +- ], +- "then": [ +- "* ", +- "То ", +- "Затем ", +- "Тогда " +- ], +- "when": [ +- "* ", +- "Когда " +- ] +- }, +- "sk": { +- "and": [ +- "* ", +- "A ", +- "A tiež ", +- "A taktiež ", +- "A zároveň " +- ], +- "background": [ +- "Pozadie" +- ], +- "but": [ +- "* ", +- "Ale " +- ], +- "examples": [ +- "Príklady" +- ], +- "feature": [ +- "Požiadavka", +- "Funkcia", +- "Vlastnosť" +- ], +- "given": [ +- "* ", +- "Pokiaľ ", +- "Za predpokladu " +- ], +- "name": "Slovak", +- "native": "Slovensky", +- "scenario": [ +- "Scenár" +- ], +- "scenarioOutline": [ +- "Náčrt Scenáru", +- "Náčrt Scenára", +- "Osnova Scenára" +- ], +- "then": [ +- "* ", +- "Tak ", +- "Potom " +- ], +- "when": [ +- "* ", +- "Keď ", +- "Ak " +- ] +- }, +- "sl": { +- "and": [ +- "In ", +- "Ter " +- ], +- "background": [ +- "Kontekst", +- "Osnova", +- "Ozadje" +- ], +- "but": [ +- "Toda ", +- "Ampak ", +- "Vendar " +- ], +- "examples": [ +- "Primeri", +- "Scenariji" +- ], +- "feature": [ +- "Funkcionalnost", +- "Funkcija", +- "Možnosti", +- "Moznosti", +- "Lastnost", +- "Značilnost" +- ], +- "given": [ +- "Dano ", +- "Podano ", +- "Zaradi ", +- "Privzeto " +- ], +- "name": "Slovenian", +- "native": "Slovenski", +- "scenario": [ +- "Scenarij", +- "Primer" +- ], +- "scenarioOutline": [ +- "Struktura scenarija", +- "Skica", +- "Koncept", +- "Oris scenarija", +- "Osnutek" +- ], +- "then": [ +- "Nato ", +- "Potem ", +- "Takrat " +- ], +- "when": [ +- "Ko ", +- "Ce ", +- "Če ", +- "Kadar " +- ] +- }, +- "sr-Cyrl": { +- "and": [ +- "* ", +- "И " +- ], +- "background": [ +- "Контекст", +- "Основа", +- "Позадина" +- ], +- "but": [ +- "* ", +- "Али " +- ], +- "examples": [ +- "Примери", +- "Сценарији" +- ], +- "feature": [ +- "Функционалност", +- "Могућност", +- "Особина" +- ], +- "given": [ +- "* ", +- "За дато ", +- "За дате ", +- "За дати " +- ], +- "name": "Serbian", +- "native": "Српски", +- "scenario": [ +- "Сценарио", +- "Пример" +- ], +- "scenarioOutline": [ +- "Структура сценарија", +- "Скица", +- "Концепт" +- ], +- "then": [ +- "* ", +- "Онда " +- ], +- "when": [ +- "* ", +- "Када ", +- "Кад " +- ] +- }, +- "sr-Latn": { +- "and": [ +- "* ", +- "I " +- ], +- "background": [ +- "Kontekst", +- "Osnova", +- "Pozadina" +- ], +- "but": [ +- "* ", +- "Ali " +- ], +- "examples": [ +- "Primeri", +- "Scenariji" +- ], +- "feature": [ +- "Funkcionalnost", +- "Mogućnost", +- "Mogucnost", +- "Osobina" +- ], +- "given": [ +- "* ", +- "Za dato ", +- "Za date ", +- "Za dati " +- ], +- "name": "Serbian (Latin)", +- "native": "Srpski (Latinica)", +- "scenario": [ +- "Scenario", +- "Primer" +- ], +- "scenarioOutline": [ +- "Struktura scenarija", +- "Skica", +- "Koncept" +- ], +- "then": [ +- "* ", +- "Onda " +- ], +- "when": [ +- "* ", +- "Kada ", +- "Kad " +- ] +- }, +- "sv": { +- "and": [ +- "* ", +- "Och " +- ], +- "background": [ +- "Bakgrund" +- ], +- "but": [ +- "* ", +- "Men " +- ], +- "examples": [ +- "Exempel" +- ], +- "feature": [ +- "Egenskap" +- ], +- "given": [ +- "* ", +- "Givet " +- ], +- "name": "Swedish", +- "native": "Svenska", +- "scenario": [ +- "Scenario" +- ], +- "scenarioOutline": [ +- "Abstrakt Scenario", +- "Scenariomall" +- ], +- "then": [ +- "* ", +- "Så " +- ], +- "when": [ +- "* ", +- "När " +- ] +- }, +- "ta": { +- "and": [ +- "* ", +- "மேலும் ", +- "மற்றும் " +- ], +- "background": [ +- "பின்னணி" +- ], +- "but": [ +- "* ", +- "ஆனால் " +- ], +- "examples": [ +- "எடுத்துக்காட்டுகள்", +- "காட்சிகள்", +- " நிலைமைகளில்" +- ], +- "feature": [ +- "அம்சம்", +- "வணிக தேவை", +- "திறன்" +- ], +- "given": [ +- "* ", +- "கொடுக்கப்பட்ட " +- ], +- "name": "Tamil", +- "native": "தமிழ்", +- "scenario": [ +- "காட்சி" +- ], +- "scenarioOutline": [ +- "காட்சி சுருக்கம்", +- "காட்சி வார்ப்புரு" +- ], +- "then": [ +- "* ", +- "அப்பொழுது " +- ], +- "when": [ +- "* ", +- "எப்போது " +- ] +- }, +- "th": { +- "and": [ +- "* ", +- "และ " +- ], +- "background": [ +- "แนวคิด" +- ], +- "but": [ +- "* ", +- "แต่ " +- ], +- "examples": [ +- "ชุดของตัวอย่าง", +- "ชุดของเหตุการณ์" +- ], +- "feature": [ +- "โครงหลัก", +- "ความต้องการทางธุรกิจ", +- "ความสามารถ" +- ], +- "given": [ +- "* ", +- "กำหนดให้ " +- ], +- "name": "Thai", +- "native": "ไทย", +- "scenario": [ +- "เหตุการณ์" +- ], +- "scenarioOutline": [ +- "สรุปเหตุการณ์", +- "โครงสร้างของเหตุการณ์" +- ], +- "then": [ +- "* ", +- "ดังนั้น " +- ], +- "when": [ +- "* ", +- "เมื่อ " +- ] +- }, +- "tl": { +- "and": [ +- "* ", +- "మరియు " +- ], +- "background": [ +- "నేపథ్యం" +- ], +- "but": [ +- "* ", +- "కాని " +- ], +- "examples": [ +- "ఉదాహరణలు" +- ], +- "feature": [ +- "గుణము" +- ], +- "given": [ +- "* ", +- "చెప్పబడినది " +- ], +- "name": "Telugu", +- "native": "తెలుగు", +- "scenario": [ +- "సన్నివేశం" +- ], +- "scenarioOutline": [ +- "కథనం" +- ], +- "then": [ +- "* ", +- "అప్పుడు " +- ], +- "when": [ +- "* ", +- "ఈ పరిస్థితిలో " +- ] +- }, +- "tlh": { +- "and": [ +- "* ", +- "'ej ", +- "latlh " +- ], +- "background": [ +- "mo'" +- ], +- "but": [ +- "* ", +- "'ach ", +- "'a " +- ], +- "examples": [ +- "ghantoH", +- "lutmey" +- ], +- "feature": [ +- "Qap", +- "Qu'meH 'ut", +- "perbogh", +- "poQbogh malja'", +- "laH" +- ], +- "given": [ +- "* ", +- "ghu' noblu' ", +- "DaH ghu' bejlu' " +- ], +- "name": "Klingon", +- "native": "tlhIngan", +- "scenario": [ +- "lut" +- ], +- "scenarioOutline": [ +- "lut chovnatlh" +- ], +- "then": [ +- "* ", +- "vaj " +- ], +- "when": [ +- "* ", +- "qaSDI' " +- ] +- }, +- "tr": { +- "and": [ +- "* ", +- "Ve " +- ], +- "background": [ +- "Geçmiş" +- ], +- "but": [ +- "* ", +- "Fakat ", +- "Ama " +- ], +- "examples": [ +- "Örnekler" +- ], +- "feature": [ +- "Özellik" +- ], +- "given": [ +- "* ", +- "Diyelim ki " +- ], +- "name": "Turkish", +- "native": "Türkçe", +- "scenario": [ +- "Senaryo" +- ], +- "scenarioOutline": [ +- "Senaryo taslağı" +- ], +- "then": [ +- "* ", +- "O zaman " +- ], +- "when": [ +- "* ", +- "Eğer ki " +- ] +- }, +- "tt": { +- "and": [ +- "* ", +- "Һәм ", +- "Вә " +- ], +- "background": [ +- "Кереш" +- ], +- "but": [ +- "* ", +- "Ләкин ", +- "Әмма " +- ], +- "examples": [ +- "Үрнәкләр", +- "Мисаллар" +- ], +- "feature": [ +- "Мөмкинлек", +- "Үзенчәлеклелек" +- ], +- "given": [ +- "* ", +- "Әйтик " +- ], +- "name": "Tatar", +- "native": "Татарча", +- "scenario": [ +- "Сценарий" +- ], +- "scenarioOutline": [ +- "Сценарийның төзелеше" +- ], +- "then": [ +- "* ", +- "Нәтиҗәдә " +- ], +- "when": [ +- "* ", +- "Әгәр " +- ] +- }, +- "uk": { +- "and": [ +- "* ", +- "І ", +- "А також ", +- "Та " +- ], +- "background": [ +- "Передумова" +- ], +- "but": [ +- "* ", +- "Але " +- ], +- "examples": [ +- "Приклади" +- ], +- "feature": [ +- "Функціонал" +- ], +- "given": [ +- "* ", +- "Припустимо ", +- "Припустимо, що ", +- "Нехай ", +- "Дано " +- ], +- "name": "Ukrainian", +- "native": "Українська", +- "scenario": [ +- "Сценарій" +- ], +- "scenarioOutline": [ +- "Структура сценарію" +- ], +- "then": [ +- "* ", +- "То ", +- "Тоді " +- ], +- "when": [ +- "* ", +- "Якщо ", +- "Коли " +- ] +- }, +- "ur": { +- "and": [ +- "* ", +- "اور " +- ], +- "background": [ +- "پس منظر" +- ], +- "but": [ +- "* ", +- "لیکن " +- ], +- "examples": [ +- "مثالیں" +- ], +- "feature": [ +- "صلاحیت", +- "کاروبار کی ضرورت", +- "خصوصیت" +- ], +- "given": [ +- "* ", +- "اگر ", +- "بالفرض ", +- "فرض کیا " +- ], +- "name": "Urdu", +- "native": "اردو", +- "scenario": [ +- "منظرنامہ" +- ], +- "scenarioOutline": [ +- "منظر نامے کا خاکہ" +- ], +- "then": [ +- "* ", +- "پھر ", +- "تب " +- ], +- "when": [ +- "* ", +- "جب " +- ] +- }, +- "uz": { +- "and": [ +- "* ", +- "Ва " +- ], +- "background": [ +- "Тарих" +- ], +- "but": [ +- "* ", +- "Лекин ", +- "Бирок ", +- "Аммо " +- ], +- "examples": [ +- "Мисоллар" +- ], +- "feature": [ +- "Функционал" +- ], +- "given": [ +- "* ", +- "Агар " +- ], +- "name": "Uzbek", +- "native": "Узбекча", +- "scenario": [ +- "Сценарий" +- ], +- "scenarioOutline": [ +- "Сценарий структураси" +- ], +- "then": [ +- "* ", +- "Унда " +- ], +- "when": [ +- "* ", +- "Агар " +- ] +- }, +- "vi": { +- "and": [ +- "* ", +- "Và " +- ], +- "background": [ +- "Bối cảnh" +- ], +- "but": [ +- "* ", +- "Nhưng " +- ], +- "examples": [ +- "Dữ liệu" +- ], +- "feature": [ +- "Tính năng" +- ], +- "given": [ +- "* ", +- "Biết ", +- "Cho " +- ], +- "name": "Vietnamese", +- "native": "Tiếng Việt", +- "scenario": [ +- "Tình huống", +- "Kịch bản" +- ], +- "scenarioOutline": [ +- "Khung tình huống", +- "Khung kịch bản" +- ], +- "then": [ +- "* ", +- "Thì " +- ], +- "when": [ +- "* ", +- "Khi " +- ] +- }, +- "zh-CN": { +- "and": [ +- "* ", +- "而且", +- "并且", +- "同时" +- ], +- "background": [ +- "背景" +- ], +- "but": [ +- "* ", +- "但是" +- ], +- "examples": [ +- "例子" +- ], +- "feature": [ +- "功能" +- ], +- "given": [ +- "* ", +- "假如", +- "假设", +- "假定" +- ], +- "name": "Chinese simplified", +- "native": "简体中文", +- "scenario": [ +- "场景", +- "剧本" +- ], +- "scenarioOutline": [ +- "场景大纲", +- "剧本大纲" +- ], +- "then": [ +- "* ", +- "那么" +- ], +- "when": [ +- "* ", +- "当" +- ] +- }, +- "zh-TW": { +- "and": [ +- "* ", +- "而且", +- "並且", +- "同時" +- ], +- "background": [ +- "背景" +- ], +- "but": [ +- "* ", +- "但是" +- ], +- "examples": [ +- "例子" +- ], +- "feature": [ +- "功能" +- ], +- "given": [ +- "* ", +- "假如", +- "假設", +- "假定" +- ], +- "name": "Chinese traditional", +- "native": "繁體中文", +- "scenario": [ +- "場景", +- "劇本" +- ], +- "scenarioOutline": [ +- "場景大綱", +- "劇本大綱" +- ], +- "then": [ +- "* ", +- "那麼" +- ], +- "when": [ +- "* ", +- "當" +- ] +- } +-} diff --git a/meta-python/recipes-devtools/python/python3-behave/0055-CLEANUP-Remove-deprecated-parts.patch b/meta-python/recipes-devtools/python/python3-behave/0055-CLEANUP-Remove-deprecated-parts.patch new file mode 100644 index 000000000..101a887e5 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0055-CLEANUP-Remove-deprecated-parts.patch @@ -0,0 +1,736 @@ +From f66a9fd1bf7c2ed117fe697b80a0ea4b071773f3 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sat, 13 Jul 2019 13:21:27 +0200 +Subject: [PATCH] CLEANUP: Remove deprecated parts. + +--- + bin/convert_i18n_yaml.py | 77 ----- + bin/i18n.yml | 635 --------------------------------------- + 2 files changed, 712 deletions(-) + delete mode 100755 bin/convert_i18n_yaml.py + delete mode 100644 bin/i18n.yml + +diff --git a/bin/convert_i18n_yaml.py b/bin/convert_i18n_yaml.py +deleted file mode 100755 +index d6a6713..0000000 +--- a/bin/convert_i18n_yaml.py ++++ /dev/null +@@ -1,77 +0,0 @@ +-#!/usr/bin/env python +-# -*- coding: UTF-8 -*- +-# USAGE: convert_i18n_yaml.py [--data=i18n.yml] behave/i18n.py +-""" +-Generates I18N python module based on YAML description (i18n.yml). +- +-REQUIRES: +- * argparse +- * six +- * PyYAML +-""" +- +-from __future__ import absolute_import, print_function +-import argparse +-import os.path +-import six +-import sys +-import pprint +-import yaml +- +-HERE = os.path.dirname(__file__) +-NAME = os.path.basename(__file__) +-__version__ = "1.0" +- +-def yaml_normalize(data): +- for part in data: +- keywords = data[part] +- for k in keywords: +- v = keywords[k] +- # bloody YAML parser returns a mixture of unicode and str +- if not isinstance(v, six.text_type): +- v = v.decode("UTF-8") +- keywords[k] = v.split("|") +- return data +- +-def main(args=None): +- if args is None: +- args = sys.argv[1:] +- parser = argparse.ArgumentParser(prog=NAME, +- description="Generate python module i18n from YAML based data") +- parser.add_argument("-d", "--data", dest="yaml_file", +- default=os.path.join(HERE, "i18n.yml"), +- help="Path to i18n.yml file (YAML file).") +- parser.add_argument("output_file", default="stdout", +- help="Filename of Python I18N module (as output).") +- parser.add_argument("--version", action="version", version=__version__) +- +- options = parser.parse_args(args) +- if not os.path.isfile(options.yaml_file): +- parser.error("YAML file not found: %s" % options.yaml_file) +- +- # -- STEP 1: Load YAML data. +- languages = yaml.load(open(options.yaml_file)) +- languages = yaml_normalize(languages) +- +- # -- STEP 2: Generate python module with i18n data. +- contents = u"""# -*- coding: UTF-8 -*- +-# -- FILE GENERATED BY: convert_i18n_yaml.py with i18n.yml +-# pylint: disable=line-too-long +- +-languages = \\ +-""" +- if options.output_file in ("-", "stdout"): +- i18n_py = sys.stdout +- should_close = False +- else: +- i18n_py = open(options.output_file, "w") +- should_close = True +- i18n_py.write(contents.encode("UTF-8")) +- i18n_py.write(pprint.pformat(languages).encode("UTF-8")) +- i18n_py.write(u"\n") +- if should_close: +- i18n_py.close() +- return 0 +- +-if __name__ == "__main__": +- sys.exit(main()) +diff --git a/bin/i18n.yml b/bin/i18n.yml +deleted file mode 100644 +index 82345a4..0000000 +--- a/bin/i18n.yml ++++ /dev/null +@@ -1,635 +0,0 @@ +-# encoding: UTF-8 +-# +-# We use ISO 639-1 (language) and ISO 3166 alpha-2 (region - if applicable): +-# http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes +-# http://en.wikipedia.org/wiki/ISO_3166-1 +-# +-# If you want several aliases for a keyword, just separate them +-# with a | character. The * is a step keyword alias for all translations. +-# +-# If you do *not* want a trailing space after a keyword, end it with a < character. +-# (See Chinese for examples). +-# +-# This file copyright (c) 2009-2011 Mike Sassak, Gregory Hnatiuk, Aslak Hellesøy +-# +-# Permission is hereby granted, free of charge, to any person obtaining +-# a copy of this software and associated documentation files (the +-# "Software"), to deal in the Software without restriction, including +-# without limitation the rights to use, copy, modify, merge, publish, +-# distribute, sublicense, and/or sell copies of the Software, and to +-# permit persons to whom the Software is furnished to do so, subject to +-# the following conditions: +-# +-# The above copyright notice and this permission notice shall be +-# included in all copies or substantial portions of the Software. +-# +-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +-# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +-# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +-# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +-# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +-# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +-# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +- +-"en": +- name: English +- native: English +- feature: Feature +- background: Background +- scenario: Scenario +- scenario_outline: Scenario Outline|Scenario Template +- examples: Examples|Scenarios +- given: "*|Given" +- when: "*|When" +- then: "*|Then" +- and: "*|And" +- but: "*|But" +- +-# Please keep the grammars in alphabetical order by name from here and down. +- +-"ar": +- name: Arabic +- native: العربية +- feature: خاصية +- background: الخلفية +- scenario: سيناريو +- scenario_outline: سيناريو مخطط +- examples: امثلة +- given: "*|بفرض" +- when: "*|متى|عندما" +- then: "*|اذاً|ثم" +- and: "*|و" +- but: "*|لكن" +-"bg": +- name: Bulgarian +- native: български +- feature: Функционалност +- background: Предистория +- scenario: Сценарий +- scenario_outline: Рамка на сценарий +- examples: Примери +- given: "*|Дадено" +- when: "*|Когато" +- then: "*|То" +- and: "*|И" +- but: "*|Но" +-"ca": +- name: Catalan +- native: català +- background: Rerefons|Antecedents +- feature: Característica|Funcionalitat +- scenario: Escenari +- scenario_outline: Esquema de l'escenari +- examples: Exemples +- given: "*|Donat|Donada|Atès|Atesa" +- when: "*|Quan" +- then: "*|Aleshores|Cal" +- and: "*|I" +- but: "*|Però" +-"cy-GB": +- name: Welsh +- native: Cymraeg +- background: Cefndir +- feature: Arwedd +- scenario: Scenario +- scenario_outline: Scenario Amlinellol +- examples: Enghreifftiau +- given: "*|Anrhegedig a" +- when: "*|Pryd" +- then: "*|Yna" +- and: "*|A" +- but: "*|Ond" +-"cs": +- name: Czech +- native: Česky +- feature: Požadavek +- background: Pozadí|Kontext +- scenario: Scénář +- scenario_outline: Náčrt Scénáře|Osnova scénáře +- examples: Příklady +- given: "*|Pokud|Za předpokladu" +- when: "*|Když" +- then: "*|Pak" +- and: "*|A|A také" +- but: "*|Ale" +-"da": +- name: Danish +- native: dansk +- feature: Egenskab +- background: Baggrund +- scenario: Scenarie +- scenario_outline: Abstrakt Scenario +- examples: Eksempler +- given: "*|Givet" +- when: "*|Når" +- then: "*|Så" +- and: "*|Og" +- but: "*|Men" +-"de": +- name: German +- native: Deutsch +- feature: Funktionalität +- background: Grundlage +- scenario: Szenario +- scenario_outline: Szenariogrundriss +- examples: Beispiele +- given: "*|Angenommen|Gegeben sei" +- when: "*|Wenn" +- then: "*|Dann" +- and: "*|Und" +- but: "*|Aber" +-"en-au": +- name: Australian +- native: Australian +- feature: Crikey +- background: Background +- scenario: Mate +- scenario_outline: Blokes +- examples: Cobber +- given: "*|Ya know how" +- when: "*|When" +- then: "*|Ya gotta" +- and: "*|N" +- but: "*|Cept" +-"en-lol": +- name: LOLCAT +- native: LOLCAT +- feature: OH HAI +- background: B4 +- scenario: MISHUN +- scenario_outline: MISHUN SRSLY +- examples: EXAMPLZ +- given: "*|I CAN HAZ" +- when: "*|WEN" +- then: "*|DEN" +- and: "*|AN" +- but: "*|BUT" +-"en-pirate": +- name: Pirate +- native: Pirate +- feature: Ahoy matey! +- background: Yo-ho-ho +- scenario: Heave to +- scenario_outline: Shiver me timbers +- examples: Dead men tell no tales +- given: "*|Gangway!" +- when: "*|Blimey!" +- then: "*|Let go and haul" +- and: "*|Aye" +- but: "*|Avast!" +-"en-Scouse": +- name: Scouse +- native: Scouse +- feature: Feature +- background: "Dis is what went down" +- scenario: "The thing of it is" +- scenario_outline: "Wharrimean is" +- examples: Examples +- given: "*|Givun|Youse know when youse got" +- when: "*|Wun|Youse know like when" +- then: "*|Dun|Den youse gotta" +- and: "*|An" +- but: "*|Buh" +-"en-tx": +- name: Texan +- native: Texan +- feature: Feature +- background: Background +- scenario: Scenario +- scenario_outline: All y'all +- examples: Examples +- given: "*|Given y'all" +- when: "*|When y'all" +- then: "*|Then y'all" +- and: "*|And y'all" +- but: "*|But y'all" +-"eo": +- name: Esperanto +- native: Esperanto +- feature: Trajto +- background: Fono +- scenario: Scenaro +- scenario_outline: Konturo de la scenaro +- examples: Ekzemploj +- given: "*|Donitaĵo" +- when: "*|Se" +- then: "*|Do" +- and: "*|Kaj" +- but: "*|Sed" +-"es": +- name: Spanish +- native: español +- background: Antecedentes +- feature: Característica +- scenario: Escenario +- scenario_outline: Esquema del escenario +- examples: Ejemplos +- given: "*|Dado|Dada|Dados|Dadas" +- when: "*|Cuando" +- then: "*|Entonces" +- and: "*|Y" +- but: "*|Pero" +-"et": +- name: Estonian +- native: eesti keel +- feature: Omadus +- background: Taust +- scenario: Stsenaarium +- scenario_outline: Raamstsenaarium +- examples: Juhtumid +- given: "*|Eeldades" +- when: "*|Kui" +- then: "*|Siis" +- and: "*|Ja" +- but: "*|Kuid" +-"fi": +- name: Finnish +- native: suomi +- feature: Ominaisuus +- background: Tausta +- scenario: Tapaus +- scenario_outline: Tapausaihio +- examples: Tapaukset +- given: "*|Oletetaan" +- when: "*|Kun" +- then: "*|Niin" +- and: "*|Ja" +- but: "*|Mutta" +-"fr": +- name: French +- native: français +- feature: Fonctionnalité +- background: Contexte +- scenario: Scénario +- scenario_outline: Plan du scénario|Plan du Scénario +- examples: Exemples +- given: "*|Soit|Etant donné|Etant donnée|Etant donnés|Etant données|Étant donné|Étant donnée|Étant donnés|Étant données" +- when: "*|Quand|Lorsque|Lorsqu'<" +- then: "*|Alors" +- and: "*|Et" +- but: "*|Mais" +-"gl": +- name: Galician +- native: galego +- feature: Característica +- background: Contexto +- scenario: Escenario +- scenario_outline: "Esbozo do escenario" +- examples: Exemplos +- given: "*|Dado|Dada|Dados|Dadas" +- when: "*|Cando" +- then: "*|Entón|Logo" +- and: "*|E" +- but: "*|Mais|Pero" +- +-"he": +- name: Hebrew +- native: עברית +- feature: תכונה +- background: רקע +- scenario: תרחיש +- scenario_outline: תבנית תרחיש +- examples: דוגמאות +- given: "*|בהינתן" +- when: "*|כאשר" +- then: "*|אז|אזי" +- and: "*|וגם" +- but: "*|אבל" +-"hr": +- name: Croatian +- native: hrvatski +- feature: Osobina|Mogućnost|Mogucnost +- background: Pozadina +- scenario: Scenarij +- scenario_outline: Skica|Koncept +- examples: Primjeri|Scenariji +- given: "*|Zadan|Zadani|Zadano" +- when: "*|Kada|Kad" +- then: "*|Onda" +- and: "*|I" +- but: "*|Ali" +-"hu": +- name: Hungarian +- native: magyar +- feature: Jellemző +- background: Háttér +- scenario: Forgatókönyv +- scenario_outline: Forgatókönyv vázlat +- examples: Példák +- given: "*|Amennyiben|Adott" +- when: "*|Majd|Ha|Amikor" +- then: "*|Akkor" +- and: "*|És" +- but: "*|De" +-"id": +- name: Indonesian +- native: Bahasa Indonesia +- feature: Fitur +- background: Dasar +- scenario: Skenario +- scenario_outline: Skenario konsep +- examples: Contoh +- given: "*|Dengan" +- when: "*|Ketika" +- then: "*|Maka" +- and: "*|Dan" +- but: "*|Tapi" +-"is": +- name: Icelandic +- native: Íslenska +- feature: Eiginleiki +- background: Bakgrunnur +- scenario: Atburðarás +- scenario_outline: Lýsing Atburðarásar|Lýsing Dæma +- examples: Dæmi|Atburðarásir +- given: "*|Ef" +- when: "*|Þegar" +- then: "*|Þá" +- and: "*|Og" +- but: "*|En" +-"it": +- name: Italian +- native: italiano +- feature: Funzionalità +- background: Contesto +- scenario: Scenario +- scenario_outline: Schema dello scenario +- examples: Esempi +- given: "*|Dato|Data|Dati|Date" +- when: "*|Quando" +- then: "*|Allora" +- and: "*|E" +- but: "*|Ma" +-"ja": +- name: Japanese +- native: 日本語 +- feature: フィーチャ|機能 +- background: 背景 +- scenario: シナリオ +- scenario_outline: シナリオアウトライン|シナリオテンプレート|テンプレ|シナリオテンプレ +- examples: 例|サンプル +- given: "*|前提<" +- when: "*|もし<" +- then: "*|ならば<" +- and: "*|かつ<" +- but: "*|しかし<|但し<|ただし<" +-"ko": +- name: Korean +- native: 한국어 +- background: 배경 +- feature: 기능 +- scenario: 시나리오 +- scenario_outline: 시나리오 개요 +- examples: 예 +- given: "*|조건<|먼저<" +- when: "*|만일<|만약<" +- then: "*|그러면<" +- and: "*|그리고<" +- but: "*|하지만<|단<" +-"lt": +- name: Lithuanian +- native: lietuvių kalba +- feature: Savybė +- background: Kontekstas +- scenario: Scenarijus +- scenario_outline: Scenarijaus šablonas +- examples: Pavyzdžiai|Scenarijai|Variantai +- given: "*|Duota" +- when: "*|Kai" +- then: "*|Tada" +- and: "*|Ir" +- but: "*|Bet" +-"lu": +- name: Luxemburgish +- native: Lëtzebuergesch +- feature: Funktionalitéit +- background: Hannergrond +- scenario: Szenario +- scenario_outline: Plang vum Szenario +- examples: Beispiller +- given: "*|ugeholl" +- when: "*|wann" +- then: "*|dann" +- and: "*|an|a" +- but: "*|awer|mä" +-"lv": +- name: Latvian +- native: latviešu +- feature: Funkcionalitāte|Fīča +- background: Konteksts|Situācija +- scenario: Scenārijs +- scenario_outline: Scenārijs pēc parauga +- examples: Piemēri|Paraugs +- given: "*|Kad" +- when: "*|Ja" +- then: "*|Tad" +- and: "*|Un" +- but: "*|Bet" +-"nl": +- name: Dutch +- native: Nederlands +- feature: Functionaliteit +- background: Achtergrond +- scenario: Scenario +- scenario_outline: Abstract Scenario +- examples: Voorbeelden +- given: "*|Gegeven|Stel" +- when: "*|Als" +- then: "*|Dan" +- and: "*|En" +- but: "*|Maar" +-"no": +- name: Norwegian +- native: norsk +- feature: Egenskap +- background: Bakgrunn +- scenario: Scenario +- scenario_outline: Scenariomal|Abstrakt Scenario +- examples: Eksempler +- given: "*|Gitt" +- when: "*|Når" +- then: "*|Så" +- and: "*|Og" +- but: "*|Men" +-"pl": +- name: Polish +- native: polski +- feature: Właściwość +- background: Założenia +- scenario: Scenariusz +- scenario_outline: Szablon scenariusza +- examples: Przykłady +- given: "*|Zakładając|Mając" +- when: "*|Jeżeli|Jeśli" +- then: "*|Wtedy" +- and: "*|Oraz|I" +- but: "*|Ale" +-"pt": +- name: Portuguese +- native: português +- background: Contexto +- feature: Funcionalidade +- scenario: Cenário|Cenario +- scenario_outline: Esquema do Cenário|Esquema do Cenario +- examples: Exemplos +- given: "*|Dado|Dada|Dados|Dadas" +- when: "*|Quando" +- then: "*|Então|Entao" +- and: "*|E" +- but: "*|Mas" +-"ro": +- name: Romanian +- native: română +- background: Context +- feature: Functionalitate|Funcționalitate|Funcţionalitate +- scenario: Scenariu +- scenario_outline: Structura scenariu|Structură scenariu +- examples: Exemple +- given: "*|Date fiind|Dat fiind|Dati fiind|Dați fiind|Daţi fiind" +- when: "*|Cand|Când" +- then: "*|Atunci" +- and: "*|Si|Și|Şi" +- but: "*|Dar" +-"ru": +- name: Russian +- native: русский +- feature: Функция|Функционал|Свойство +- background: Предыстория|Контекст +- scenario: Сценарий +- scenario_outline: Структура сценария +- examples: Примеры +- given: "*|Допустим|Дано|Пусть" +- when: "*|Если|Когда" +- then: "*|То|Тогда" +- and: "*|И|К тому же" +- but: "*|Но|А" +-"sv": +- name: Swedish +- native: Svenska +- feature: Egenskap +- background: Bakgrund +- scenario: Scenario +- scenario_outline: Abstrakt Scenario|Scenariomall +- examples: Exempel +- given: "*|Givet" +- when: "*|När" +- then: "*|Så" +- and: "*|Och" +- but: "*|Men" +-"sk": +- name: Slovak +- native: Slovensky +- feature: Požiadavka +- background: Pozadie +- scenario: Scenár +- scenario_outline: Náčrt Scenáru +- examples: Príklady +- given: "*|Pokiaľ" +- when: "*|Keď" +- then: "*|Tak" +- and: "*|A" +- but: "*|Ale" +-"sr-Latn": +- name: Serbian (Latin) +- native: Srpski (Latinica) +- feature: Funkcionalnost|Mogućnost|Mogucnost|Osobina +- background: Kontekst|Osnova|Pozadina +- scenario: Scenario|Primer +- scenario_outline: Struktura scenarija|Skica|Koncept +- examples: Primeri|Scenariji +- given: "*|Zadato|Zadate|Zatati" +- when: "*|Kada|Kad" +- then: "*|Onda" +- and: "*|I" +- but: "*|Ali" +-"sr-Cyrl": +- name: Serbian +- native: Српски +- feature: Функционалност|Могућност|Особина +- background: Контекст|Основа|Позадина +- scenario: Сценарио|Пример +- scenario_outline: Структура сценарија|Скица|Концепт +- examples: Примери|Сценарији +- given: "*|Задато|Задате|Задати" +- when: "*|Када|Кад" +- then: "*|Онда" +- and: "*|И" +- but: "*|Али" +-"tr": +- name: Turkish +- native: Türkçe +- feature: Özellik +- background: Geçmiş +- scenario: Senaryo +- scenario_outline: Senaryo taslağı +- examples: Örnekler +- given: "*|Diyelim ki" +- when: "*|Eğer ki" +- then: "*|O zaman" +- and: "*|Ve" +- but: "*|Fakat|Ama" +-"uk": +- name: Ukrainian +- native: Українська +- feature: Функціонал +- background: Передумова +- scenario: Сценарій +- scenario_outline: Структура сценарію +- examples: Приклади +- given: "*|Припустимо|Припустимо, що|Нехай|Дано" +- when: "*|Якщо|Коли" +- then: "*|То|Тоді" +- and: "*|І|А також|Та" +- but: "*|Але" +-"uz": +- name: Uzbek +- native: Узбекча +- feature: Функционал +- background: Тарих +- scenario: Сценарий +- scenario_outline: Сценарий структураси +- examples: Мисоллар +- given: "*|Агар" +- when: "*|Агар" +- then: "*|Унда" +- and: "*|Ва" +- but: "*|Лекин|Бирок|Аммо" +-"vi": +- name: Vietnamese +- native: Tiếng Việt +- feature: Tính năng +- background: Bối cảnh +- scenario: Tình huống|Kịch bản +- scenario_outline: Khung tình huống|Khung kịch bản +- examples: Dữ liệu +- given: "*|Biết|Cho" +- when: "*|Khi" +- then: "*|Thì" +- and: "*|Và" +- but: "*|Nhưng" +-"zh-CN": +- name: Chinese simplified +- native: 简体中文 +- feature: 功能 +- background: 背景 +- scenario: 场景 +- scenario_outline: 场景大纲 +- examples: 例子 +- given: "*|假如<" +- when: "*|当<" +- then: "*|那么<" +- and: "*|而且<" +- but: "*|但是<" +-"zh-TW": +- name: Chinese traditional +- native: 繁體中文 +- feature: 功能 +- background: 背景 +- scenario: 場景|劇本 +- scenario_outline: 場景大綱|劇本大綱 +- examples: 例子 +- given: "*|假設<" +- when: "*|當<" +- then: "*|那麼<" +- and: "*|而且<|並且<" +- but: "*|但是<" diff --git a/meta-python/recipes-devtools/python/python3-behave/0056-more.features-Perform-more-Gherkin-v6-checks-and-run.patch b/meta-python/recipes-devtools/python/python3-behave/0056-more.features-Perform-more-Gherkin-v6-checks-and-run.patch new file mode 100644 index 000000000..b5bac22cf --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0056-more.features-Perform-more-Gherkin-v6-checks-and-run.patch @@ -0,0 +1,155 @@ +From 65ec9d5d69251746a2723f7d2455c23166f3d1a9 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sat, 13 Jul 2019 13:22:21 +0200 +Subject: [PATCH] more.features: Perform more Gherkin v6 checks and run + examples. + +--- + invoke.yaml | 10 ++--- + more.features/environment.py | 28 +++++++++++++ + .../formatter.json.validate_output.feature | 22 +++++++++- + more.features/run_examples.feature | 41 +++++++++++++++++++ + 4 files changed, 93 insertions(+), 8 deletions(-) + create mode 100644 more.features/environment.py + create mode 100644 more.features/run_examples.feature + +diff --git a/invoke.yaml b/invoke.yaml +index d6f141c..a32345e 100644 +--- a/invoke.yaml ++++ b/invoke.yaml +@@ -24,13 +24,6 @@ sphinx: + - de + # PREPARED: - zh-CN + +-cleanup: +- extra_directories: +- - "build" +- - "dist" +- - "__WORKDIR__" +- - reports +- + cleanup: + extra_directories: + - "build" +@@ -47,6 +40,9 @@ cleanup_all: + - .hypothesis + - .pytest_cache + ++ extra_files: ++ - "**/testrun*.json" ++ + behave_test: + scopes: + - features +diff --git a/more.features/environment.py b/more.features/environment.py +new file mode 100644 +index 0000000..9d4302b +--- /dev/null ++++ b/more.features/environment.py +@@ -0,0 +1,28 @@ ++# -*- coding: UTF-8 -*- ++ ++from behave.tag_matcher import ActiveTagMatcher, setup_active_tag_values ++import sys ++ ++# -- MATCHES ANY TAGS: @use.with_{category}={value} ++# NOTE: active_tag_value_provider provides category values for active tags. ++python_version = "%s.%s" % sys.version_info[:2] ++active_tag_value_provider = { ++ "python.version": python_version, ++} ++active_tag_matcher = ActiveTagMatcher(active_tag_value_provider) ++ ++# ----------------------------------------------------------------------------- ++# HOOKS: ++# ----------------------------------------------------------------------------- ++def before_all(context): ++ # -- SETUP ACTIVE-TAG MATCHER (with userdata): ++ setup_active_tag_values(active_tag_value_provider, context.config.userdata) ++ ++def before_feature(context, feature): ++ if active_tag_matcher.should_exclude_with(feature.tags): ++ feature.skip(reason=active_tag_matcher.exclude_reason) ++ ++def before_scenario(context, scenario): ++ if active_tag_matcher.should_exclude_with(scenario.effective_tags): ++ scenario.skip(reason=active_tag_matcher.exclude_reason) ++ +diff --git a/more.features/formatter.json.validate_output.feature b/more.features/formatter.json.validate_output.feature +index a5f8ab6..31550d5 100644 +--- a/more.features/formatter.json.validate_output.feature ++++ b/more.features/formatter.json.validate_output.feature +@@ -34,4 +34,24 @@ Feature: Validate JSON Formatter Output + Then it should pass with: + """ + validate: testrun3.json ... OK +- """ +\ No newline at end of file ++ """ ++ ++ @gherkin_v6 ++ Scenario: Validate JSON output from example/gherkin_v6/ test run ++ Given I use the directory "examples/gherkin_v6" as working directory ++ When I run "behave -f json -o testrun_gherkin6_1.json features/" ++ When I run "../../bin/jsonschema_validate.py testrun_gherkin6_1.json" ++ Then it should pass with: ++ """ ++ validate: testrun_gherkin6_1.json ... OK ++ """ ++ ++ @gherkin_v6 ++ Scenario: Validate JSON output from example/gherkin_v6/ test run (case: partly failing) ++ Given I use the directory "examples/gherkin_v6" as working directory ++ When I run "behave --tags=fail -f json -o testrun_gherkin6_2.json features/" ++ When I run "../../bin/jsonschema_validate.py testrun_gherkin6_2.json" ++ Then it should pass with: ++ """ ++ validate: testrun_gherkin6_2.json ... OK ++ """ +diff --git a/more.features/run_examples.feature b/more.features/run_examples.feature +new file mode 100644 +index 0000000..4778866 +--- /dev/null ++++ b/more.features/run_examples.feature +@@ -0,0 +1,41 @@ ++Feature: Ensure that all examples are usable ++ ++ Scenario Outline: Use ++ Given I use the directory "" as working directory ++ When I run "behave " ++ Then it should ++ ++ Examples: ++ | example_dir | behave_cmdline | outcome | ++ | examples/env_vars | features/ | pass | ++ | examples/fixture.no_background | features/ | pass | ++ | examples/gherkin_v6 | features/ | pass | ++ ++ ++ Scenario: examples/gherkin_v6 -- @xfail parts ++ Given I use the directory "examples/gherkin_v6" as working directory ++ When I run "behave --tags=fail features/" ++ Then it should fail with: ++ """ ++ 0 features passed, 1 failed, 2 skipped ++ 0 rules passed, 1 failed, 6 skipped ++ 1 scenario passed, 2 failed, 12 skipped ++ 2 steps passed, 2 failed, 39 skipped, 0 undefined ++ """ ++ And the command output should contain: ++ """ ++ Failing scenarios: ++ features/rule_fails.feature:7 F0 -- Fails ++ features/rule_fails.feature:16 F2 -- Fails ++ """ ++ ++ ++ @use.with_python.version=3.4 ++ @use.with_python.version=3.5 ++ @use.with_python.version=3.6 ++ @use.with_python.version=3.7 ++ @use.with_python.version=3.8 ++ Scenario: examples/async_step (needs: py34 or newer) ++ Given I use the directory "examples/async_step" as working directory ++ When I run "behave features/" ++ Then it should pass diff --git a/meta-python/recipes-devtools/python/python3-behave/0057-UTIL-Correct-URL-and-python-module-old-was-broken-no.patch b/meta-python/recipes-devtools/python/python3-behave/0057-UTIL-Correct-URL-and-python-module-old-was-broken-no.patch new file mode 100644 index 000000000..79718261c --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0057-UTIL-Correct-URL-and-python-module-old-was-broken-no.patch @@ -0,0 +1,72 @@ +From da0a88f5311e4181a9c24a5853f6392209dceabe Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sat, 13 Jul 2019 13:39:49 +0200 +Subject: [PATCH] UTIL: Correct URL and python module (old was broken, no + longer exists). + +--- + bin/behave2cucumber_json.py | 19 ++++++++++++------- + 1 file changed, 12 insertions(+), 7 deletions(-) + +diff --git a/bin/behave2cucumber_json.py b/bin/behave2cucumber_json.py +index 738e444..541061e 100755 +--- a/bin/behave2cucumber_json.py ++++ b/bin/behave2cucumber_json.py +@@ -3,9 +3,10 @@ + # CONVERT: behave JSON dialect to cucumber JSON dialect + # ============================================================================= + # STATUS: __PROTOTYPE__ +-# REQUIRES: Python >= 2.6 +-# REQUIRES: https://github.com/behalfinc/b2c/ +-# SEE: https://github.com/behave/behave/issues/267#issuecomment-249607191 ++# REQUIRES: Python >= 2.7 ++# REQUIRES: https://github.com/behalf-oss/behave2cucumber ++# SEE: ++# * https://github.com/behave/behave/issues/267#issuecomment-251746565 + # ============================================================================= + """ + Convert a file with behave JSON data into a file with cucumber JSON data. +@@ -17,15 +18,16 @@ import json + import sys + import os.path + try: +- import b2c ++ import behave2cucumber + except ImportError: +- print("REQUIRE: https://github.com/behalfinc/b2c/ (not installed yet)") +- print("INSTALL: pip install b2c") ++ print("REQUIRE: https://github.com/behalf-oss/behave2cucumber (not installed yet)") ++ print("INSTALL: pip install behave2cucumber") + sys.exit(2) + + + NAME = os.path.basename(__file__) + ++ + def convert_behave_to_cucumber_json(behave_filename, cucumber_filename, + encoding="UTF-8", pretty=True): + """Convert behave JSON dialect into cucumber JSON dialect. +@@ -39,12 +41,14 @@ def convert_behave_to_cucumber_json(behave_filename, cucumber_filename, + + with open(behave_filename, "r") as behave_json: + with open(cucumber_filename, "w+") as output_file: +- cucumber_json = b2c.convert(json.load(behave_json, encoding)) ++ behave_json = json.load(behave_json, encoding) ++ cucumber_json = behave2cucumber.convert(behave_json) + # cucumber_text = json.dumps(cucumber_json, **dump_kwargs) + # output_file.write(cucumber_text) + json.dump(cucumber_json, output_file, **dump_kwargs) + return 0 + ++ + def main(args=None): + """Main function to run the script.""" + if args is None: +@@ -58,6 +62,7 @@ def main(args=None): + cucumber_filename = args[1] + return convert_behave_to_cucumber_json(behave_filename, cucumber_filename) + ++ + # -- AUTO-MAIN: + if __name__ == "__main__": + sys.exit(main()) diff --git a/meta-python/recipes-devtools/python/python3-behave/0058-UTIL-Formatting-tweaks.patch b/meta-python/recipes-devtools/python/python3-behave/0058-UTIL-Formatting-tweaks.patch new file mode 100644 index 000000000..4b708fa94 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0058-UTIL-Formatting-tweaks.patch @@ -0,0 +1,22 @@ +From 709d7c3604cc8f958dbd6360c66cabf612419c99 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sat, 13 Jul 2019 13:40:46 +0200 +Subject: [PATCH] UTIL: Formatting tweaks. + +--- + bin/behave2cucumber_json.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/bin/behave2cucumber_json.py b/bin/behave2cucumber_json.py +index 541061e..893a5ef 100755 +--- a/bin/behave2cucumber_json.py ++++ b/bin/behave2cucumber_json.py +@@ -20,7 +20,7 @@ import os.path + try: + import behave2cucumber + except ImportError: +- print("REQUIRE: https://github.com/behalf-oss/behave2cucumber (not installed yet)") ++ print("REQUIRE: https://github.com/behalf-oss/behave2cucumber") + print("INSTALL: pip install behave2cucumber") + sys.exit(2) + diff --git a/meta-python/recipes-devtools/python/python3-behave/0059-Fixed-a-bug-where-use_fixture_by_tag-didn-t-return-t.patch b/meta-python/recipes-devtools/python/python3-behave/0059-Fixed-a-bug-where-use_fixture_by_tag-didn-t-return-t.patch new file mode 100644 index 000000000..ff0a4238f --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0059-Fixed-a-bug-where-use_fixture_by_tag-didn-t-return-t.patch @@ -0,0 +1,23 @@ +From c507804a610e09aaabe7c5da7665857ce7947736 Mon Sep 17 00:00:00 2001 +From: Jon-Pierre Gentil +Date: Thu, 1 Aug 2019 11:21:36 -0500 +Subject: [PATCH] Fixed a bug where use_fixture_by_tag didn't return the actual + fixture if the registry entry was just a direct function. + +--- + behave/fixture.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/behave/fixture.py b/behave/fixture.py +index 3a9f1bc..51e18bf 100644 +--- a/behave/fixture.py ++++ b/behave/fixture.py +@@ -272,7 +272,7 @@ def use_fixture_by_tag(tag, context, fixture_registry): + + if callable(fixture_data): + fixture_func = fixture_data +- use_fixture(fixture_func, context) ++ return use_fixture(fixture_func, context) + elif isinstance(fixture_data, (tuple, list)): + assert len(fixture_data) == 3 + fixture_func, fixture_args, fixture_kwargs = fixture_data diff --git a/meta-python/recipes-devtools/python/python3-behave/0060-Added-issue-unit-test.patch b/meta-python/recipes-devtools/python/python3-behave/0060-Added-issue-unit-test.patch new file mode 100644 index 000000000..b35ba1448 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0060-Added-issue-unit-test.patch @@ -0,0 +1,62 @@ +From ee4784efbbf1cbd18390e2dd36f61dee1b003858 Mon Sep 17 00:00:00 2001 +From: Jon-Pierre Gentil +Date: Thu, 1 Aug 2019 11:48:08 -0500 +Subject: [PATCH] Added issue unit test + +--- + tests/issues/test_issue0767.py | 46 ++++++++++++++++++++++++++++++++++ + 1 file changed, 46 insertions(+) + create mode 100644 tests/issues/test_issue0767.py + +diff --git a/tests/issues/test_issue0767.py b/tests/issues/test_issue0767.py +new file mode 100644 +index 0000000..1de3589 +--- /dev/null ++++ b/tests/issues/test_issue0767.py +@@ -0,0 +1,46 @@ ++""" ++https://github.com/behave/behave/issues/767 ++ ++When trying to do something like:: ++ ++ fixture_registry = {'fixture.foo': foo_fixture} ++ f = use_fixture_by_tag('fixture.foo', context, fixture_registry) ++ ++Behave returns nothing. :: ++ ++ repr(f) ++ 'None' ++ ++This seems to be an oversight. ++""" ++ ++from mock import Mock ++ ++def test_issue_767_use_feature_by_tag_has_no_return(): ++ """Verifies that issue #767 is fixed.""" ++ from behave.fixture import fixture, use_fixture_by_tag ++ from behave.runner import Context ++ ++ @fixture(name='fixture.foo') ++ def foo_fixture(context, *args, **kwargs): ++ context.foo = 'foo' ++ return context.foo ++ ++ # -- SCHEMA 1: fixture_func ++ fixture_registry1 = { ++ "fixture.foo": foo_fixture ++ } ++ # -- SCHEMA 2: fixture_func, fixture_args, fixture_kwargs ++ fixture_registry2 = { ++ "fixture.foo": (foo_fixture, (), {}) ++ } ++ ++ context = Context(runner=Mock()) ++ f1 = use_fixture_by_tag('fixture.foo', context, fixture_registry1) ++ assert f1 == 'foo' ++ assert context.foo is f1 ++ ++ context = Context(runner=Mock()) ++ f2 = use_fixture_by_tag('fixture.foo', context, fixture_registry2) ++ assert f2 == 'foo' ++ assert context.foo is f2 diff --git a/meta-python/recipes-devtools/python/python3-behave/0061-Merge-pull-request-767-with-minor-tweaks.patch b/meta-python/recipes-devtools/python/python3-behave/0061-Merge-pull-request-767-with-minor-tweaks.patch new file mode 100644 index 000000000..b59ebabe1 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0061-Merge-pull-request-767-with-minor-tweaks.patch @@ -0,0 +1,60 @@ +From 2c3c2a4217d033c3f7157046765e0888bde3bfbd Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Wed, 7 Aug 2019 23:14:59 +0200 +Subject: [PATCH] Merge pull-request #767 with minor tweaks. FIX: + use_fixture_by_tag didn't return the actual fixture in all cases. + +--- + CHANGES.rst | 1 + + tests/issues/test_issue0767.py | 17 +++++++++-------- + 2 files changed, 10 insertions(+), 8 deletions(-) + +diff --git a/CHANGES.rst b/CHANGES.rst +index 15a4ef9..7a9163f 100644 +--- a/CHANGES.rst ++++ b/CHANGES.rst +@@ -43,6 +43,7 @@ FIXED: + + MINOR: + ++* pull #767: FIX: use_fixture_by_tag didn't return the actual fixture in all cases (provided by: jgentil) + * pull #751: gherkin: Adding Rule keyword translation in portuguese and spanish to gherkin-languages.json (provided by: dunossauro) + * pull #660: Fix minor typos (provided by: rrueth) + * pull #655: Use pytest instead of py.test per upstream recommendation (provided by: scop) +diff --git a/tests/issues/test_issue0767.py b/tests/issues/test_issue0767.py +index 1de3589..401dbd0 100644 +--- a/tests/issues/test_issue0767.py ++++ b/tests/issues/test_issue0767.py +@@ -15,11 +15,12 @@ This seems to be an oversight. + """ + + from mock import Mock ++from behave.fixture import fixture, use_fixture_by_tag ++from behave.runner import Context ++ + + def test_issue_767_use_feature_by_tag_has_no_return(): + """Verifies that issue #767 is fixed.""" +- from behave.fixture import fixture, use_fixture_by_tag +- from behave.runner import Context + + @fixture(name='fixture.foo') + def foo_fixture(context, *args, **kwargs): +@@ -36,11 +37,11 @@ def test_issue_767_use_feature_by_tag_has_no_return(): + } + + context = Context(runner=Mock()) +- f1 = use_fixture_by_tag('fixture.foo', context, fixture_registry1) +- assert f1 == 'foo' +- assert context.foo is f1 ++ fixture1 = use_fixture_by_tag("fixture.foo", context, fixture_registry1) ++ assert fixture1 == "foo" ++ assert context.foo is fixture1 + + context = Context(runner=Mock()) +- f2 = use_fixture_by_tag('fixture.foo', context, fixture_registry2) +- assert f2 == 'foo' +- assert context.foo is f2 ++ fixture2 = use_fixture_by_tag("fixture.foo", context, fixture_registry2) ++ assert fixture2 == "foo" ++ assert context.foo is fixture2 diff --git a/meta-python/recipes-devtools/python/python3-behave/0062-CHECK-Issue-766-PrettyFormatter-UnicodeError.patch b/meta-python/recipes-devtools/python/python3-behave/0062-CHECK-Issue-766-PrettyFormatter-UnicodeError.patch new file mode 100644 index 000000000..0b8f63408 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0062-CHECK-Issue-766-PrettyFormatter-UnicodeError.patch @@ -0,0 +1,83 @@ +From 2034f2d33b24d43c9f0c120ace578ca53656fab6 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Wed, 7 Aug 2019 23:55:05 +0200 +Subject: [PATCH] CHECK: Issue #766 -- PrettyFormatter: UnicodeError + +--- + issue.features/issue0766.feature | 47 +++++++++++++++++++++++++ + issue.features/steps/issue0766_steps.py | 12 +++++++ + 2 files changed, 59 insertions(+) + create mode 100644 issue.features/issue0766.feature + create mode 100644 issue.features/steps/issue0766_steps.py + +diff --git a/issue.features/issue0766.feature b/issue.features/issue0766.feature +new file mode 100644 +index 0000000..94d44d8 +--- /dev/null ++++ b/issue.features/issue0766.feature +@@ -0,0 +1,47 @@ ++@issue ++@not_reproducible ++Feature: Issue #766 -- UnicodeEncodeError in PrettyFormatter ++ ++ Explore the described problem. ++ ++ Scenario Outline: ++ Given a step with name="" ++ ++ Examples: ++ | name | value | comment | ++ | 😄 | 123 | Use emoticon (smiley) in a table | ++ ++ Scenario: ++ Given a step with table data: ++ | name | value | comment | ++ | 😄 | 123 | Use emoticon (smiley) in a table | ++ ++ Scenario: Explore problem by using the pretty formatter ++ Given a new working directory ++ And a file named "features/syndrome_766.feature" with: ++ """ ++ Feature: Alice ++ Scenario Outline: ++ Given a step with name="" ++ ++ Examples: ++ | name | value | comment | ++ | 😄 | 123 | Use emoticon (smiley) in a table | ++ """ ++ And a file named "features/steps/issue766_steps.py" with: ++ """ ++ from behave import given ++ ++ @given(u'a step with name="{name}"') ++ def step_with_table_data(ctx, name): ++ pass ++ """ ++ When I run "behave -f pretty features/syndrome_766.feature" ++ Then it should pass with: ++ """ ++ 1 feature passed, 0 failed, 0 skipped ++ 1 scenario passed, 0 failed, 0 skipped ++ 1 step passed, 0 failed, 0 skipped, 0 undefine ++ """ ++ And the command output should not contain "UnicodeEncodeError" ++ And the command output should not contain "Traceback" +diff --git a/issue.features/steps/issue0766_steps.py b/issue.features/steps/issue0766_steps.py +new file mode 100644 +index 0000000..33ed317 +--- /dev/null ++++ b/issue.features/steps/issue0766_steps.py +@@ -0,0 +1,12 @@ ++# -*- coding: UTF-8 -*- ++ ++from __future__ import print_function ++from behave import given ++ ++@given(u'a step with table data') ++def step_with_table_data(ctx): ++ assert ctx.table is not None, "REQUIRE: step.table" ++ ++@given(u'a step with name="{name}"') ++def step_with_table_data(ctx, name): ++ print(u"name: {}".format(name)) diff --git a/meta-python/recipes-devtools/python/python3-behave/0063-FIX-issue-772-ScenarioOutline.Examples-without-table.patch b/meta-python/recipes-devtools/python/python3-behave/0063-FIX-issue-772-ScenarioOutline.Examples-without-table.patch new file mode 100644 index 000000000..ffa265eed --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0063-FIX-issue-772-ScenarioOutline.Examples-without-table.patch @@ -0,0 +1,74 @@ +From 82065e999bfc125461c2e3db17856bf5a1384443 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sun, 22 Sep 2019 18:08:22 +0200 +Subject: [PATCH] FIX issue #772: ScenarioOutline.Examples without table. + +--- + behave/model.py | 8 +++++++- + issue.features/issue0772.feature | 31 +++++++++++++++++++++++++++++++ + 2 files changed, 38 insertions(+), 1 deletion(-) + create mode 100644 issue.features/issue0772.feature + +diff --git a/behave/model.py b/behave/model.py +index 69f38ab..f46d2c2 100644 +--- a/behave/model.py ++++ b/behave/model.py +@@ -10,7 +10,7 @@ This module provides the model element class that represent a behave model: + * ... + """ + +-from __future__ import absolute_import, with_statement ++from __future__ import absolute_import, with_statement, print_function + import copy + import difflib + import logging +@@ -1355,6 +1355,12 @@ class ScenarioOutlineBuilder(object): + example.index = example_index+1 + params["examples.name"] = example.name + params["examples.index"] = _text(example.index) ++ if not example.table: ++ # -- SYNDROME: Examples keyword without table ++ print("ERROR: ScenarioOutline.Examples: Has NO-TABLE syndrome ({0})"\ ++ .format(example.location)) ++ continue ++ + for row_index, row in enumerate(example.table): + row.index = row_index+1 + row.id = "%d.%d" % (example.index, row.index) +diff --git a/issue.features/issue0772.feature b/issue.features/issue0772.feature +new file mode 100644 +index 0000000..eba0ea5 +--- /dev/null ++++ b/issue.features/issue0772.feature +@@ -0,0 +1,31 @@ ++@issue ++Feature: Issue #772 -- Syndrome: ScenarioOutline with Examples keyword w/o Table ++ ++ ++ ++ Background: Setup ++ Given a new working directory ++ And a file named "features/syndrome_772.feature" with: ++ """ ++ Feature: Examples without table ++ ++ Scenario Outline: ++ Given a step passes ++ When another step passes ++ ++ Examples: Without table ++ """ ++ And a file named "features/steps/use_step_library.py" with: ++ """ ++ # -- REUSE STEPS: ++ import behave4cmd0.passing_steps ++ """ ++ ++ Scenario: Use ScenarioOutline with Examples keyword without table ++ When I run "behave -f plain features/syndrome_772.feature" ++ Then it should pass with: ++ """ ++ Feature: Examples without table ++ ERROR: ScenarioOutline.Examples: Has NO-TABLE syndrome (features/syndrome_772.feature:7) ++ """ ++ And the command output should not contain "Parser failure in state" diff --git a/meta-python/recipes-devtools/python/python3-behave/0064-FIX-issue-772-ScenarioOutline.Examples-without-table.patch b/meta-python/recipes-devtools/python/python3-behave/0064-FIX-issue-772-ScenarioOutline.Examples-without-table.patch new file mode 100644 index 000000000..243cb82d0 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0064-FIX-issue-772-ScenarioOutline.Examples-without-table.patch @@ -0,0 +1,21 @@ +From 1d45845ffb9a30ca66806ba165bea54f7d43cd46 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sun, 22 Sep 2019 18:09:50 +0200 +Subject: [PATCH] FIX issue #772: ScenarioOutline.Examples without table. + +--- + CHANGES.rst | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/CHANGES.rst b/CHANGES.rst +index 7a9163f..ba4daad 100644 +--- a/CHANGES.rst ++++ b/CHANGES.rst +@@ -33,6 +33,7 @@ ENHANCEMENTS: + + FIXED: + ++* 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) + * issue #713: Background section doesn't support description (provided by: dgou) diff --git a/meta-python/recipes-devtools/python/python3-behave/0065-Nibble-TravisCI-to-wake-up.patch b/meta-python/recipes-devtools/python/python3-behave/0065-Nibble-TravisCI-to-wake-up.patch new file mode 100644 index 000000000..683ed9275 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0065-Nibble-TravisCI-to-wake-up.patch @@ -0,0 +1,21 @@ +From 5c181e07ca0fda2ea26cf8c8949dd8baaa2be953 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Wed, 11 Dec 2019 08:23:08 +0100 +Subject: [PATCH] Nibble TravisCI to wake up. + +--- + .travis.yml | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/.travis.yml b/.travis.yml +index c6027e0..781a610 100644 +--- a/.travis.yml ++++ b/.travis.yml +@@ -6,6 +6,7 @@ python: + - "3.7" + - "2.7" + ++ + # -- DISABLE-TEMPORARILY: Ensure faster builds + # - "3.6" + # - "3.5" diff --git a/meta-python/recipes-devtools/python/python3-behave/0066-Tweak-pytest-version-selection.patch b/meta-python/recipes-devtools/python/python3-behave/0066-Tweak-pytest-version-selection.patch new file mode 100644 index 000000000..143c178d0 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0066-Tweak-pytest-version-selection.patch @@ -0,0 +1,37 @@ +From ba6f76873dadead64be57c215ba2f7eb40d0ea6d Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Wed, 11 Dec 2019 08:30:11 +0100 +Subject: [PATCH] Tweak pytest version selection + +--- + py.requirements/ci.travis.txt | 3 ++- + setup.py | 3 ++- + 2 files changed, 4 insertions(+), 2 deletions(-) + +diff --git a/py.requirements/ci.travis.txt b/py.requirements/ci.travis.txt +index 73d65f6..5f31802 100644 +--- a/py.requirements/ci.travis.txt ++++ b/py.requirements/ci.travis.txt +@@ -1,6 +1,7 @@ + mock + PyHamcrest >= 1.9 +-pytest >= 3.0 ++pytest < 5.0; python_version < '3.0' ++pytest >= 5.0; python_version >= '3.0' + pytest-html >= 1.19.0 + + # -- NEEDED: By some tests (as proof of concept) +diff --git a/setup.py b/setup.py +index 8de3ec0..75d6847 100644 +--- a/setup.py ++++ b/setup.py +@@ -87,7 +87,8 @@ setup( + "colorama", + ], + tests_require=[ +- "pytest >= 4.2", ++ "pytest < 5.0; python_version < '3.0'", # >= 4.2 ++ "pytest >= 5.0; python_version >= '3.0'", + "pytest-html >= 1.19.0", + "mock >= 1.1", + "PyHamcrest >= 1.9", diff --git a/meta-python/recipes-devtools/python/python3-behave/0067-Tweak-pytest-configuration-to-silence-JUnit-XML-dial.patch b/meta-python/recipes-devtools/python/python3-behave/0067-Tweak-pytest-configuration-to-silence-JUnit-XML-dial.patch new file mode 100644 index 000000000..f0e642634 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0067-Tweak-pytest-configuration-to-silence-JUnit-XML-dial.patch @@ -0,0 +1,37 @@ +From 7bd632e11a5ab75cec3e20722cf8fa3cebb1983f Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Wed, 11 Dec 2019 08:37:11 +0100 +Subject: [PATCH] Tweak pytest configuration to silence JUnit XML dialect + warning. + +--- + pytest.ini | 7 ++++++- + 1 file changed, 6 insertions(+), 1 deletion(-) + +diff --git a/pytest.ini b/pytest.ini +index ff2a8a2..228279c 100644 +--- a/pytest.ini ++++ b/pytest.ini +@@ -16,9 +16,10 @@ + # ============================================================================ + + [pytest] +-minversion = 2.8 ++minversion = 4.2 + testpaths = tests + python_files = test_*.py ++junit_family = xunit2 + addopts = --metadata PACKAGE_UNDER_TEST behave + --metadata PACKAGE_VERSION 1.2.7.dev1 + --html=build/testing/report.html --self-contained-html +@@ -27,6 +28,10 @@ markers = + smoke + slow + ++# -- PREPARED: ++# filterwarnings = ++# ignore:.*invalid escape sequence.*:DeprecationWarning ++ + # -- BACKWARD COMPATIBILITY: pytest < 2.8 + # norecursedirs = .git .tox build dist py.requirements tmp* _WORKSPACE + diff --git a/meta-python/recipes-devtools/python/python3-behave/0068-Add-basic-feature-test-for-wildcard-pattern-matching.patch b/meta-python/recipes-devtools/python/python3-behave/0068-Add-basic-feature-test-for-wildcard-pattern-matching.patch new file mode 100644 index 000000000..97db40580 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0068-Add-basic-feature-test-for-wildcard-pattern-matching.patch @@ -0,0 +1,56 @@ +From 56c4ee34b403d69ed3c7092edee95ae6f3b6fd74 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sun, 15 Dec 2019 15:50:37 +0100 +Subject: [PATCH] Add basic feature-test for wildcard pattern-matching that is + supported by cucumber-tag-expressions (python-only). + +--- + .../tags.tag_expression_v2.wildcards.feature | 39 +++++++++++++++++++ + 1 file changed, 39 insertions(+) + create mode 100644 features/tags.tag_expression_v2.wildcards.feature + +diff --git a/features/tags.tag_expression_v2.wildcards.feature b/features/tags.tag_expression_v2.wildcards.feature +new file mode 100644 +index 0000000..372a731 +--- /dev/null ++++ b/features/tags.tag_expression_v2.wildcards.feature +@@ -0,0 +1,39 @@ ++Feature: Tag Expression v2 Extension: Wildcards for tag matching ++ ++ As a tester ++ I want to use a wildcard pattern to select tags following a naming scheme ++ So that it is simpler to select a subset of scenarios and features. ++ ++ . SPECIFICATION: Wildcards in tag-expressions v2 ++ . * Use file-name matching wildcards (fnmatch): *, ? ++ . * a tag expression is a boolean expression ++ . * a tag expression supports the operators: and, or, not ++ . * a tag expression supports '(' and ')' for grouping expressions ++ . ++ . EXAMPLES: ++ . | Tag expression | Comment | ++ . | @foo.* | Matches any tags that start with "@foo." | ++ . | not @foo.* | Excludes any element that have tags that start with "@foo." | ++ ++ ++ Scenario: Select tags that match the "@foo.*" pattern ++ Given the tag expression "@foo.*" ++ Then the tag expression selects elements with tags: ++ | tags | selected? | ++ | | no | ++ | @foo | no | ++ | @foo.one | yes | ++ | @foo.two | yes | ++ | @other | no | ++ | @foo.3 @other | yes | ++ ++ Scenario: Select tags that do not match the "@foo.*" pattern ++ Given the tag expression "not @foo.*" ++ Then the tag expression selects elements with tags: ++ | tags | selected? | ++ | | yes | ++ | @foo | yes | ++ | @foo.one | no | ++ | @foo.two | no | ++ | @other | yes | ++ | @foo.3 @other | no | diff --git a/meta-python/recipes-devtools/python/python3-behave/0069-UPDATE-dependencies-path.py-path-pytest.patch b/meta-python/recipes-devtools/python/python3-behave/0069-UPDATE-dependencies-path.py-path-pytest.patch new file mode 100644 index 000000000..1cdf70944 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0069-UPDATE-dependencies-path.py-path-pytest.patch @@ -0,0 +1,141 @@ +From ff1f1e74f5f81eb634d72d472ce18f8d543bf3c0 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sun, 15 Dec 2019 20:12:14 +0100 +Subject: [PATCH] UPDATE: dependencies (path.py <=> path, pytest, ...) + +--- + py.requirements/ci.tox.txt | 8 ++++++-- + py.requirements/ci.travis.txt | 11 ++++++++--- + py.requirements/develop.txt | 5 ++++- + py.requirements/testing.txt | 7 +++++-- + setup.py | 6 ++++-- + tasks/py.requirements.txt | 5 ++++- + 6 files changed, 31 insertions(+), 11 deletions(-) + +diff --git a/py.requirements/ci.tox.txt b/py.requirements/ci.tox.txt +index 6b3b3ae..4001bc2 100644 +--- a/py.requirements/ci.tox.txt ++++ b/py.requirements/ci.tox.txt +@@ -2,8 +2,12 @@ + # BEHAVE: PYTHON PACKAGE REQUIREMENTS: ci.tox.txt + # ============================================================================ + +-pytest >= 4.2 ++pytest < 5.0; python_version < '3.0' # pytest >= 4.2 ++pytest >= 5.0; python_version >= '3.0' + pytest-html >= 1.19.0 + mock >= 2.0 + PyHamcrest >= 1.9 +-path.py >= 10.1 ++ ++# -- HINT: path.py => path (python-install-package was renamed for python3) ++path.py >= 11.5.0; python_version < '3.5' ++path >= 13.1.0; python_version >= '3.5' +diff --git a/py.requirements/ci.travis.txt b/py.requirements/ci.travis.txt +index 5f31802..de4120a 100644 +--- a/py.requirements/ci.travis.txt ++++ b/py.requirements/ci.travis.txt +@@ -1,12 +1,17 @@ +-mock +-PyHamcrest >= 1.9 ++# ============================================================================ ++# PYTHON PACKAGE REQUIREMENTS FOR: behave -- ci.travis.txt ++# ============================================================================ + pytest < 5.0; python_version < '3.0' + pytest >= 5.0; python_version >= '3.0' + pytest-html >= 1.19.0 ++mock >= 2.0 ++PyHamcrest >= 1.9 + + # -- NEEDED: By some tests (as proof of concept) + # NOTE: path.py-10.1 is required for python2.6 +-path.py >= 10.1 ++# HINT: path.py => path (python-install-package was renamed for python3) ++path.py >= 11.5.0; python_version < '3.5' ++path >= 13.1.0; python_version >= '3.5' + + # -- NOTE: Travis.CI tweak related w/ invalid linecache2 tests. + # This problem does not exist if you use pip. +diff --git a/py.requirements/develop.txt b/py.requirements/develop.txt +index a16d7bf..a92ee5f 100644 +--- a/py.requirements/develop.txt ++++ b/py.requirements/develop.txt +@@ -6,10 +6,13 @@ + # PREPARE USAGE: invoke + # ALREADY: six >= 1.11.0 + invoke >= 1.2.0 +-path.py >= 11.5.0 + pathlib; python_version <= '3.4' + pycmd + ++# -- HINT: path.py => path (python-install-package was renamed for python3) ++path.py >= 11.5.0; python_version < '3.5' ++path >= 13.1.0; python_version >= '3.5' ++ + # -- CONFIGURATION MANAGEMENT (helpers): + # FORMER: bumpversion >= 0.4.0 + bump2version >= 0.5.6 +diff --git a/py.requirements/testing.txt b/py.requirements/testing.txt +index a418739..85b0908 100644 +--- a/py.requirements/testing.txt ++++ b/py.requirements/testing.txt +@@ -4,11 +4,14 @@ + + # -- TESTING: Unit tests and behave self-tests. + # PREPARED-FUTURE: behave4cmd0, behave4cmd +-pytest >= 4.2 ++pytest < 5.0; python_version < '3.0' # pytest >= 4.2 ++pytest >= 5.0; python_version >= '3.0' + pytest-html >= 1.19.0 + mock >= 2.0 + PyHamcrest >= 1.9 + + # -- NEEDED: By some tests (as proof of concept) + # NOTE: path.py-10.1 is required for python2.6 +-path.py >= 11.5.0 ++# HINT: path.py => path (python-install-package was renamed for python3) ++path.py >= 11.5.0; python_version < '3.5' ++path >= 13.1.0; python_version >= '3.5' +diff --git a/setup.py b/setup.py +index 75d6847..2afc147 100644 +--- a/setup.py ++++ b/setup.py +@@ -77,7 +77,7 @@ setup( + python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*", + install_requires=[ + "cucumber-tag-expressions >= 1.1.2", +- "parse >= 1.8.2", ++ "parse >= 1.9.1", + "parse_type >= 0.4.2", + "six >= 1.12.0", + "traceback2; python_version < '3.0'", +@@ -92,7 +92,9 @@ setup( + "pytest-html >= 1.19.0", + "mock >= 1.1", + "PyHamcrest >= 1.9", +- "path.py >= 11.5.0" ++ # -- HINT: path.py => path (python-install-package was renamed for python3) ++ "path.py >= 11.5.0; python_version < '3.5'", ++ "path >= 13.1.0; python_version >= '3.5'", + ], + cmdclass = { + "behave_test": behave_test, +diff --git a/tasks/py.requirements.txt b/tasks/py.requirements.txt +index a77d3bc..810f834 100644 +--- a/tasks/py.requirements.txt ++++ b/tasks/py.requirements.txt +@@ -9,10 +9,13 @@ + # ============================================================================ + + invoke >= 1.2.0 +-path.py >= 11.5.0 + pycmd + six >= 1.12.0 + ++# -- HINT: path.py => path (python-install-package was renamed for python3) ++path.py >= 11.5.0; python_version < '3.5' ++path >= 13.1.0; python_version >= '3.5' ++ + # -- PYTHON2 BACKPORTS: + pathlib; python_version <= '3.4' + backports.shutil_which; python_version <= '3.3' diff --git a/meta-python/recipes-devtools/python/python3-behave/0070-pytest-Disable-DeprecatedWarning-from-distutils-pack.patch b/meta-python/recipes-devtools/python/python3-behave/0070-pytest-Disable-DeprecatedWarning-from-distutils-pack.patch new file mode 100644 index 000000000..df5b0d372 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0070-pytest-Disable-DeprecatedWarning-from-distutils-pack.patch @@ -0,0 +1,25 @@ +From 451f88ab5d1ea7da9737ef701a4309963516f3c4 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sun, 15 Dec 2019 20:16:43 +0100 +Subject: [PATCH] pytest: Disable DeprecatedWarning from distutils package. + +--- + pytest.ini | 5 +++-- + 1 file changed, 3 insertions(+), 2 deletions(-) + +diff --git a/pytest.ini b/pytest.ini +index 228279c..b9f281a 100644 +--- a/pytest.ini ++++ b/pytest.ini +@@ -29,8 +29,9 @@ markers = + slow + + # -- PREPARED: +-# filterwarnings = +-# ignore:.*invalid escape sequence.*:DeprecationWarning ++filterwarnings = ++ ignore:.*the imp module is deprecated in favour of importlib.*:DeprecationWarning ++# ignore:.*invalid escape sequence.*:DeprecationWarning + + # -- BACKWARD COMPATIBILITY: pytest < 2.8 + # norecursedirs = .git .tox build dist py.requirements tmp* _WORKSPACE diff --git a/meta-python/recipes-devtools/python/python3-behave/0071-CLEANUP-Add-ContextMode-enum-related-to-797.patch b/meta-python/recipes-devtools/python/python3-behave/0071-CLEANUP-Add-ContextMode-enum-related-to-797.patch new file mode 100644 index 000000000..122a63af8 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0071-CLEANUP-Add-ContextMode-enum-related-to-797.patch @@ -0,0 +1,216 @@ +From 37fd19b56ba31634a34349d956120b6a5898e2d9 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sun, 15 Dec 2019 21:20:18 +0100 +Subject: [PATCH] CLEANUP: Add ContextMode enum related to #797 + +Add ContextMode enum to cleanup weirdness related to issue #797. +Pre-existing Context.BEHAVE/USER constants overshadowed user attributes +in Context.attribute retrieval case. +--- + behave/runner.py | 33 ++++++++++++++++++++++----------- + tests/unit/test_runner.py | 29 +++++++++++++++-------------- + 2 files changed, 37 insertions(+), 25 deletions(-) + +diff --git a/behave/runner.py b/behave/runner.py +index cbedb5a..bcf4ab2 100644 +--- a/behave/runner.py ++++ b/behave/runner.py +@@ -21,6 +21,7 @@ from behave.runner_util import \ + collect_feature_locations, parse_features, \ + exec_file, load_step_modules, PathManager + from behave.step_registry import registry as the_step_registry ++from enum import Enum + + if six.PY2: + # -- USE PYTHON3 BACKPORT: With unicode traceback support. +@@ -45,6 +46,16 @@ class ContextMaskWarning(UserWarning): + pass + + ++class ContextMode(Enum): ++ """Used to distinguish between the two usage modes while using the context: ++ ++ * BEHAVE: Indicates "behave" (internal) mode ++ * USER: Indicates "user" mode (in steps, hooks, fixtures, ...) ++ """ ++ BEHAVE = 1 ++ USER = 2 ++ ++ + class Context(object): + """Hold contextual information during the running of tests. + +@@ -147,8 +158,8 @@ class Context(object): + .. _`configuration file section names`: behave.html#configuration-files + """ + # pylint: disable=too-many-instance-attributes +- BEHAVE = "behave" +- USER = "user" ++ # BEHAVE = "behave" ++ # USER = "user" + FAIL_ON_CLEANUP_ERRORS = True + + def __init__(self, runner): +@@ -166,7 +177,7 @@ class Context(object): + self._stack = [d] + self._record = {} + self._origin = {} +- self._mode = self.BEHAVE ++ self._mode = ContextMode.BEHAVE + + # -- MODEL ENTITY REFERENCES/SUPPORT: + self.feature = None +@@ -260,11 +271,11 @@ class Context(object): + + def _use_with_behave_mode(self): + """Provides a context manager for using the context in BEHAVE mode.""" +- return use_context_with_mode(self, Context.BEHAVE) ++ return use_context_with_mode(self, ContextMode.BEHAVE) + + def use_with_user_mode(self): + """Provides a context manager for using the context in USER mode.""" +- return use_context_with_mode(self, Context.USER) ++ return use_context_with_mode(self, ContextMode.USER) + + def user_mode(self): + warnings.warn("Use 'use_with_user_mode()' instead", +@@ -291,11 +302,11 @@ class Context(object): + + def _emit_warning(self, attr, params): + msg = "" +- if self._mode is self.BEHAVE and self._origin[attr] is not self.BEHAVE: ++ if self._mode is ContextMode.BEHAVE and self._origin[attr] is not ContextMode.BEHAVE: + msg = "behave runner is masking context attribute '%(attr)s' " \ + "originally set in %(function)s (%(filename)s:%(line)s)" +- elif self._mode is self.USER: +- if self._origin[attr] is not self.USER: ++ elif self._mode is ContextMode.USER: ++ if self._origin[attr] is not ContextMode.USER: + msg = "user code is masking context attribute '%(attr)s' " \ + "originally set by behave" + elif self._config.verbose: +@@ -442,13 +453,13 @@ class Context(object): + + @contextlib.contextmanager + def use_context_with_mode(context, mode): +- """Switch context to BEHAVE or USER mode. ++ """Switch context to ContextMode.BEHAVE or ContextMode.USER mode. + Provides a context manager for switching between the two context modes. + + .. sourcecode:: python + + context = Context() +- with use_context_with_mode(context, Context.BEHAVE): ++ with use_context_with_mode(context, ContextMode.BEHAVE): + ... # Do something + # -- POSTCONDITION: Original context._mode is restored. + +@@ -456,7 +467,7 @@ def use_context_with_mode(context, mode): + :param mode: Mode to apply to context object. + """ + # pylint: disable=protected-access +- assert mode in (Context.BEHAVE, Context.USER) ++ assert mode in (ContextMode.BEHAVE, ContextMode.USER) + current_mode = context._mode + try: + context._mode = mode +diff --git a/tests/unit/test_runner.py b/tests/unit/test_runner.py +index f0d03cd..beaff8f 100644 +--- a/tests/unit/test_runner.py ++++ b/tests/unit/test_runner.py +@@ -17,6 +17,7 @@ from behave import runner_util + from behave.model import Table + from behave.step_registry import StepRegistry + from behave import parser, runner ++from behave.runner import ContextMode + from behave.exception import ConfigError + from behave.formatter.base import StreamOpener + +@@ -36,29 +37,29 @@ class TestContext(unittest.TestCase): + + def test_user_mode_shall_restore_behave_mode(self): + # -- CASE: No exception is raised. +- initial_mode = runner.Context.BEHAVE ++ initial_mode = ContextMode.BEHAVE + assert self.context._mode == initial_mode + with self.context.use_with_user_mode(): +- assert self.context._mode == runner.Context.USER ++ assert self.context._mode == ContextMode.USER + self.context.thing = "stuff" + assert self.context._mode == initial_mode + + def test_user_mode_shall_restore_behave_mode_if_assert_fails(self): +- initial_mode = runner.Context.BEHAVE ++ initial_mode = ContextMode.BEHAVE + assert self.context._mode == initial_mode + try: + with self.context.use_with_user_mode(): +- assert self.context._mode == runner.Context.USER ++ assert self.context._mode == ContextMode.USER + assert False, "XFAIL" + except AssertionError: + assert self.context._mode == initial_mode + + def test_user_mode_shall_restore_behave_mode_if_exception_is_raised(self): +- initial_mode = runner.Context.BEHAVE ++ initial_mode = ContextMode.BEHAVE + assert self.context._mode == initial_mode + try: + with self.context.use_with_user_mode(): +- assert self.context._mode == runner.Context.USER ++ assert self.context._mode == ContextMode.USER + raise RuntimeError("XFAIL") + except RuntimeError: + assert self.context._mode == initial_mode +@@ -66,21 +67,21 @@ class TestContext(unittest.TestCase): + def test_use_with_user_mode__shall_restore_initial_mode(self): + # -- CASE: No exception is raised. + # pylint: disable=protected-access +- initial_mode = runner.Context.BEHAVE ++ initial_mode = ContextMode.BEHAVE + self.context._mode = initial_mode + with self.context.use_with_user_mode(): +- assert self.context._mode == runner.Context.USER ++ assert self.context._mode == ContextMode.USER + self.context.thing = "stuff" + assert self.context._mode == initial_mode + + def test_use_with_user_mode__shall_restore_initial_mode_with_error(self): + # -- CASE: Exception is raised. + # pylint: disable=protected-access +- initial_mode = runner.Context.BEHAVE ++ initial_mode = ContextMode.BEHAVE + self.context._mode = initial_mode + try: + with self.context.use_with_user_mode(): +- assert self.context._mode == runner.Context.USER ++ assert self.context._mode == ContextMode.USER + raise RuntimeError("XFAIL") + except RuntimeError: + assert self.context._mode == initial_mode +@@ -88,21 +89,21 @@ class TestContext(unittest.TestCase): + def test_use_with_behave_mode__shall_restore_initial_mode(self): + # -- CASE: No exception is raised. + # pylint: disable=protected-access +- initial_mode = runner.Context.USER ++ initial_mode = ContextMode.USER + self.context._mode = initial_mode + with self.context._use_with_behave_mode(): +- assert self.context._mode == runner.Context.BEHAVE ++ assert self.context._mode == ContextMode.BEHAVE + self.context.thing = "stuff" + assert self.context._mode == initial_mode + + def test_use_with_behave_mode__shall_restore_initial_mode_with_error(self): + # -- CASE: Exception is raised. + # pylint: disable=protected-access +- initial_mode = runner.Context.USER ++ initial_mode = ContextMode.USER + self.context._mode = initial_mode + try: + with self.context._use_with_behave_mode(): +- assert self.context._mode == runner.Context.BEHAVE ++ assert self.context._mode == ContextMode.BEHAVE + raise RuntimeError("XFAIL") + except RuntimeError: + assert self.context._mode == initial_mode diff --git a/meta-python/recipes-devtools/python/python3-behave/0072-Cleanup-comments.patch b/meta-python/recipes-devtools/python/python3-behave/0072-Cleanup-comments.patch new file mode 100644 index 000000000..9b0e77d1b --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0072-Cleanup-comments.patch @@ -0,0 +1,22 @@ +From 1c3eab0fed1d0eb33da64eb568e42971822655df Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sun, 15 Dec 2019 21:25:00 +0100 +Subject: [PATCH] Cleanup comments + +--- + behave/runner.py | 2 -- + 1 file changed, 2 deletions(-) + +diff --git a/behave/runner.py b/behave/runner.py +index bcf4ab2..6b20937 100644 +--- a/behave/runner.py ++++ b/behave/runner.py +@@ -158,8 +158,6 @@ class Context(object): + .. _`configuration file section names`: behave.html#configuration-files + """ + # pylint: disable=too-many-instance-attributes +- # BEHAVE = "behave" +- # USER = "user" + FAIL_ON_CLEANUP_ERRORS = True + + def __init__(self, runner): diff --git a/meta-python/recipes-devtools/python/python3-behave/0073-FIX-sphinx-build-problem-async_steps3x.py.patch b/meta-python/recipes-devtools/python/python3-behave/0073-FIX-sphinx-build-problem-async_steps3x.py.patch new file mode 100644 index 000000000..79755daa7 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0073-FIX-sphinx-build-problem-async_steps3x.py.patch @@ -0,0 +1,29 @@ +From f9977969e7fabad5644b57f53462954ed097e66e Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sun, 15 Dec 2019 21:51:47 +0100 +Subject: [PATCH] FIX: sphinx-build problem: async_steps3x.py + +--- + docs/new_and_noteworthy_v1.2.6.rst | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/docs/new_and_noteworthy_v1.2.6.rst b/docs/new_and_noteworthy_v1.2.6.rst +index 848c409..2c8e865 100644 +--- a/docs/new_and_noteworthy_v1.2.6.rst ++++ b/docs/new_and_noteworthy_v1.2.6.rst +@@ -325,13 +325,13 @@ A simple example for the implementation of the async-steps is shown for: + * Python 3.5 with new ``async``/``await`` keywords + * Python 3.4 with ``@asyncio.coroutine`` decorator and ``yield from`` keyword + +-.. literalinclude:: ../examples/async_step/features/steps/async_steps35.py ++.. literalinclude:: ../examples/async_step/features/steps/_async_steps35.py + :language: python + :prepend: + # -- FILE: features/steps/async_steps35.py + + +-.. literalinclude:: ../examples/async_step/features/steps/async_steps34.py ++.. literalinclude:: ../examples/async_step/features/steps/_async_steps34.py + :language: python + :prepend: + # -- FILE: features/steps/async_steps34.py diff --git a/meta-python/recipes-devtools/python/python3-behave/0074-docs-Rename-page-parse_expressions-was-parse_builtin.patch b/meta-python/recipes-devtools/python/python3-behave/0074-docs-Rename-page-parse_expressions-was-parse_builtin.patch new file mode 100644 index 000000000..444ed5d14 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0074-docs-Rename-page-parse_expressions-was-parse_builtin.patch @@ -0,0 +1,185 @@ +From 70943d9fae75cce204052ac0f498e603f1c7f4c4 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sun, 15 Dec 2019 21:52:53 +0100 +Subject: [PATCH] docs: Rename page 'parse_expressions' (was: + parse_builtin_types) and update table to current parse-1.12.1 + +--- + docs/appendix.rst | 2 +- + docs/parse_builtin_types.rst | 59 ------------------------ + docs/parse_expressions.rst | 87 ++++++++++++++++++++++++++++++++++++ + 3 files changed, 88 insertions(+), 60 deletions(-) + delete mode 100644 docs/parse_builtin_types.rst + create mode 100644 docs/parse_expressions.rst + +diff --git a/docs/appendix.rst b/docs/appendix.rst +index 8c0cb05..79b5455 100644 +--- a/docs/appendix.rst ++++ b/docs/appendix.rst +@@ -11,7 +11,7 @@ Appendix + + formatters + context_attributes +- parse_builtin_types ++ parse_expressions + regular_expressions + test_domains + behave_ecosystem +diff --git a/docs/parse_builtin_types.rst b/docs/parse_builtin_types.rst +deleted file mode 100644 +index 32e18ec..0000000 +--- a/docs/parse_builtin_types.rst ++++ /dev/null +@@ -1,59 +0,0 @@ +-.. _id.appendix.parse_builtin_types: +- +-Predefined Data Types in ``parse`` +-============================================================================== +- +-:pypi:`behave` uses the :pypi:`parse` module (inverse of Python `string.format`_) +-under the hoods to parse parameters in step definitions. +-This leads to rather simple and readable parse expressions for step parameters. +- +-.. code-block:: python +- +- # -- FILE: features/steps/type_transform_example_steps.py +- from behave import given +- +- @given('I have {number:d} friends') #< Convert 'number' into int type. +- def step_given_i_have_number_friends(context, number): +- assert number > 0 +- ... +- +-Therefore, the following ``parse types`` are already supported +-in step definitions without registration of any *user-defined type*: +- +- +-===== =========================================== ============ +-Type Characters Matched Output Type +-===== =========================================== ============ +- w Letters and underscore str +- W Non-letter and underscore str +- s Whitespace str +- S Non-whitespace str +- d Digits (effectively integer numbers) int +- D Non-digit str +- n Numbers with thousands separators (, or .) int +- % Percentage (converted to value/100.0) float +- f Fixed-point numbers float +- e Floating-point numbers with exponent float +- e.g. 1.1e-10, NAN (all case insensitive) +- g General number format (either d, f or e) float +- b Binary numbers int +- o Octal numbers int +- x Hexadecimal numbers (lower and upper case) int +- ti ISO 8601 format date/time datetime +- e.g. 1972-01-20T10:21:36Z +- te RFC2822 e-mail format date/time datetime +- e.g. Mon, 20 Jan 1972 10:21:36 +1000 +- tg Global (day/month) format date/time datetime +- e.g. 20/1/1972 10:21:36 AM +1:00 +- ta US (month/day) format date/time datetime +- e.g. 1/20/1972 10:21:36 PM +10:30 +- tc ctime() format date/time datetime +- e.g. Sun Sep 16 01:03:52 1973 +- th HTTP log format date/time datetime +- e.g. 21/Nov/2011:00:07:11 +0000 +- tt Time time +- e.g. 10:21:36 PM -5:30 +-===== =========================================== ============ +- +- +-.. _string.format: https://docs.python.org/3/library/string.html#format-string-syntax +diff --git a/docs/parse_expressions.rst b/docs/parse_expressions.rst +new file mode 100644 +index 0000000..36ca549 +--- /dev/null ++++ b/docs/parse_expressions.rst +@@ -0,0 +1,87 @@ ++.. _id.appendix.parse_expressions: ++ ++============================================================================== ++Parse Expressions ++============================================================================== ++ ++.. index:: parse expressions, regexp ++ ++`Parse expressions`_ are a simplified form of regular expressions. ++The actual regular expression is hidden behind the **type** name / hint. ++ ++`Parse expressions`_ are used in step definitions as a simplified alternative ++to regular expressions. They are used for parameters and type conversions ++(which are not supported for regular expression patterns). ++ ++.. code-block:: python ++ ++ # -- FILE: features/steps/example_steps.py ++ from behave import when ++ ++ @when('we implement {number:d} tests') ++ def step_impl(context, number): # -- NOTE: number is converted into integer ++ assert number > 1 or number == 0 ++ context.tests_count = number ++ ++The following tables provide a overview of the `parse expressions`_ syntax. ++See also `Python regular expressions`_ description in the Python `re module`_. ++ ++===== =========================================== ======== ++Type Characters Matched Output ++===== =========================================== ======== ++l Letters (ASCII) str ++w Letters, numbers and underscore str ++W Not letters, numbers and underscore str ++s Whitespace str ++S Non-whitespace str ++d Digits (effectively integer numbers) int ++D Non-digit str ++n Numbers with thousands separators (, or .) int ++% Percentage (converted to value/100.0) float ++f Fixed-point numbers float ++F Decimal numbers Decimal ++e Floating-point numbers with exponent float ++ e.g. 1.1e-10, NAN (all case insensitive) ++g General number format (either d, f or e) float ++b Binary numbers int ++o Octal numbers int ++x Hexadecimal numbers (lower and upper case) int ++ti ISO 8601 format date/time datetime ++ e.g. 1972-01-20T10:21:36Z ("T" and "Z" ++ optional) ++te RFC2822 e-mail format date/time datetime ++ e.g. Mon, 20 Jan 1972 10:21:36 +1000 ++tg Global (day/month) format date/time datetime ++ e.g. 20/1/1972 10:21:36 AM +1:00 ++ta US (month/day) format date/time datetime ++ e.g. 1/20/1972 10:21:36 PM +10:30 ++tc ctime() format date/time datetime ++ e.g. Sun Sep 16 01:03:52 1973 ++th HTTP log format date/time datetime ++ e.g. 21/Nov/2011:00:07:11 +0000 ++ts Linux system log format date/time datetime ++ e.g. Nov 9 03:37:44 ++tt Time time ++ e.g. 10:21:36 PM -5:30 ++===== =========================================== ======== ++ ++ ++===================== ============================================================== ++Cardinality Description ++===================== ============================================================== ++``?`` Pattern with cardinality 0..1: optional part (question mark). ++``*`` Pattern with cardinality zero or more, 0.. (asterisk). ++``+`` Pattern with cardinality one or more, 1.. (plus sign). ++``{m}`` Matches ``m`` repetitions of a pattern. ++``{m,n}`` Matches from ``m`` to ``n`` repetitions of a pattern. ++``[A-Za-z]+`` EXAMPLE: Matches one or more alphabetical characters. ++===================== ============================================================== ++ ++ ++.. _parse module: https://github.com/r1chardj0n3s/parse ++.. _string.format: https://docs.python.org/3/library/string.html#format-string-syntax ++ ++.. _re module: https://docs.python.org/3/library/re.html#module-re ++.. _Python regular expressions: https://docs.python.org/3/library/re.html#module-re ++.. _`regular expressions`: https://en.wikipedia.org/wiki/Regular_expression ++ diff --git a/meta-python/recipes-devtools/python/python3-behave/0075-docs-parse_expression-add-links-to-parse_type-module.patch b/meta-python/recipes-devtools/python/python3-behave/0075-docs-parse_expression-add-links-to-parse_type-module.patch new file mode 100644 index 000000000..6bdf151fe --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0075-docs-parse_expression-add-links-to-parse_type-module.patch @@ -0,0 +1,40 @@ +From f2704149ae1bc11b01b6be854a28f178e20d00e4 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sun, 15 Dec 2019 22:08:05 +0100 +Subject: [PATCH] docs: parse_expression, add links to parse_type module and + CardinalityField support. + +--- + docs/parse_expressions.rst | 7 ++++--- + 1 file changed, 4 insertions(+), 3 deletions(-) + +diff --git a/docs/parse_expressions.rst b/docs/parse_expressions.rst +index 36ca549..3810222 100644 +--- a/docs/parse_expressions.rst ++++ b/docs/parse_expressions.rst +@@ -65,6 +65,8 @@ tt Time time + e.g. 10:21:36 PM -5:30 + ===== =========================================== ======== + ++If `parse_type`_ module is used, the cardinality of a type can be specified, too ++(by using the `CardinalityField`_ support): + + ===================== ============================================================== + Cardinality Description +@@ -72,14 +74,13 @@ Cardinality Description + ``?`` Pattern with cardinality 0..1: optional part (question mark). + ``*`` Pattern with cardinality zero or more, 0.. (asterisk). + ``+`` Pattern with cardinality one or more, 1.. (plus sign). +-``{m}`` Matches ``m`` repetitions of a pattern. +-``{m,n}`` Matches from ``m`` to ``n`` repetitions of a pattern. +-``[A-Za-z]+`` EXAMPLE: Matches one or more alphabetical characters. + ===================== ============================================================== + + + .. _parse module: https://github.com/r1chardj0n3s/parse ++.. _parse_type: https://github.com/jenisys/parse_type + .. _string.format: https://docs.python.org/3/library/string.html#format-string-syntax ++.. _CardinalityField: https://github.com/jenisys/parse_type/blob/master/README.rst#extended-parser-with-cardinalityfield-support + + .. _re module: https://docs.python.org/3/library/re.html#module-re + .. _Python regular expressions: https://docs.python.org/3/library/re.html#module-re diff --git a/meta-python/recipes-devtools/python/python3-behave/0076-BUMP-VERSION-1.2.7.dev2-was-1.2.7.dev1.patch b/meta-python/recipes-devtools/python/python3-behave/0076-BUMP-VERSION-1.2.7.dev2-was-1.2.7.dev1.patch new file mode 100644 index 000000000..e71056e1f --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0076-BUMP-VERSION-1.2.7.dev2-was-1.2.7.dev1.patch @@ -0,0 +1,65 @@ +From 95812437eba0154b91108890dd53aab9615f7860 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Thu, 19 Dec 2019 12:31:47 +0100 +Subject: [PATCH] BUMP-VERSION: 1.2.7.dev2 (was: 1.2.7.dev1) + +--- + .bumpversion.cfg | 2 +- + VERSION.txt | 2 +- + behave/version.py | 2 +- + pytest.ini | 2 +- + setup.py | 2 +- + 5 files changed, 5 insertions(+), 5 deletions(-) + +diff --git a/.bumpversion.cfg b/.bumpversion.cfg +index a5d3d2f..4f2bb76 100644 +--- a/.bumpversion.cfg ++++ b/.bumpversion.cfg +@@ -1,5 +1,5 @@ + [bumpversion] +-current_version = 1.2.7.dev1 ++current_version = 1.2.7.dev2 + files = behave/version.py setup.py VERSION.txt pytest.ini .bumpversion.cfg + parse = (?P\d+)\.(?P\d+)\.(?P\d+)(?P\w*) + serialize = {major}.{minor}.{patch}{drop} +diff --git a/VERSION.txt b/VERSION.txt +index c0ef36b..c4e75f6 100644 +--- a/VERSION.txt ++++ b/VERSION.txt +@@ -1 +1 @@ +-1.2.7.dev1 ++1.2.7.dev2 +diff --git a/behave/version.py b/behave/version.py +index b19cb5e..67f4a41 100644 +--- a/behave/version.py ++++ b/behave/version.py +@@ -1,2 +1,2 @@ + # -- BEHAVE-VERSION: +-VERSION = "1.2.7.dev1" ++VERSION = "1.2.7.dev2" +diff --git a/pytest.ini b/pytest.ini +index b9f281a..df2a81f 100644 +--- a/pytest.ini ++++ b/pytest.ini +@@ -21,7 +21,7 @@ testpaths = tests + python_files = test_*.py + junit_family = xunit2 + addopts = --metadata PACKAGE_UNDER_TEST behave +- --metadata PACKAGE_VERSION 1.2.7.dev1 ++ --metadata PACKAGE_VERSION 1.2.7.dev2 + --html=build/testing/report.html --self-contained-html + --junit-xml=build/testing/report.xml + markers = +diff --git a/setup.py b/setup.py +index 2afc147..23f6654 100644 +--- a/setup.py ++++ b/setup.py +@@ -55,7 +55,7 @@ def find_packages_by_root_package(where): + # ----------------------------------------------------------------------------- + setup( + name="behave", +- version="1.2.7.dev1", ++ version="1.2.7.dev2", + description="behave is behaviour-driven development, Python style", + long_description=description, + author="Jens Engel, Benno Rice and Richard Jones", diff --git a/meta-python/recipes-devtools/python/python3-behave/0077-Gherkin-parser-Cleanups-related-to-question-in-800-P.patch b/meta-python/recipes-devtools/python/python3-behave/0077-Gherkin-parser-Cleanups-related-to-question-in-800-P.patch new file mode 100644 index 000000000..d244eea22 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0077-Gherkin-parser-Cleanups-related-to-question-in-800-P.patch @@ -0,0 +1,223 @@ +From dcde4bb2e1b32d676bb0ff16de80dfdf7df6b085 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Fri, 20 Dec 2019 16:45:46 +0100 +Subject: [PATCH] Gherkin parser: Cleanups related to question in #800 + (ParseError usage) + +--- + CHANGES.rst | 1 + + behave/parser.py | 41 +++++++++++++------- + features/background.feature | 4 +- + features/parser.background.sad_cases.feature | 10 ++--- + features/parser.feature.sad_cases.feature | 6 +-- + issue.features/issue0148.feature | 2 +- + 6 files changed, 38 insertions(+), 26 deletions(-) + +diff --git a/CHANGES.rst b/CHANGES.rst +index ba4daad..5653492 100644 +--- a/CHANGES.rst ++++ b/CHANGES.rst +@@ -44,6 +44,7 @@ FIXED: + + MINOR: + ++* issue #800: Cleanups related to Gherkin parser/ParseError question (submitted by: otstanteplz) + * pull #767: FIX: use_fixture_by_tag didn't return the actual fixture in all cases (provided by: jgentil) + * pull #751: gherkin: Adding Rule keyword translation in portuguese and spanish to gherkin-languages.json (provided by: dunossauro) + * pull #660: Fix minor typos (provided by: rrueth) +diff --git a/behave/parser.py b/behave/parser.py +index 520f678..58c68be 100644 +--- a/behave/parser.py ++++ b/behave/parser.py +@@ -132,13 +132,24 @@ def parse_tags(text): + + + class ParserError(Exception): +- def __init__(self, message, line, filename=None, line_text=None): +- if line: +- message += u" at line %d" % line +- if line_text: +- message += u': "%s"' % line_text.strip() ++ @staticmethod ++ def make_annotated(message, line_number, line_text=None, reason=None): ++ """Make annotated message enriched w/ line_number, line_text.""" ++ if line_number: ++ message += u" at line %d" % line_number ++ if line_text: ++ message += u': "%s"' % line_text.strip() ++ if reason: ++ message += u"\nREASON: %s" % reason ++ return message ++ ++ def __init__(self, message, line, filename=None, line_text=None, ++ reason=None, use_annotated_message=True): ++ if use_annotated_message: ++ message = self.make_annotated(message, line, line_text, reason) ++ + super(ParserError, self).__init__(message) +- self.line = line ++ self.line = line # Line number of parse failure. + self.line_text = line_text + self.filename = filename + +@@ -386,14 +397,13 @@ class Parser(object): + line = line.strip() + msg = u"Parser in unknown state %s;" % self.state + raise ParserError(msg, self.line, self.filename, line) ++ + if not func(line): + line = line.strip() +- msg = u'\nParser failure in state %s, at line %d: "%s"\n' % \ +- (self.state, self.line, line) ++ msg = u'\nParser failure in state=%s' % self.state + reason = self.ask_parse_failure_oracle(line) +- if reason: +- msg += u"REASON: %s" % reason +- raise ParserError(msg, None, self.filename) ++ raise ParserError(msg, self.line, self.filename, ++ line_text=line, reason=reason) + + def action_init(self, line): + line = line.strip() +@@ -642,7 +652,7 @@ class Parser(object): + self.table = model.Table(cells, self.line) + else: + if len(cells) != len(self.table.headings): +- raise ParserError(u"Malformed table", self.line) ++ raise ParserError(u"Malformed table", self.line, self.filename) + # MAYBE: self.filename) + self.table.add_row(cells, self.line) + return True +@@ -704,8 +714,8 @@ class Parser(object): + break # -- COMMENT: Skip rest of line. + else: + # -- BAD-TAG: Abort here. +- raise ParserError(u"tag: %s (line: %s)" % (word, line), +- self.line, self.filename) ++ message = u"tag: %s (line: %s)" % (word, line) ++ raise ParserError(message, self.line, self.filename) + return tags + + def parse_step(self, line): +@@ -723,7 +733,8 @@ class Parser(object): + step_text_after_keyword = line[len(kw):].strip() + if step_type in ("and", "but"): + if not self.last_step: +- raise ParserError(u"No previous step", self.line) ++ raise ParserError(u"No previous step", ++ self.line, self.filename) + step_type = self.last_step + else: + self.last_step = step_type +diff --git a/features/background.feature b/features/background.feature +index b2f5833..65c4882 100644 +--- a/features/background.feature ++++ b/features/background.feature +@@ -362,7 +362,7 @@ Feature: Background + When I run "behave -f plain -T features/background_sad_example1.feature" + Then it should fail with: + """ +- Parser failure in state steps, at line 5: "Background: B1" ++ Parser failure in state=steps at line 5: "Background: B1" + REASON: Background may not occur after Scenario/ScenarioOutline. + """ + +@@ -387,6 +387,6 @@ Feature: Background + When I run "behave -f plain -T features/background_sad_example2.feature" + Then it should fail with: + """ +- Parser failure in state steps, at line 5: "Background: B2 (XFAIL)" ++ Parser failure in state=steps at line 5: "Background: B2 (XFAIL)" + REASON: Background should not be used here. + """ +diff --git a/features/parser.background.sad_cases.feature b/features/parser.background.sad_cases.feature +index 37956ad..eb234ae 100644 +--- a/features/parser.background.sad_cases.feature ++++ b/features/parser.background.sad_cases.feature +@@ -37,7 +37,7 @@ Feature: Ensure that BAD/SAD Use cases of Background are detected + Then it should fail with + """ + Failed to parse "{__WORKDIR__}/features/syndrome.background_with_tags.feature": +- Parser failure in state taggable_statement, at line 4: "Background: Oops..." ++ Parser failure in state=taggable_statement at line 4: "Background: Oops..." + REASON: Background does not support tags. + """ + +@@ -57,7 +57,7 @@ Feature: Ensure that BAD/SAD Use cases of Background are detected + Then it should fail with + """ + Failed to parse "{__WORKDIR__}/features/syndrome.background_after_scenario.feature": +- Parser failure in state steps, at line 6: "Background: Oops, too late (after Scenario)" ++ Parser failure in state=steps at line 6: "Background: Oops, too late (after Scenario)" + REASON: Background may not occur after Scenario/ScenarioOutline. + """ + +@@ -77,7 +77,7 @@ Feature: Ensure that BAD/SAD Use cases of Background are detected + When I run "behave -f plain -T features/syndrome.tagged_background_after_scenario.feature" + Then it should fail with + """ +- Parser failure in state taggable_statement, at line 7: "Background: Oops, too late (after Scenario)" ++ Parser failure in state=taggable_statement at line 7: "Background: Oops, too late (after Scenario)" + REASON: Background may not occur after Scenario/ScenarioOutline. + """ + +@@ -100,7 +100,7 @@ Feature: Ensure that BAD/SAD Use cases of Background are detected + When I run "behave -f plain -T features/syndrome.background_after_scenario_outline.feature" + Then it should fail with + """ +- Parser failure in state steps, at line 10: "Background: Oops, too late (after Scenario Outline)" ++ Parser failure in state=steps at line 10: "Background: Oops, too late (after Scenario Outline)" + REASON: Background may not occur after Scenario/ScenarioOutline. + """ + +@@ -124,6 +124,6 @@ Feature: Ensure that BAD/SAD Use cases of Background are detected + When I run "behave -f plain -T features/syndrome.background_after_scenario_outline.feature" + Then it should fail with + """ +- Parser failure in state taggable_statement, at line 11: "Background: Oops, too late (after Scenario Outline)" ++ Parser failure in state=taggable_statement at line 11: "Background: Oops, too late (after Scenario Outline)" + REASON: Background may not occur after Scenario/ScenarioOutline. + """ +diff --git a/features/parser.feature.sad_cases.feature b/features/parser.feature.sad_cases.feature +index d89d9b7..0e12d9f 100644 +--- a/features/parser.feature.sad_cases.feature ++++ b/features/parser.feature.sad_cases.feature +@@ -84,7 +84,7 @@ Feature: Parsing a Feature File without a Feature or with several Features + Then it should fail with: + """ + Failed to parse "{__WORKDIR__}/features/only_text.feature": +- Parser failure in state init, at line 1: "This File: Contains only text without keywords." ++ Parser failure in state=init at line 1: "This File: Contains only text without keywords." + REASON: No feature found. + """ + +@@ -103,7 +103,7 @@ Feature: Parsing a Feature File without a Feature or with several Features + Then it should fail with: + """ + Failed to parse "{__WORKDIR__}/features/naked_scenario_only.feature": +- Parser failure in state init, at line 1: "Scenario:" ++ Parser failure in state=init at line 1: "Scenario:" + REASON: Scenario may not occur before Feature. + """ + +@@ -139,6 +139,6 @@ Feature: Parsing a Feature File without a Feature or with several Features + Then it should fail with: + """ + Failed to parse "{__WORKDIR__}/features/two_features.feature": +- Parser failure in state steps, at line 7: "Feature: F2" ++ Parser failure in state=steps at line 7: "Feature: F2" + REASON: Multiple features in one file are not supported. + """ +diff --git a/issue.features/issue0148.feature b/issue.features/issue0148.feature +index 4387795..667d196 100644 +--- a/issue.features/issue0148.feature ++++ b/issue.features/issue0148.feature +@@ -67,7 +67,7 @@ Feature: Issue #148: Substeps do not fail + And the command output should contain: + """ + ParserError: Failed to parse : +- Parser failure in state steps, at line 2: "I do something stupid" ++ Parser failure in state=steps at line 2: "I do something stupid" + """ + + diff --git a/meta-python/recipes-devtools/python/python3-behave/0078-Clarify-select-by-name-uses-regex-pattern-related-to.patch b/meta-python/recipes-devtools/python/python3-behave/0078-Clarify-select-by-name-uses-regex-pattern-related-to.patch new file mode 100644 index 000000000..60e9e9309 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0078-Clarify-select-by-name-uses-regex-pattern-related-to.patch @@ -0,0 +1,82 @@ +From 230b5bf8184beb02a3fa2678def7f11acf916e37 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sun, 9 Feb 2020 20:30:53 +0100 +Subject: [PATCH] Clarify select-by-name uses regex pattern (related to: issue + #810) + +Clarifiy select-by-name uses regex pattern: + +* Adapt command-line option --name help text +* Update command-line args docs for behave +--- + CHANGES.rst | 4 ++++ + behave/configuration.py | 10 +++++----- + docs/behave.rst | 12 ++++++------ + 3 files changed, 15 insertions(+), 11 deletions(-) + +diff --git a/CHANGES.rst b/CHANGES.rst +index 5653492..d0bf6fd 100644 +--- a/CHANGES.rst ++++ b/CHANGES.rst +@@ -31,6 +31,10 @@ ENHANCEMENTS: + * issue #678: Scenario Outline: Support tags with commas and semicolons (provided by: lawnmowerlatte, pull #679) + * issue #675: Feature files cannot be found within symlink directories (provided by: smadness, pull #680) + ++CLARIFICATION: ++ ++* issue #810: Clarify select-by-name using regex pattern (submitted by: xv-chris-w) ++ + FIXED: + + * issue #772: ScenarioOutline.Examples without table (submitted by: The-QA-Geek) +diff --git a/behave/configuration.py b/behave/configuration.py +index bd8b039..65e2e3e 100644 +--- a/behave/configuration.py ++++ b/behave/configuration.py +@@ -164,11 +164,11 @@ options = [ + override a configuration file setting.""")), + + (("-n", "--name"), +- dict(action="append", +- help="""Only execute the feature elements which match part +- of the given name. If this option is given more +- than once, it will match against all the given +- names.""")), ++ dict(action="append", metavar="NAME_PATTERN", ++ help="""Select feature elements (scenarios, ...) to run ++ which match part of the given name (regex pattern). ++ If this option is given more than once, ++ it will match against all the given names.""")), + + (("--no-capture",), + dict(action="store_false", dest="stdout_capture", +diff --git a/docs/behave.rst b/docs/behave.rst +index dfb390a..25ce523 100644 +--- a/docs/behave.rst ++++ b/docs/behave.rst +@@ -95,9 +95,9 @@ You may see the same information presented below at any time using ``behave + + .. option:: -n, --name + +- Only execute the feature elements which match part of the given name. +- If this option is given more than once, it will match against all +- the given names. ++ Select feature elements (scenarios, ...) to run which match part of ++ the given name (regex pattern). If this option is given more than ++ once, it will match against all the given names. + + .. option:: --no-capture + +@@ -449,9 +449,9 @@ Configuration Parameters + + .. describe:: name : sequence + +- Only execute the feature elements which match part of the given name. +- If this option is given more than once, it will match against all +- the given names. ++ Select feature elements (scenarios, ...) to run which match part of ++ the given name (regex pattern). If this option is given more than ++ once, it will match against all the given names. + + .. index:: + single: configuration param; stdout_capture diff --git a/meta-python/recipes-devtools/python/python3-behave/0079-FIX-Cross-reference-problem-copy-paste-in-Rule-class.patch b/meta-python/recipes-devtools/python/python3-behave/0079-FIX-Cross-reference-problem-copy-paste-in-Rule-class.patch new file mode 100644 index 000000000..bd55ca5f2 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0079-FIX-Cross-reference-problem-copy-paste-in-Rule-class.patch @@ -0,0 +1,34 @@ +From e55569d0a3fd1aa653fd702705e09359bdd90685 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Mon, 13 Apr 2020 10:31:54 +0200 +Subject: [PATCH] FIX: Cross-reference problem (copy+paste) in Rule class + docstring. + +--- + behave/model.py | 6 +++--- + 1 file changed, 3 insertions(+), 3 deletions(-) + +diff --git a/behave/model.py b/behave/model.py +index f46d2c2..cb69f9e 100644 +--- a/behave/model.py ++++ b/behave/model.py +@@ -84,8 +84,8 @@ class ScenarioContainer(TagAndStatusStatement, Replayable): + + .. attribute:: keyword + +- This is the keyword as seen in the *feature file*. In English this will +- be "Feature" or "Rule". ++ This is the keyword as seen in the *feature file*. ++ In English this will be "Feature" or "Rule". + + .. attribute:: name + +@@ -671,7 +671,7 @@ class Rule(ScenarioContainer): + + + .. versionadded:: 1.2.7 +- .. _`feature`: gherkin.html#rule ++ .. _`rule`: gherkin.html#rule + """ + type = "rule" + diff --git a/meta-python/recipes-devtools/python/python3-behave/0080-DOCS-Update-API-description-for-Runner-Operation.patch b/meta-python/recipes-devtools/python/python3-behave/0080-DOCS-Update-API-description-for-Runner-Operation.patch new file mode 100644 index 000000000..da7b2be63 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0080-DOCS-Update-API-description-for-Runner-Operation.patch @@ -0,0 +1,195 @@ +From 7b5f6f27a281714c6fef8e609416b7f405e8b1ba Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Mon, 13 Apr 2020 10:33:44 +0200 +Subject: [PATCH] DOCS: Update API description for "Runner Operation". + +* Provide description Gherkin grammar containments +* Add description for Rule(s). +--- + docs/api.rst | 137 ++++++++++++++++++++++++++++++++++++--------------- + 1 file changed, 96 insertions(+), 41 deletions(-) + +diff --git a/docs/api.rst b/docs/api.rst +index 5e4b7b4..39fa755 100644 +--- a/docs/api.rst ++++ b/docs/api.rst +@@ -196,24 +196,34 @@ Environment File Functions + The environment.py module may define code to run before and after certain + events during your testing: + +-**before_step(context, step), after_step(context, step)** +- These run before and after every step. The step passed in is an instance +- of :class:`~behave.model.Step`. ++**before_all(context), after_all(context)** ++ These run before and after the whole shooting match. ++ ++**before_feature(context, feature), after_feature(context, feature)** ++ These run before and after each feature is executed. ++ The feature object, that is passed in, is an instance of :class:`~behave.model.Feature`. ++ ++**before_rule(context, rule), after_rule(context, rule)** ++ These run before and after each rule is execured. ++ The rule object, that is passed in, is an instance of :class:`~behave.model.Rule`. + + **before_scenario(context, scenario), after_scenario(context, scenario)** +- These run before and after each scenario is run. The scenario passed in is an +- instance of :class:`~behave.model.Scenario`. ++ These run before and after each scenario is run. ++ The scenario object, that is passed in, is an instance of :class:`~behave.model.Scenario`. + +-**before_feature(context, feature), after_feature(context, feature)** +- These run before and after each feature file is exercised. The feature +- passed in is an instance of :class:`~behave.model.Feature`. ++**before_step(context, step), after_step(context, step)** ++ These run before and after every step. ++ The step object, that is passed in, is an instance of :class:`~behave.model.Step`. + + **before_tag(context, tag), after_tag(context, tag)** + These run before and after a section tagged with the given name. They are + invoked for each tag encountered in the order they're found in the +- feature file. See :ref:`controlling things with tags`. The tag passed in is +- an instance of :class:`~behave.model.Tag` and because it's a subclass of +- string you can do simple tests like: ++ feature file. See :ref:`controlling things with tags`. ++ ++ Taggable statements are: Feature, Rule, Scenario, ScenarioOutline, Examples. ++ ++ The tag, that is passed in, is an instance of :class:`~behave.model.Tag` and ++ because it's a subclass of string you can do simple tests like: + + .. code-block:: python + +@@ -227,8 +237,6 @@ events during your testing: + else: + context.browser = webdriver.PlainVanilla() + +-**before_all(context), after_all(context)** +- These run before and after the whole shooting match. + + + Some Useful Environment Ideas +@@ -311,42 +319,87 @@ Use Fixtures + Runner Operation + ================ + +-Given all the code that could be run by *behave*, this is the order in +-which that code is invoked (if they exist.) ++The execution of code is based on the Gherkin description in `*.feature` files. ++The following section provides a short overview of the hierarchical containment ++that is possible in the Gherkin grammer: + + .. parsed-literal:: + +- before_all +- for feature in all_features: +- before_feature +- for scenario in feature.scenarios: +- before_scenario +- for step in scenario.steps: +- before_step +- step.run() +- after_step +- after_scenario +- after_feature +- after_all ++ # -- SIMPLIFIED GHERKIN GRAMMAR (for Gherkin v6): ++ # CARDINALITY DECORATOR: '*' means 0..N (many), '?' means 0..1 (optional) ++ # EXAMPLE: Feature ++ # A Feature can have many Tags (as TaggableStatement: zero or more tags before its keyword). ++ # A Feature can have an optional Background. ++ # A Feature can have many Scenario(s), meaning zero or more Scenarios. ++ # A Feature can have many ScenarioOutline(s). ++ # A Feature can have many Rule(s). ++ Feature(TaggableStatement): ++ Background? ++ Scenario* ++ ScenarioOutline* ++ Rule* ++ ++ Background: ++ Step* # Background steps are injected into any Scenario of its scope. ++ ++ Scenario(TaggableStatement): ++ Step* + +-If the feature contains scenario outlines then there is an additional loop +-over all the scenarios in the outline making the running look like this: ++ ScenarioOutline(ScenarioTemplateWithPlaceholders): ++ Scenario* # Rendered Template by using ScenarioOutline.Examples.rows placeholder values. ++ ++ Rule(TaggableStatement): ++ Background? # Behave-specific extension (after removal from final Gherkin v6). ++ Scenario* ++ ScenarioOutline* ++ ++ ++Given all the code that could be run by *behave*, ++this is the order in which that code is invoked (if they exist.) + + .. parsed-literal:: + +- before_all ++ # -- PSEUDO-CODE: ++ # HOOK: before_tag(), after_tag() is called for Feature, Rule, Scenario ++ ctx = createContext() ++ call-optional-hook before_all(ctx) + for feature in all_features: +- before_feature +- for outline in feature.scenarios: +- for scenario in outline.scenarios: +- before_scenario +- for step in scenario.steps: +- before_step +- step.run() +- after_step +- after_scenario +- after_feature +- after_all ++ for tag in feature.tags: call-optional-hook before_tag(ctx, tag) ++ call-optional-hook before_feature(ctx, feature) ++ for run_item in feature.run_items: # CAN BE: Rule, Scenario, ScenarioOutline ++ execute_run_item(ctx, run_item) ++ call-optional-hook after_feature(ctx, feature) ++ for tag in feature.tags: call-optional-hook after_tag(ctx, tag) ++ call-optional-hook after_all(ctx) ++ ++ function execute_run_item(run_item, ctx): ++ if run_item isa Rule: ++ # -- CASE: Rule ++ rule = run_item ++ for tag in rule.tags: call-optional-hook before_tag(ctx, tag) ++ call-optional-hook before_rule(ctx, rule) ++ for run_item in rule.run_items: # CAN BE: Scenario, ScenarioOutline ++ execute_run_item(run_item, ctx) ++ call-optional-hook after_rule(ctx, rule) ++ for tag in rule.tags: call-optional-hook after_tag(ctx, tag) ++ else if run_item isa ScenarioOutline: ++ # -- CASE: ScenarioOutline ++ # HINT: All Scenarios are already created from Example(s) rows. ++ scenario_outline = run_item ++ for scenario in scenario_outline.scenarios: ++ execute_run_item(scenario, ctx) ++ else if run_item isa Scenario: ++ # -- CASE: Scenario ++ # HINT: Background steps are injected before scenario steps. ++ scenario = run_item ++ for tag in scenario.tags: call-optional-hook before_tag(ctx, tag) ++ call-optional-hook before_scenario(ctx, scenario) ++ for step in scenario.steps: ++ call-optional-hook before_step(ctx, step) ++ step.run(ctx) ++ call-optional-hook after_step(ctx, step) ++ call-optional-hook after_scenario(ctx, scenario) ++ for tag in scenario.tags: call-optional-hook after_tag(ctx, tag) + + + Model Objects +@@ -397,6 +450,8 @@ be: + + .. autoclass:: behave.model.Feature + ++.. autoclass:: behave.model.Rule ++ + .. autoclass:: behave.model.Background + + .. autoclass:: behave.model.Scenario diff --git a/meta-python/recipes-devtools/python/python3-behave/0081-FIX-DOCS-Runner-operations-typo.patch b/meta-python/recipes-devtools/python/python3-behave/0081-FIX-DOCS-Runner-operations-typo.patch new file mode 100644 index 000000000..3b6b83a1b --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0081-FIX-DOCS-Runner-operations-typo.patch @@ -0,0 +1,22 @@ +From 07ad7958eab96a4dadae514f7f963fa73d356191 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Mon, 13 Apr 2020 10:38:17 +0200 +Subject: [PATCH] FIX DOCS: Runner operations typo. + +--- + docs/api.rst | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/docs/api.rst b/docs/api.rst +index 39fa755..4763ad6 100644 +--- a/docs/api.rst ++++ b/docs/api.rst +@@ -367,7 +367,7 @@ this is the order in which that code is invoked (if they exist.) + for tag in feature.tags: call-optional-hook before_tag(ctx, tag) + call-optional-hook before_feature(ctx, feature) + for run_item in feature.run_items: # CAN BE: Rule, Scenario, ScenarioOutline +- execute_run_item(ctx, run_item) ++ execute_run_item(run_item, ctx) + call-optional-hook after_feature(ctx, feature) + for tag in feature.tags: call-optional-hook after_tag(ctx, tag) + call-optional-hook after_all(ctx) diff --git a/meta-python/recipes-devtools/python/python3-behave/0082-issue-740-Enhancement-Context.add_cleanup-with-layer.patch b/meta-python/recipes-devtools/python/python3-behave/0082-issue-740-Enhancement-Context.add_cleanup-with-layer.patch new file mode 100644 index 000000000..843f53113 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0082-issue-740-Enhancement-Context.add_cleanup-with-layer.patch @@ -0,0 +1,295 @@ +From 2cf10215b5e8fcbb9162bde48456b60786d828cf Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Wed, 23 Sep 2020 23:22:45 +0200 +Subject: [PATCH] issue #740: Enhancement: Context.add_cleanup() with + layer-name + +Context.add_cleanup() can choose which cleanup layer to use (outer layer). +This provides the possibility that a cleanup-funcion, registered with +Context.add_cleanup(cleanup_func, ...) to be called upon leaving +the outer context stack frames. + +Submitted by: nizwiz +Requested by: dcvmoole +--- + CHANGES.rst | 7 +++ + behave/model.py | 4 +- + behave/runner.py | 45 ++++++++++++--- + tests/unit/test_context_cleanups.py | 86 ++++++++++++++++++++++++++++- + 4 files changed, 130 insertions(+), 12 deletions(-) + +diff --git a/CHANGES.rst b/CHANGES.rst +index d0bf6fd..d758364 100644 +--- a/CHANGES.rst ++++ b/CHANGES.rst +@@ -28,6 +28,7 @@ ENHANCEMENTS: + * Use cucumber "gherkin-languages.json" now (simplify: Gherkin v6 aliases, language usage) + * Support emojis in ``*.feature`` files and steps + * Select-by-location: Add support for "Scenario container" (Feature, Rule, ScenarioOutline) (related to: #391) ++* issue #740: Enhancement: possibility to add cleanup to be called upon leaving outer context stack frames (submitted by: nizwiz, dcvmoole) + * issue #678: Scenario Outline: Support tags with commas and semicolons (provided by: lawnmowerlatte, pull #679) + * issue #675: Feature files cannot be found within symlink directories (provided by: smadness, pull #680) + +@@ -65,6 +66,12 @@ DOCUMENTATION: + * pull #684: Fix typo in "install.rst" (provided by: mstred) + * pull #628: Changed pythonhosted.org links to readthedocs.io (provided by: chrisbrake) + ++BREAKING CHANGES (naming): ++ ++* behave.runner.Context._push(layer=None): Was Context._push(layer_name=None) ++* behave.runner.scoped_context_layer(context, layer=None): ++ Was scoped_context_layer(context.layer_name=None) ++ + + .. _`cucumber-tag-expressions`: https://pypi.org/project/cucumber-tag-expressions/ + +diff --git a/behave/model.py b/behave/model.py +index cb69f9e..f1ec725 100644 +--- a/behave/model.py ++++ b/behave/model.py +@@ -363,7 +363,7 @@ class ScenarioContainer(TagAndStatusStatement, Replayable): + hook_before_entity = "before_{0}".format(entity_name) + hook_after_entity = "after_{0}".format(entity_name) + +- runner.context._push(layer_name=entity_name) # pylint: disable=protected-access ++ runner.context._push(layer=entity_name) # pylint: disable=protected-access + runner.context.tags = set(self.tags) + self._setup_context_for_run(runner.context) + +@@ -1136,7 +1136,7 @@ class Scenario(TagAndStatusStatement, Replayable): + dry_run_scenario = run_scenario and runner.config.dry_run + self.was_dry_run = dry_run_scenario + +- runner.context._push(layer_name="scenario") # pylint: disable=protected-access ++ runner.context._push(layer="scenario") # pylint: disable=protected-access + runner.context.scenario = self + runner.context.tags = set(self.effective_tags) + +diff --git a/behave/runner.py b/behave/runner.py +index 6b20937..d01bff0 100644 +--- a/behave/runner.py ++++ b/behave/runner.py +@@ -145,7 +145,7 @@ class Context(object): + tries to overwrite a user-set variable. + + You may use the "in" operator to test whether a certain value has been set +- on the context, for example: ++ on the context, for example:: + + "feature" in context + +@@ -158,6 +158,7 @@ class Context(object): + .. _`configuration file section names`: behave.html#configuration-files + """ + # pylint: disable=too-many-instance-attributes ++ LAYER_NAMES = ["testrun", "feature", "rule", "scenario"] + FAIL_ON_CLEANUP_ERRORS = True + + def __init__(self, runner): +@@ -245,16 +246,15 @@ class Context(object): + del cleanup_errors # -- ENSURE: Release other exception frames. + six.reraise(*first_cleanup_erro_info) + +- +- def _push(self, layer_name=None): ++ def _push(self, layer=None): + """Push a new layer on the context stack. +- HINT: Use layer_name values: "scenario", "feature", "testrun". ++ HINT: Use layer values: "testrun", "feature", "rule, "scenario". + +- :param layer_name: Layer name to use (or None). ++ :param layer: Layer name to use (or None). + """ + initial_data = {"@cleanups": []} +- if layer_name: +- initial_data["@layer"] = layer_name ++ if layer: ++ initial_data["@layer"] = layer + self._stack.insert(0, initial_data) + + def _pop(self): +@@ -426,6 +426,20 @@ class Context(object): + self.text = original_text + return True + ++ def _select_stack_frame_by_layer(self, layer): ++ """Select context stack frame by layer name. ++ ++ :param layer: Layer name (as string). ++ :return: Selected frame object (if any) ++ :raises: LookupError, if layer was not found. ++ """ ++ for frame in self._stack: ++ frame_layer = frame.get("@layer", None) ++ if layer == frame_layer: ++ return frame ++ # -- OOPS, NOT FOUND: ++ raise LookupError("Context.stack: layer=%s not found" % layer) ++ + def add_cleanup(self, cleanup_func, *args, **kwargs): + """Adds a cleanup function that is called when :meth:`Context._pop()` + is called. This is intended for user-cleanups. +@@ -433,10 +447,21 @@ class Context(object): + :param cleanup_func: Callable function + :param args: Args for cleanup_func() call (optional). + :param kwargs: Kwargs for cleanup_func() call (optional). ++ ++ .. note:: RESERVED :obj:`layer` : optional-string ++ ++ The keyword argument ``layer="LAYER_NAME"`` can to be used to ++ assign the :obj:`cleanup_func` to specific a layer on the context stack ++ (instead of the current layer). ++ ++ Known layer names are: "testrun", "feature", "rule", "scenario" ++ ++ .. seealso:: :attr:`.Context.LAYER_NAMES` + """ + # MAYBE: + assert callable(cleanup_func), "REQUIRES: callable(cleanup_func)" + assert self._stack ++ layer = kwargs.pop("layer", None) + if args or kwargs: + def internal_cleanup_func(): + cleanup_func(*args, **kwargs) +@@ -444,6 +469,8 @@ class Context(object): + internal_cleanup_func = cleanup_func + + current_frame = self._stack[0] ++ if layer: ++ current_frame = self._select_stack_frame_by_layer(layer) + if cleanup_func not in current_frame["@cleanups"]: + # -- AVOID DUPLICATES: + current_frame["@cleanups"].append(internal_cleanup_func) +@@ -477,7 +504,7 @@ def use_context_with_mode(context, mode): + + + @contextlib.contextmanager +-def scoped_context_layer(context, layer_name=None): ++def scoped_context_layer(context, layer=None): + """Provides context manager for context layer (push/do-something/pop cycle). + + .. code-block:: +@@ -487,7 +514,7 @@ def scoped_context_layer(context, layer_name=None): + """ + # pylint: disable=protected-access + try: +- context._push(layer_name) ++ context._push(layer) + yield context + finally: + context._pop() +diff --git a/tests/unit/test_context_cleanups.py b/tests/unit/test_context_cleanups.py +index bf0ab50..c32c572 100644 +--- a/tests/unit/test_context_cleanups.py ++++ b/tests/unit/test_context_cleanups.py +@@ -23,6 +23,9 @@ import pytest + def cleanup_func(): + pass + ++def cleanup_func_with_args(*args, **kwargs): ++ pass ++ + class CleanupFunction(object): + def __init__(self, name="CLEANUP-FUNC", listener=None): + self.name = name +@@ -42,7 +45,6 @@ class CallListener(object): + self.collected.append(message) + + +- + # ------------------------------------------------------------------------------ + # TESTS: + # ------------------------------------------------------------------------------ +@@ -145,6 +147,24 @@ class TestContextCleanup(object): + my_cleanup_B2M.assert_called_once() + my_cleanup_B3M.assert_called_once() + ++ def test_add_cleanup_with_args(self): ++ my_cleanup = Mock(spec=cleanup_func_with_args) ++ context = Context(runner=Mock()) ++ with scoped_context_layer(context): ++ context.add_cleanup(my_cleanup, 1, 2, 3) ++ my_cleanup.assert_not_called() ++ # CALLS-HERE: context._pop() ++ my_cleanup.assert_called_once_with(1, 2, 3) ++ ++ def test_add_cleanup_with_args_and_kwargs(self): ++ my_cleanup = Mock(spec=cleanup_func_with_args) ++ context = Context(runner=Mock()) ++ with scoped_context_layer(context): ++ context.add_cleanup(my_cleanup, 1, 2, 3, name="alice") ++ my_cleanup.assert_not_called() ++ # CALLS-HERE: context._pop() ++ my_cleanup.assert_called_once_with(1, 2, 3, name="alice") ++ + def test_add_cleanup__rejects_noncallable_cleanup_func(self): + class NonCallable(object): pass + non_callable = NonCallable() +@@ -218,3 +238,67 @@ class TestContextCleanup(object): + assert collect_cleanup_error.collected[0][:-1] == expected[0][:-1] + assert collect_cleanup_error.collected[1][:-1] == expected[1][:-1] + ++ ++class TestContextCleanupWithLayer(object): ++ """Tests :meth:`behave.runner.Context.add_cleanup()` ++ with layer parameter. ++ ++ :meth:`cleanup_func()` is called when Context layer is removed/popped. ++ """ ++ ++ def test_add_cleanup_with_known_layer(self): ++ my_cleanup = Mock(spec=cleanup_func) ++ context = Context(runner=Mock()) ++ with scoped_context_layer(context, layer="scenario"): ++ context.add_cleanup(my_cleanup, layer="scenario") ++ my_cleanup.assert_not_called() ++ # CALLS-HERE: context._pop() ++ my_cleanup.assert_called_once() ++ ++ def test_add_cleanup_with_known_layer_and_args(self): ++ my_cleanup = Mock(spec=cleanup_func_with_args) ++ context = Context(runner=Mock()) ++ with scoped_context_layer(context, layer="scenario"): ++ context.add_cleanup(my_cleanup, 1, 2, 3, layer="scenario") ++ my_cleanup.assert_not_called() ++ # CALLS-HERE: context._pop() ++ my_cleanup.assert_called_once_with(1, 2, 3) ++ ++ def test_add_cleanup_with_known_layer_and_kwargs(self): ++ my_cleanup = Mock(spec=cleanup_func_with_args) ++ context = Context(runner=Mock()) ++ with scoped_context_layer(context, layer="scenario"): ++ context.add_cleanup(my_cleanup, layer="scenario", name="alice") ++ my_cleanup.assert_not_called() ++ # CALLS-HERE: context._pop() ++ my_cleanup.assert_called_once_with(name="alice") ++ ++ def test_add_cleanup_with_known_deeper_layer2(self): ++ my_cleanup = Mock(spec=cleanup_func) ++ context = Context(runner=Mock()) ++ with scoped_context_layer(context, layer="feature"): ++ with scoped_context_layer(context, layer="scenario"): ++ context.add_cleanup(my_cleanup, layer="feature") ++ my_cleanup.assert_not_called() ++ # CALLS-HERE: context._pop() ++ my_cleanup.assert_called_once() ++ ++ def test_add_cleanup_with_known_deeper_layer3(self): ++ my_cleanup = Mock(spec=cleanup_func) ++ context = Context(runner=Mock()) ++ with scoped_context_layer(context, layer="testrun"): ++ with scoped_context_layer(context, layer="feature"): ++ with scoped_context_layer(context, layer="scenario"): ++ context.add_cleanup(my_cleanup, layer="feature") ++ my_cleanup.assert_not_called() ++ my_cleanup.assert_called_once() # LEFT: layer="feature" ++ my_cleanup.assert_called_once() ++ ++ def test_add_cleanup_with_unknown_layer_raises_lookup_error(self): ++ """Cleanup function is not registered""" ++ my_cleanup = Mock(spec=cleanup_func) ++ context = Context(runner=Mock()) ++ with scoped_context_layer(context): # CALLS-HERE: context._push() ++ with pytest.raises(LookupError) as error: ++ context.add_cleanup(my_cleanup, layer="other") ++ my_cleanup.assert_not_called() diff --git a/meta-python/recipes-devtools/python/python3-behave/0083-UPDATE-parse-1.18.0-parse-versions-1.16.0-.-1.17.x-h.patch b/meta-python/recipes-devtools/python/python3-behave/0083-UPDATE-parse-1.18.0-parse-versions-1.16.0-.-1.17.x-h.patch new file mode 100644 index 000000000..d03a7ccae --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0083-UPDATE-parse-1.18.0-parse-versions-1.16.0-.-1.17.x-h.patch @@ -0,0 +1,37 @@ +From a53e82fa963696988c743aaca2341390332e7e3b Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Tue, 20 Oct 2020 22:40:46 +0200 +Subject: [PATCH] UPDATE: parse >= 1.18.0 (parse versions: 1.16.0 .. 1.17.x has + a problem; parse issue #119, #121) + +--- + py.requirements/basic.txt | 2 +- + setup.py | 2 +- + 2 files changed, 2 insertions(+), 2 deletions(-) + +diff --git a/py.requirements/basic.txt b/py.requirements/basic.txt +index ad5b9a6..f976748 100644 +--- a/py.requirements/basic.txt ++++ b/py.requirements/basic.txt +@@ -9,7 +9,7 @@ + # ============================================================================ + + cucumber-tag-expressions >= 1.1.2 +-parse >= 1.8.2 ++parse >= 1.18.0 + parse_type >= 0.4.2 + six >= 1.12.0 + +diff --git a/setup.py b/setup.py +index 23f6654..fd89bda 100644 +--- a/setup.py ++++ b/setup.py +@@ -77,7 +77,7 @@ setup( + python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*", + install_requires=[ + "cucumber-tag-expressions >= 1.1.2", +- "parse >= 1.9.1", ++ "parse >= 1.18.0", + "parse_type >= 0.4.2", + "six >= 1.12.0", + "traceback2; python_version < '3.0'", diff --git a/meta-python/recipes-devtools/python/python3-behave/0084-RELATED-TO-Duplicated-steps-AmbiguousStepErrors.patch b/meta-python/recipes-devtools/python/python3-behave/0084-RELATED-TO-Duplicated-steps-AmbiguousStepErrors.patch new file mode 100644 index 000000000..32304e345 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0084-RELATED-TO-Duplicated-steps-AmbiguousStepErrors.patch @@ -0,0 +1,34 @@ +From fb3f43735f7c9b8363940595d12d7b59544f64c0 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Mon, 2 Nov 2020 17:50:10 +0100 +Subject: [PATCH] RELATED TO: Duplicated steps/AmbiguousStepErrors + +* Remove @xfail from third scenario (it worked already for some time). +* Added more detailled description to third scenario. +--- + features/step.duplicated_step.feature | 11 ++++++++++- + 1 file changed, 10 insertions(+), 1 deletion(-) + +diff --git a/features/step.duplicated_step.feature b/features/step.duplicated_step.feature +index 396cca2..f204307 100644 +--- a/features/step.duplicated_step.feature ++++ b/features/step.duplicated_step.feature +@@ -76,8 +76,17 @@ Feature: Duplicated Step Definitions + # File "features/steps/bob2_steps.py", line 3, in + # """ + +- @xfail ++ + Scenario: Duplicated Same Step Definition via import from another File ++ ++ VERIFY THAT: Duplicated step-detection works. ++ Duplicated step-registration occured through a twice imported step-module: ++ First registration may occurs by step-loading the step-module, ++ second registration due to import of first step-module. ++ ++ The step registry detects that the same step-function should be registered ++ another time and ignores it (step is already registered). ++ + Given a new working directory + And a file named "features/steps/charly1_steps.py" with: + """ diff --git a/meta-python/recipes-devtools/python/python3-behave/0085-Add-renovate.json.patch b/meta-python/recipes-devtools/python/python3-behave/0085-Add-renovate.json.patch new file mode 100644 index 000000000..8b0f22d8f --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0085-Add-renovate.json.patch @@ -0,0 +1,21 @@ +From d65b38953bfe1cc346a2548a8e617229df87058b Mon Sep 17 00:00:00 2001 +From: Renovate Bot +Date: Wed, 4 Nov 2020 08:43:32 +0000 +Subject: [PATCH] Add renovate.json + +--- + renovate.json | 5 +++++ + 1 file changed, 5 insertions(+) + create mode 100644 renovate.json + +diff --git a/renovate.json b/renovate.json +new file mode 100644 +index 0000000..f45d8f1 +--- /dev/null ++++ b/renovate.json +@@ -0,0 +1,5 @@ ++{ ++ "extends": [ ++ "config:base" ++ ] ++} diff --git a/meta-python/recipes-devtools/python/python3-behave/0086-PRPEPARE-RENOVATE-With-adaptions.patch b/meta-python/recipes-devtools/python/python3-behave/0086-PRPEPARE-RENOVATE-With-adaptions.patch new file mode 100644 index 000000000..39a6d76c2 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0086-PRPEPARE-RENOVATE-With-adaptions.patch @@ -0,0 +1,175 @@ +From 61f3a341b7accd0580af7c7b2e8db4cb6a7f4bec Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Wed, 4 Nov 2020 10:28:59 +0100 +Subject: [PATCH] PRPEPARE-RENOVATE: With adaptions + +* Provide locations/patterns for pip requirements file(s) +* Move "renovate.json" to ".github/" +--- + .github/renovate.json | 13 ++++++++ + MANIFEST.in | 4 +-- + issue.features/README.rst | 32 +++++++++++++++++++ + issue.features/README.txt | 17 ---------- + .../{requirements.txt => py.requirements.txt} | 7 ++-- + py.requirements/{README.txt => README.rst} | 0 + py.requirements/testing.txt | 4 ++- + renovate.json | 5 --- + 8 files changed, 53 insertions(+), 29 deletions(-) + create mode 100644 .github/renovate.json + create mode 100644 issue.features/README.rst + delete mode 100644 issue.features/README.txt + rename issue.features/{requirements.txt => py.requirements.txt} (82%) + rename py.requirements/{README.txt => README.rst} (100%) + delete mode 100644 renovate.json + +diff --git a/.github/renovate.json b/.github/renovate.json +new file mode 100644 +index 0000000..3399a00 +--- /dev/null ++++ b/.github/renovate.json +@@ -0,0 +1,13 @@ ++{ ++ "extends": [ ++ "config:base" ++ ], ++ "pip_requirements": { ++ "fileMatch": [ ++ "py.requirements/all.txt", ++ "py.requirements/*.txt", ++ "tasks/py.requirements.txt", ++ "issue.features/py.requirements.txt" ++ ] ++} ++} +diff --git a/MANIFEST.in b/MANIFEST.in +index 60c2601..84d20f4 100644 +--- a/MANIFEST.in ++++ b/MANIFEST.in +@@ -28,8 +28,8 @@ recursive-include tasks *.py *.zip *.txt *.rst + recursive-include tools *.feature *.py *.yml *.sh + recursive-include features *.feature *.py *.txt + recursive-include issue.features *.feature *.py *.txt +-recursive-include more.features *.feature *.py *.txt +-recursive-include py.requirements *.txt ++recursive-include more.features *.feature *.py *.txt *.rst ++recursive-include py.requirements *.txt *.rst + + prune .tox + prune .venv* +diff --git a/issue.features/README.rst b/issue.features/README.rst +new file mode 100644 +index 0000000..1d1d980 +--- /dev/null ++++ b/issue.features/README.rst +@@ -0,0 +1,32 @@ ++issue.features: ++=============================================================================== ++ ++:Requires: Python >= 2.7 or Python >= 3.5 ++ ++This directory contains behave self-tests to ensure that behave related ++issues are fixed. ++ ++PROCEDURE: ++ ++ * ONCE: Install python requirements ("py.requirements.txt") ++ * Run the tests with behave ++ ++.. code-block:: shell ++ ++ # -- FOR: ++ # pip: For python2.7 or python3 (depends on platform and/or user)) ++ # pip3: For python3 ++ pip install -U -r issue.features/testing.txt ++ pip3 install -U -r issue.features/testing.txt ++ ++ ++ALTERNATIVE: ++ ++.. code-block:: shell ++ ++ pip install -U -r py.requirements/testing.txt ++ pip3 install -U -r py.requirements/testing.txt ++ ++.. code-block:: shell ++ ++ bin/behave -f progress issue.features/ +diff --git a/issue.features/README.txt b/issue.features/README.txt +deleted file mode 100644 +index af499f3..0000000 +--- a/issue.features/README.txt ++++ /dev/null +@@ -1,17 +0,0 @@ +-issue.features: +-=============================================================================== +- +-:Status: PREPARED (fixes are being applied). +-:Requires: Python >= 2.6 (due to step implementations) +- +-This directory contains behave self-tests to ensure that behave related +-issues are fixed. +- +-PROCEDURE: +- +- * ONCE: Install python requirements ("requirements.txt") +- * Run the tests with behave +- +-:: +- +- bin/behave -f progress issue.features/ +diff --git a/issue.features/requirements.txt b/issue.features/py.requirements.txt +similarity index 82% +rename from issue.features/requirements.txt +rename to issue.features/py.requirements.txt +index d0c4bab..f0def9d 100644 +--- a/issue.features/requirements.txt ++++ b/issue.features/py.requirements.txt +@@ -1,12 +1,11 @@ + # ============================================================================ + # PYTHON PACKAGE REQUIREMENTS: For running issue.features/ + # ============================================================================ +-# REQUIRES: Python >= 2.5 +-# REQUIRES: Python >= 3.2 ++# REQUIRES: Python >= 2.7 ++# REQUIRES: Python >= 3.5 + # DESCRIPTION: + # pip install -r + # + # ============================================================================ + +-PyHamcrest >= 1.6 +- ++PyHamcrest >= 2.0.2 +diff --git a/py.requirements/README.txt b/py.requirements/README.rst +similarity index 100% +rename from py.requirements/README.txt +rename to py.requirements/README.rst +diff --git a/py.requirements/testing.txt b/py.requirements/testing.txt +index 85b0908..5ccdda8 100644 +--- a/py.requirements/testing.txt ++++ b/py.requirements/testing.txt +@@ -8,10 +8,12 @@ pytest < 5.0; python_version < '3.0' # pytest >= 4.2 + pytest >= 5.0; python_version >= '3.0' + pytest-html >= 1.19.0 + mock >= 2.0 +-PyHamcrest >= 1.9 ++PyHamcrest >= 2.0.2 + + # -- NEEDED: By some tests (as proof of concept) + # NOTE: path.py-10.1 is required for python2.6 + # HINT: path.py => path (python-install-package was renamed for python3) + path.py >= 11.5.0; python_version < '3.5' + path >= 13.1.0; python_version >= '3.5' ++ ++-r ../issue.features/py.requirements.txt +diff --git a/renovate.json b/renovate.json +deleted file mode 100644 +index f45d8f1..0000000 +--- a/renovate.json ++++ /dev/null +@@ -1,5 +0,0 @@ +-{ +- "extends": [ +- "config:base" +- ] +-} diff --git a/meta-python/recipes-devtools/python/python3-behave/0087-Pin-dependencies.patch b/meta-python/recipes-devtools/python/python3-behave/0087-Pin-dependencies.patch new file mode 100644 index 000000000..eeebd7be3 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0087-Pin-dependencies.patch @@ -0,0 +1,36 @@ +From 9271668477574b317c72099eab587f516ae082de Mon Sep 17 00:00:00 2001 +From: Renovate Bot +Date: Wed, 4 Nov 2020 09:32:34 +0000 +Subject: [PATCH] Pin dependencies + +--- + issue.features/py.requirements.txt | 2 +- + tasks/py.requirements.txt | 4 ++-- + 2 files changed, 3 insertions(+), 3 deletions(-) + +diff --git a/issue.features/py.requirements.txt b/issue.features/py.requirements.txt +index f0def9d..38f452d 100644 +--- a/issue.features/py.requirements.txt ++++ b/issue.features/py.requirements.txt +@@ -8,4 +8,4 @@ + # + # ============================================================================ + +-PyHamcrest >= 2.0.2 ++PyHamcrest==2.0.2 +diff --git a/tasks/py.requirements.txt b/tasks/py.requirements.txt +index 810f834..9c82d11 100644 +--- a/tasks/py.requirements.txt ++++ b/tasks/py.requirements.txt +@@ -8,9 +8,9 @@ + # * http://www.pip-installer.org/ + # ============================================================================ + +-invoke >= 1.2.0 ++invoke==1.4.1 + pycmd +-six >= 1.12.0 ++six==1.15.0 + + # -- HINT: path.py => path (python-install-package was renamed for python3) + path.py >= 11.5.0; python_version < '3.5' diff --git a/meta-python/recipes-devtools/python/python3-behave/0088-renovate-Extend-pip-requirements-file-list.patch b/meta-python/recipes-devtools/python/python3-behave/0088-renovate-Extend-pip-requirements-file-list.patch new file mode 100644 index 000000000..33ec83848 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0088-renovate-Extend-pip-requirements-file-list.patch @@ -0,0 +1,31 @@ +From 85d1f009d933d356e65a548f29e4d5a1e78a72d2 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Wed, 4 Nov 2020 10:54:02 +0100 +Subject: [PATCH] renovate: Extend pip requirements file list. + +--- + .github/renovate.json | 9 +++++++-- + 1 file changed, 7 insertions(+), 2 deletions(-) + +diff --git a/.github/renovate.json b/.github/renovate.json +index 3399a00..9b72f76 100644 +--- a/.github/renovate.json ++++ b/.github/renovate.json +@@ -4,10 +4,15 @@ + ], + "pip_requirements": { + "fileMatch": [ +- "py.requirements/all.txt", + "py.requirements/*.txt", ++ "py.requirements/all.txt", ++ "py.requirements/basic.txt", ++ "py.requirements/develop.txt", ++ "py.requirements/testing.txt", ++ "py.requirements/ci.tox.txt", ++ "py.requirements/ci.travis.txt", + "tasks/py.requirements.txt", + "issue.features/py.requirements.txt" + ] +-} ++ } + } diff --git a/meta-python/recipes-devtools/python/python3-behave/0089-PIN-REQUIREMENTS-Extend-to-all-places.patch b/meta-python/recipes-devtools/python/python3-behave/0089-PIN-REQUIREMENTS-Extend-to-all-places.patch new file mode 100644 index 000000000..758bd9878 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0089-PIN-REQUIREMENTS-Extend-to-all-places.patch @@ -0,0 +1,92 @@ +From a45ef62144036b481e8432bcd2698a5f7bfcddd2 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Wed, 4 Nov 2020 10:54:48 +0100 +Subject: [PATCH] PIN REQUIREMENTS: Extend to all places. PINNED: invoke, six, + PyHamcrest + +--- + issue.features/py.requirements.txt | 2 +- + py.requirements/basic.txt | 2 +- + py.requirements/ci.tox.txt | 2 +- + py.requirements/ci.travis.txt | 2 +- + py.requirements/develop.txt | 4 +--- + py.requirements/testing.txt | 2 +- + 6 files changed, 6 insertions(+), 8 deletions(-) + +diff --git a/issue.features/py.requirements.txt b/issue.features/py.requirements.txt +index 38f452d..6e3cf83 100644 +--- a/issue.features/py.requirements.txt ++++ b/issue.features/py.requirements.txt +@@ -8,4 +8,4 @@ + # + # ============================================================================ + +-PyHamcrest==2.0.2 ++PyHamcrest == 2.0.2 +diff --git a/py.requirements/basic.txt b/py.requirements/basic.txt +index f976748..6c644e0 100644 +--- a/py.requirements/basic.txt ++++ b/py.requirements/basic.txt +@@ -11,7 +11,7 @@ + cucumber-tag-expressions >= 1.1.2 + parse >= 1.18.0 + parse_type >= 0.4.2 +-six >= 1.12.0 ++six == 1.15.0 + + traceback2; python_version < '3.0' + contextlib2 # MAYBE: python_version < '3.5' +diff --git a/py.requirements/ci.tox.txt b/py.requirements/ci.tox.txt +index 4001bc2..20ae791 100644 +--- a/py.requirements/ci.tox.txt ++++ b/py.requirements/ci.tox.txt +@@ -6,7 +6,7 @@ pytest < 5.0; python_version < '3.0' # pytest >= 4.2 + pytest >= 5.0; python_version >= '3.0' + pytest-html >= 1.19.0 + mock >= 2.0 +-PyHamcrest >= 1.9 ++PyHamcrest == 2.0.2 + + # -- HINT: path.py => path (python-install-package was renamed for python3) + path.py >= 11.5.0; python_version < '3.5' +diff --git a/py.requirements/ci.travis.txt b/py.requirements/ci.travis.txt +index de4120a..c69445c 100644 +--- a/py.requirements/ci.travis.txt ++++ b/py.requirements/ci.travis.txt +@@ -5,7 +5,7 @@ pytest < 5.0; python_version < '3.0' + pytest >= 5.0; python_version >= '3.0' + pytest-html >= 1.19.0 + mock >= 2.0 +-PyHamcrest >= 1.9 ++PyHamcrest == 2.0.2 + + # -- NEEDED: By some tests (as proof of concept) + # NOTE: path.py-10.1 is required for python2.6 +diff --git a/py.requirements/develop.txt b/py.requirements/develop.txt +index a92ee5f..e7dc418 100644 +--- a/py.requirements/develop.txt ++++ b/py.requirements/develop.txt +@@ -3,9 +3,7 @@ + # ============================================================================ + + # -- BUILD-TOOL: +-# PREPARE USAGE: invoke +-# ALREADY: six >= 1.11.0 +-invoke >= 1.2.0 ++invoke == 1.4.1 + pathlib; python_version <= '3.4' + pycmd + +diff --git a/py.requirements/testing.txt b/py.requirements/testing.txt +index 5ccdda8..d3bca18 100644 +--- a/py.requirements/testing.txt ++++ b/py.requirements/testing.txt +@@ -8,7 +8,7 @@ pytest < 5.0; python_version < '3.0' # pytest >= 4.2 + pytest >= 5.0; python_version >= '3.0' + pytest-html >= 1.19.0 + mock >= 2.0 +-PyHamcrest >= 2.0.2 ++PyHamcrest == 2.0.2 + + # -- NEEDED: By some tests (as proof of concept) + # NOTE: path.py-10.1 is required for python2.6 diff --git a/meta-python/recipes-devtools/python/python3-behave/0090-tasks-Add-invoke_cleanup-replaces-_tasklet_cleanup-r.patch b/meta-python/recipes-devtools/python/python3-behave/0090-tasks-Add-invoke_cleanup-replaces-_tasklet_cleanup-r.patch new file mode 100644 index 000000000..9c6735886 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0090-tasks-Add-invoke_cleanup-replaces-_tasklet_cleanup-r.patch @@ -0,0 +1,8116 @@ +From dc91d552ba45dd34022f37a4d4f328d5de5c1e17 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Wed, 4 Nov 2020 21:18:12 +0100 +Subject: [PATCH] tasks: Add invoke_cleanup (replaces: _tasklet_cleanup), + remove _vendor/ directory. + +--- + py.requirements/docs.txt | 3 +- + tasks/__init__.py | 10 +- + tasks/_setup.py | 10 +- + tasks/_tasklet_cleanup.py | 295 ------- + tasks/_vendor/README.rst | 35 - + tasks/_vendor/invoke.zip | Bin 172281 -> 0 bytes + tasks/_vendor/path.py | 1725 ------------------------------------- + tasks/_vendor/pathlib.py | 1280 --------------------------- + tasks/_vendor/six.py | 868 ------------------- + tasks/docs.py | 33 +- + tasks/invoke_cleanup.py | 447 ++++++++++ + tasks/py.requirements.txt | 2 +- + tasks/release.py | 2 +- + tasks/test.py | 3 +- + 14 files changed, 497 insertions(+), 4216 deletions(-) + delete mode 100644 tasks/_tasklet_cleanup.py + delete mode 100644 tasks/_vendor/README.rst + delete mode 100644 tasks/_vendor/invoke.zip + delete mode 100644 tasks/_vendor/path.py + delete mode 100644 tasks/_vendor/pathlib.py + delete mode 100644 tasks/_vendor/six.py + create mode 100644 tasks/invoke_cleanup.py + +diff --git a/py.requirements/docs.txt b/py.requirements/docs.txt +index 6839ba9..1384e00 100644 +--- a/py.requirements/docs.txt ++++ b/py.requirements/docs.txt +@@ -3,7 +3,8 @@ + # ============================================================================ + # REQUIRES: pip >= 8.0 + +-Sphinx >= 1.6 ++sphinx >= 1.6 ++sphinx-autobuild + sphinx_bootstrap_theme >= 0.6.0 + + # -- SUPPORT: sphinx-doc translations (prepared) +diff --git a/tasks/__init__.py b/tasks/__init__.py +index a572465..9ae899b 100644 +--- a/tasks/__init__.py ++++ b/tasks/__init__.py +@@ -20,7 +20,7 @@ from __future__ import absolute_import + from . import _setup # pylint: disable=wrong-import-order + import os.path + import sys +-INVOKE_MINVERSION = "1.2.0" ++INVOKE_MINVERSION = "1.4.0" + _setup.setup_path() + _setup.require_invoke_minversion(INVOKE_MINVERSION) + +@@ -35,7 +35,8 @@ import sys + from invoke import Collection + + # -- TASK-LIBRARY: +-from . import _tasklet_cleanup as cleanup ++# PREPARED: import invoke_cleanup as cleanup ++from . import invoke_cleanup as cleanup + from . import docs + from . import test + from . import release +@@ -52,19 +53,18 @@ from . import develop + # TASK CONFIGURATION: + # ----------------------------------------------------------------------------- + namespace = Collection() +-# DISABLED: namespace.add_task(clean.clean) +-# DISABLED: namespace.add_task(clean.clean_all) + namespace.add_collection(Collection.from_module(cleanup), name="cleanup") + namespace.add_collection(Collection.from_module(docs)) + namespace.add_collection(Collection.from_module(test)) + namespace.add_collection(Collection.from_module(release)) + namespace.add_collection(Collection.from_module(develop)) +-cleanup.cleanup_tasks.add_task(cleanup.clean_python) + ++# -- ENSURE: python cleanup is used for this project. + cleanup.cleanup_tasks.add_task(cleanup.clean_python) + + # -- INJECT: clean configuration into this namespace + namespace.configure(cleanup.namespace.configuration()) ++namespace.configure(test.namespace.configuration()) + if sys.platform.startswith("win"): + # -- OVERRIDE SETTINGS: For platform=win32, ... (Windows) + from ._compat_shutil import which +diff --git a/tasks/_setup.py b/tasks/_setup.py +index eda5ca9..e69ec82 100644 +--- a/tasks/_setup.py ++++ b/tasks/_setup.py +@@ -14,7 +14,7 @@ import sys + HERE = os.path.dirname(__file__) + TASKS_VENDOR_DIR = os.path.join(HERE, "_vendor") + INVOKE_BUNDLE = os.path.join(TASKS_VENDOR_DIR, "invoke.zip") +-INVOKE_BUNDLE_VERSION = "0.13.0" # pylint: disable=invalid-name ++INVOKE_BUNDLE_VERSION = "1.4.0" + + DEBUG_SYSPATH = False + +@@ -25,6 +25,7 @@ DEBUG_SYSPATH = False + class VersionRequirementError(SystemExit): + pass + ++ + # ----------------------------------------------------------------------------- + # FUNCTIONS: + # ----------------------------------------------------------------------------- +@@ -32,7 +33,7 @@ def setup_path(invoke_minversion=None): + """Setup python search and add ``TASKS_VENDOR_DIR`` (if available).""" + # print("INVOKE.tasks: setup_path") + if not os.path.isdir(TASKS_VENDOR_DIR): +- print("SKIP: TASKS_VENDOR_DIR=%s is missing" % TASKS_VENDOR_DIR) ++ # SILENT: print("SKIP: TASKS_VENDOR_DIR=%s is missing" % os.path.relpath(TASKS_VENDOR_DIR)) + return + elif os.path.abspath(TASKS_VENDOR_DIR) in sys.path: + # -- SETUP ALREADY DONE: +@@ -86,6 +87,7 @@ def require_invoke_minversion(min_version, verbose=False): + os.environ["INVOKE_VERSION"] = invoke_version + print("USING: invoke.version=%s" % invoke_version) + ++ + def need_vendor_bundles(invoke_minversion=None): + invoke_minversion = invoke_minversion or "0.0.0" + need_vendor_answers = [] +@@ -102,6 +104,7 @@ def need_vendor_bundles(invoke_minversion=None): + # return need_bundle1 or need_bundle2 + return any(need_vendor_answers) + ++ + def need_vendor_bundle_invoke(invoke_minversion="0.0.0"): + # -- REQUIRE: invoke + try: +@@ -116,6 +119,7 @@ def need_vendor_bundle_invoke(invoke_minversion="0.0.0"): + need_bundle = True + return need_bundle + ++ + # ----------------------------------------------------------------------------- + # UTILITY FUNCTIONS: + # ----------------------------------------------------------------------------- +@@ -125,11 +129,13 @@ def setup_path_for_bundle(bundle_path, pos=0): + return True + return False + ++ + def syspath_insert(pos, path): + if path in sys.path: + sys.path.remove(path) + sys.path.insert(pos, path) + ++ + def syspath_append(path): + if path in sys.path: + sys.path.remove(path) +diff --git a/tasks/_tasklet_cleanup.py b/tasks/_tasklet_cleanup.py +deleted file mode 100644 +index 2999bc6..0000000 +--- a/tasks/_tasklet_cleanup.py ++++ /dev/null +@@ -1,295 +0,0 @@ +-# -*- coding: UTF-8 -*- +-""" +-Provides cleanup tasks for invoke build scripts (as generic invoke tasklet). +-Simplifies writing common, composable and extendable cleanup tasks. +- +-PYTHON PACKAGE REQUIREMENTS: +-* path.py >= 8.2.1 (as path-object abstraction) +-* pathlib (for ant-like wildcard patterns; since: python > 3.5) +-* pycmd (required-by: clean_python()) +- +-clean task: Add Additional Directories and Files to be removed +-------------------------------------------------------------------------------- +- +-Create an invoke configuration file (YAML of JSON) with the additional +-configuration data: +- +-.. code-block:: yaml +- +- # -- FILE: invoke.yaml +- # USE: clean.directories, clean.files to override current configuration. +- clean: +- extra_directories: +- - **/tmp/ +- extra_files: +- - **/*.log +- - **/*.bak +- +- +-Registration of Cleanup Tasks +------------------------------- +- +-Other task modules often have an own cleanup task to recover the clean state. +-The :meth:`clean` task, that is provided here, supports the registration +-of additional cleanup tasks. Therefore, when the :meth:`clean` task is executed, +-all registered cleanup tasks will be executed. +- +-EXAMPLE:: +- +- # -- FILE: tasks/docs.py +- from __future__ import absolute_import +- from invoke import task, Collection +- from tasklet_cleanup import cleanup_tasks, cleanup_dirs +- +- @task +- def clean(ctx, dry_run=False): +- "Cleanup generated documentation artifacts." +- cleanup_dirs(["build/docs"]) +- +- namespace = Collection(clean) +- ... +- +- # -- REGISTER CLEANUP TASK: +- cleanup_tasks.add_task(clean, "clean_docs") +- cleanup_tasks.configure(namespace.configuration()) +-""" +- +-from __future__ import absolute_import, print_function +-import os.path +-import sys +-import pathlib +-from invoke import task, Collection +-from invoke.executor import Executor +-from invoke.exceptions import Exit, Failure, UnexpectedExit +-from path import Path +- +- +-# ----------------------------------------------------------------------------- +-# CLEANUP UTILITIES: +-# ----------------------------------------------------------------------------- +-def cleanup_accept_old_config(ctx): +- ctx.cleanup.directories.extend(ctx.clean.directories or []) +- ctx.cleanup.extra_directories.extend(ctx.clean.extra_directories or []) +- ctx.cleanup.files.extend(ctx.clean.files or []) +- ctx.cleanup.extra_files.extend(ctx.clean.extra_files or []) +- +- ctx.cleanup_all.directories.extend(ctx.clean_all.directories or []) +- ctx.cleanup_all.extra_directories.extend(ctx.clean_all.extra_directories or []) +- ctx.cleanup_all.files.extend(ctx.clean_all.files or []) +- ctx.cleanup_all.extra_files.extend(ctx.clean_all.extra_files or []) +- +- +-def execute_cleanup_tasks(ctx, cleanup_tasks, dry_run=False): +- """Execute several cleanup tasks as part of the cleanup. +- +- REQUIRES: ``clean(ctx, dry_run=False)`` signature in cleanup tasks. +- +- :param ctx: Context object for the tasks. +- :param cleanup_tasks: Collection of cleanup tasks (as Collection). +- :param dry_run: Indicates dry-run mode (bool) +- """ +- # pylint: disable=redefined-outer-name +- executor = Executor(cleanup_tasks, ctx.config) +- failure_count = 0 +- for cleanup_task in cleanup_tasks.tasks: +- try: +- print("CLEANUP TASK: %s" % cleanup_task) +- executor.execute((cleanup_task, dict(dry_run=dry_run))) +- except (Exit, Failure, UnexpectedExit) as e: +- print("FAILURE in CLEANUP TASK: %s (GRACEFULLY-IGNORED)" % cleanup_task) +- failure_count += 1 +- +- if failure_count: +- print("CLEANUP TASKS: %d failure(s) occured" % failure_count) +- +- +-def cleanup_dirs(patterns, dry_run=False, workdir="."): +- """Remove directories (and their contents) recursively. +- Skips removal if directories does not exist. +- +- :param patterns: Directory name patterns, like "**/tmp*" (as list). +- :param dry_run: Dry-run mode indicator (as bool). +- :param workdir: Current work directory (default=".") +- """ +- current_dir = Path(workdir) +- python_basedir = Path(Path(sys.executable).dirname()).joinpath("..").abspath() +- warn2_counter = 0 +- for dir_pattern in patterns: +- for directory in path_glob(dir_pattern, current_dir): +- directory2 = directory.abspath() +- if sys.executable.startswith(directory2): +- # pylint: disable=line-too-long +- print("SKIP-SUICIDE: '%s' contains current python executable" % directory) +- continue +- elif directory2.startswith(python_basedir): +- # -- PROTECT CURRENTLY USED VIRTUAL ENVIRONMENT: +- if warn2_counter <= 4: +- print("SKIP-SUICIDE: '%s'" % directory) +- warn2_counter += 1 +- continue +- +- if not directory.isdir(): +- print("RMTREE: %s (SKIPPED: Not a directory)" % directory) +- continue +- +- if dry_run: +- print("RMTREE: %s (dry-run)" % directory) +- else: +- print("RMTREE: %s" % directory) +- directory.rmtree_p() +- +- +-def cleanup_files(patterns, dry_run=False, workdir="."): +- """Remove files or files selected by file patterns. +- Skips removal if file does not exist. +- +- :param patterns: File patterns, like "**/*.pyc" (as list). +- :param dry_run: Dry-run mode indicator (as bool). +- :param workdir: Current work directory (default=".") +- """ +- current_dir = Path(workdir) +- python_basedir = Path(Path(sys.executable).dirname()).joinpath("..").abspath() +- error_message = None +- error_count = 0 +- for file_pattern in patterns: +- for file_ in path_glob(file_pattern, current_dir): +- if file_.abspath().startswith(python_basedir): +- # -- PROTECT CURRENTLY USED VIRTUAL ENVIRONMENT: +- continue +- if not file_.isfile(): +- print("REMOVE: %s (SKIPPED: Not a file)" % file_) +- continue +- +- if dry_run: +- print("REMOVE: %s (dry-run)" % file_) +- else: +- print("REMOVE: %s" % file_) +- try: +- file_.remove_p() +- except os.error as e: +- message = "%s: %s" % (e.__class__.__name__, e) +- print(message + " basedir: "+ python_basedir) +- error_count += 1 +- if not error_message: +- error_message = message +- if False and error_message: +- class CleanupError(RuntimeError): +- pass +- raise CleanupError(error_message) +- +- +-def path_glob(pattern, current_dir=None): +- """Use pathlib for ant-like patterns, like: "**/*.py" +- +- :param pattern: File/directory pattern to use (as string). +- :param current_dir: Current working directory (as Path, pathlib.Path, str) +- :return Resolved Path (as path.Path). +- """ +- if not current_dir: +- current_dir = pathlib.Path.cwd() +- elif not isinstance(current_dir, pathlib.Path): +- # -- CASE: string, path.Path (string-like) +- current_dir = pathlib.Path(str(current_dir)) +- +- for p in current_dir.glob(pattern): +- yield Path(str(p)) +- +- +-# ----------------------------------------------------------------------------- +-# GENERIC CLEANUP TASKS: +-# ----------------------------------------------------------------------------- +-@task +-def clean(ctx, dry_run=False): +- """Cleanup temporary dirs/files to regain a clean state.""" +- cleanup_accept_old_config(ctx) +- directories = ctx.cleanup.directories or [] +- directories.extend(ctx.cleanup.extra_directories or []) +- files = ctx.cleanup.files or [] +- files.extend(ctx.cleanup.extra_files or []) +- +- # -- PERFORM CLEANUP: +- execute_cleanup_tasks(ctx, cleanup_tasks, dry_run=dry_run) +- cleanup_dirs(directories, dry_run=dry_run) +- cleanup_files(files, dry_run=dry_run) +- +- +-@task(name="all", aliases=("distclean",)) +-def clean_all(ctx, dry_run=False): +- """Clean up everything, even the precious stuff. +- NOTE: clean task is executed first. +- """ +- cleanup_accept_old_config(ctx) +- directories = ctx.config.cleanup_all.directories or [] +- directories.extend(ctx.config.cleanup_all.extra_directories or []) +- files = ctx.config.cleanup_all.files or [] +- files.extend(ctx.config.cleanup_all.extra_files or []) +- +- # -- PERFORM CLEANUP: +- # HINT: Remove now directories, files first before cleanup-tasks. +- cleanup_dirs(directories, dry_run=dry_run) +- cleanup_files(files, dry_run=dry_run) +- execute_cleanup_tasks(ctx, cleanup_all_tasks, dry_run=dry_run) +- clean(ctx, dry_run=dry_run) +- +- +-@task(name="python") +-def clean_python(ctx, dry_run=False): +- """Cleanup python related files/dirs: *.pyc, *.pyo, ...""" +- # MAYBE NOT: "**/__pycache__" +- cleanup_dirs(["build", "dist", "*.egg-info", "**/__pycache__"], +- dry_run=dry_run) +- if not dry_run: +- ctx.run("py.cleanup") +- cleanup_files(["**/*.pyc", "**/*.pyo", "**/*$py.class"], dry_run=dry_run) +- +- +-# ----------------------------------------------------------------------------- +-# TASK CONFIGURATION: +-# ----------------------------------------------------------------------------- +-CLEANUP_EMPTY_CONFIG = { +- "directories": [], +- "files": [], +- "extra_directories": [], +- "extra_files": [], +-} +-def make_cleanup_config(**kwargs): +- config_data = CLEANUP_EMPTY_CONFIG.copy() +- config_data.update(kwargs) +- return config_data +- +- +-namespace = Collection(clean_all, clean_python) +-namespace.add_task(clean, default=True) +-namespace.configure({ +- "cleanup": make_cleanup_config( +- files=["*.bak", "*.log", "*.tmp", "**/.DS_Store", "**/*.~*~"] +- ), +- "cleanup_all": make_cleanup_config( +- directories=[".venv*", ".tox", "downloads", "tmp"] +- ), +- # -- BACKWARD-COMPATIBLE: OLD-STYLE +- "clean": CLEANUP_EMPTY_CONFIG.copy(), +- "clean_all": CLEANUP_EMPTY_CONFIG.copy(), +-}) +- +- +-# -- EXTENSION-POINT: CLEANUP TASKS (called by: clean, clean_all task) +-# NOTE: Can be used by other tasklets to register cleanup tasks. +-cleanup_tasks = Collection("cleanup_tasks") +-cleanup_all_tasks = Collection("cleanup_all_tasks") +- +-# -- EXTEND NORMAL CLEANUP-TASKS: +-# DISABLED: cleanup_tasks.add_task(clean_python) +-# +-# ----------------------------------------------------------------------------- +-# EXTENSION-POINT: CONFIGURATION HELPERS: Can be used from other task modules +-# ----------------------------------------------------------------------------- +-def config_add_cleanup_dirs(directories): +- # pylint: disable=protected-access +- the_cleanup_directories = namespace._configuration["clean"]["directories"] +- the_cleanup_directories.extend(directories) +- +-def config_add_cleanup_files(files): +- # pylint: disable=protected-access +- the_cleanup_files = namespace._configuration["clean"]["files"] +- the_cleanup_files.extend(files) +diff --git a/tasks/_vendor/README.rst b/tasks/_vendor/README.rst +deleted file mode 100644 +index 68fc06a..0000000 +--- a/tasks/_vendor/README.rst ++++ /dev/null +@@ -1,35 +0,0 @@ +-tasks/_vendor: Bundled vendor parts -- needed by tasks +-=============================================================================== +- +-This directory contains bundled archives that may be needed to run the tasks. +-Especially, it contains an executable "invoke.zip" archive. +-This archive can be used when invoke is not installed. +- +-To execute invoke from the bundled ZIP archive:: +- +- +- python -m tasks/_vendor/invoke.zip --help +- python -m tasks/_vendor/invoke.zip --version +- +- +-Example for a local "bin/invoke" script in a UNIX like platform environment:: +- +- #!/bin/bash +- # RUN INVOKE: From bundled ZIP file. +- +- HERE=$(dirname $0) +- +- python ${HERE}/../tasks/_vendor/invoke.zip $* +- +-Example for a local "bin/invoke.cmd" script in a Windows environment:: +- +- @echo off +- REM ========================================================================== +- REM RUN INVOKE: From bundled ZIP file. +- REM ========================================================================== +- +- setlocal +- set HERE=%~dp0 +- if not defined PYTHON set PYTHON=python +- +- %PYTHON% %HERE%../tasks/_vendor/invoke.zip "%*" +diff --git a/tasks/_vendor/invoke.zip b/tasks/_vendor/invoke.zip +deleted file mode 100644 +index bd1941289b427b6cd6beaf188c85def58ee4ce33..0000000000000000000000000000000000000000 +GIT binary patch +literal 0 +HcmV?d00001 + +literal 172281 +zcmZ^}W2|UFx3#%>wr$(CZQHhO+qP}nwr$%y+unWdm%e%L*SWotT2((*MyirAGgrn_ +z@>0MckO2SnfVo6T{GY}D`vL>N2C%SowX-szQ&ENh0OnDXQP)+MQFn2N0ssVg0R#X5 +zLH_rt{6B&Jn!y2VouedBnh_HXfdByPK>+{||0AHMXJKpMtfxn7@9}@M@Kt5h*Z=AL +zf3#ebwrscPp?&7m;CF@=2k*h2Xvh`w`Po2pI(arDrN=a_CzeRkc&@j^HXD!7ZQzSj +zoZa2s#ZyKOutSAiS9#;Nh5LxuvW*(y#(kfdS5&EL|<}r0uPIz9_7Lwk8Y7SA~>; +zuncf&YgnG#RepBf%#fTDq$hTt1+tQSRatE5Xh`dMsSsalOYC*8EwK}=Ef1uOvc$Ox +z+=bQ>Rf&I6jA7L20dut`qeSG|A$vVhD`8U|zYuj*&Fv~~&q(v3ch?63pY3}ERz#86 +z4cj5z9B{MNet!OJHBu3|n!FYW+?b`SpZHsTlQ><{&C0J{*Pshzxpxn&=d;ADh2eN# +z?IOCfC@w=p_r@m`yyS5lhHyoDdyD%IQv6hW?kvvO^jEaK8^Q2#!^j|UB@C0Ie$h=S +zc=B~*Txn24QU_3KOD-`7id`NY*dw~$-Qsb*bai#z-bL|d0t@Df>E{M5;^&`E4us%n +zP%Lodv(=vmovd-+9=?U0P#e)BBYfPFC7`th45`j +za*zkba>-oyuRerp7NmlcQEjG*3aRhLk$+Sb89N9#t+F-3U^>q75Z{0-4p%8UsL?`@ +z!zFURjIS)Lol6O6aha%OrbeUKzhjH`JltlU?*ZIQ+03hjPV=frs#+VLp*;)6Yru8~ +z?3X*`1npR#5UKFRUbk0+zR%2#wDz+Zr_ji{l~3N|J*7>N0ra> +z&$|l$#{YDytBIqNg`MqxyOr{PcWYD}ofHk_?DSmJ+_cnGT%20H$~^tN0zLDB0{#4P +zWY|<7@$&QXbK?WkBeNqC6mz6hq%-p2g0q#SBjEoi8}Sec3D5RDol-;qfEq3U0ObFa +zjgg(TwTY4Q|JaJS+K1ZzQaB}AHcnfusXw`T0i8@#lPMX_FIjdw9U09zSsh!&OYV2R +z_r}EH!PLyi6Ntq&Hl&|huiXH{fD&69wWTp62<+_abaNhn#&I$`u9j+SWoW5;Ze^qG +zXml?&KFaSo6_K4PKM7566R#c?xYxC{gO9)aaR!XtJ6l`#_vV{gO|;jwPCp(+T4sL^ +z?Jg^s;BppazeVA`(0uL7cF531b%(;x=yb0oZ?L7QrJAm+bxfXL;N07~y$kkTTwO)| +z^oCvr!By^3Jm|Z6m$nwk2{XxxpqkXKT57D@bj@ayKT|l@Zfb(~&hl>m=8(~m9bCy+ +zIh!S`rgV@E8g8Ve@M5l==S=89Be8lMdjm(`$Xw}nGT}ISFz*!5W^V?)we`?IY3&Ry +z8B+mMpu$2--e~UJz*4Ow_Y^^(kWXbowrF?M-nik!ciXuv;Y*a+6zKlGsM5c*Dvr8> +z$a0|Aa$fKTFssfYd|J>sEXQ?cgYzzFw{Zp-GwS +z@Jm1HP< +zR$U1-I$ed%Sh;L%4cZ}{?IYIjM3vSoq_3D*y`*8il;Ao?Jz01N+=2OeJJgr1+}zrK +zI1oK~`1-sCo>W!hf`Ce^f--WWo0vXqX`?sN +zMzU<~pOW;&6|PgOr~y0No6%c2%DGWUL+Ww4kmR^$@M4FX)$(F`wjsB6f}H|A?%o!L +z3e4(uwI&IQxkwOUjTf}JgB^`Jw$ +zcHb55b~s-_eOWeSM@KUH%jtqRN}uE+8jzg2xirzvcwx&r#|$*7z$GZ1Kh<8RDrYfB$Rsr8$Z`xxoG5yOM${Tf7vcll5B2o0u58PN9m`$FB@Y60bQ8HE$(<6Q-|*eP3K8_;{OjHX90Ns#9} +zmamyWya@#W$A?+Nf8&RISzBR`C(J0=U2z48Ld8ilz*l0yH3He8kw=R(J0d@VT+=Jl +zD6jxiJkIw|l8<1*cqADihx9MSt^+~cIkyNgAQJCttwT$j^TP)+ZO$Ua%EkH@TJTfl +zOsv$oEC@}*e56!gEUx79GFL~dco9}@A~_iePe{a^3gBfeR*QRTDXqRPTv2W@!;1>U +z?H|OmC@(m6erDcKH|VbdiswGlzbrTSm4x^ +zRz|Gl9)@WqXB7=bEsfIr6G%R{E8XwfopMZYrE+@{$)Cm_`^t$C_u1o_q(2JX3QnPo +z=L{x*`_y*T-!zlg1FE_Z-}T`U{`CvA@6Ly`{DCR0UE#kxv=28bpJ;Ce#a|6ifkx5p +zejZVvL>j**c!*+X&ulWz?{n*g@?h1#<%yl)Whf8srgn*w?(q?AZE-umDZ^E}d`1_z +zof5lVjR8FC*{tRUUWMDyk6yirV_p#$|(9UjT1Md>o^RO=e5 +zpw@gNRl!j}j~R+!+*~QgROkM_@5%t4!erdYMz}>-bB0Y_d+`0R0b2MSWm0dSu%u~2 +z5FirB*JiV0YIbJ{4XBBO?-W6Cd8W44tk=si)v!6Q6FQ9_H)XP{Xw}(U!;PmS2zGKp3`v(0JmQVZV@h +z>cHwR;!9|j&KI_r=I>~#`7}s{JCrwHL1GvZ_LPwJ#zmq`-EAQA)byO7e2aDpHwdp5 +z*)Z-056juA6j9N~)TfagRzWGNFCw0;hYf#xe#B6 +zvRe>H0(uO-0ql*?ka`<*(xYco;EIJ*6h}C~W{=>sh5rwwCCnD>C2Y&7C~RDLphn1O +zVid$FrB~KaUw=T|zUSD;umW2-zGPQLzkkZ(Y4e6~cL)#`$An1m?}il^xV<3V5$#Pw +z?c}Y6x{%RR1)VomvwIdTm +z&z{2a8o)|HAd(k(GvhE1DW+B4jz?Z>00OstZ=Ke0w5hD9JHAQpIS@HW +zE5R8LF_#wHOzu10{nmyM&<-UFnK(vLCBOklLzlhE58Y1JW0V3vFmgMn>qnp*YXyUn +zHgx13^b*SSDjh&{*Yj=-a0}oV2g3-ub%op=!Op--~Y&{w=a09O_HC +z5svrT;E_g>z=|d@D9Bs_#sPNQ0nH~ydSSbDT4&P^o_)O8_{a0hXF-2X3{cnn+&QH1lXd4tKh_-f +zB0RVdC78cK5D@uyQJotlOM!Nifaz!0>|tr_M-zvV1TI-=!^NY~NN1x)oM3*zb3Z^N +zEN<{D`qUi#%P~ZqFb0y?Kl@bcfWf}v(u-eR%VRbLD?=sDlVEzk#eY1?w9a&VGMu19 +zlMfqLy?m-@mboMr$&fJ=sV^lFUIBLj%)uB9!LMeZpcXLYyT3Mjcj}OE)#W7SvtEQH$%Xr?;RaObZ5{hR(95Q?EZ2(*|!l+ELYl +zh*l7WCZzT1zu$nMsF8H$X6q3j<8N)huGr2vpEfe3K9ZmUIo}F!kwr-)0UD(G&U#OS +zc^xRPD^N?D1S{Z(pDA4&Or1dtg&PC3VBk*`_e%-2x;O-?^IY3uSB()ncqfmpA;3*c +zIyDcqBdj<~0qJeGzM&n4-)VIdaHz51mbq*yIz%;x6;Wv)W;LBCkFFH!u&)s&SSf_j +z6=$azag{OWLe@qp_{iH!5iK(dDFtZ~OZE3w{wQH5!r%m9Vc#!AQQVZLsCEHJariJg +z^|1VL@yuQ5^G9v5n*k39zK8nku=Nt0l=gm&l?9qda$vbyB_NiAR(??}+vMVeZZeCS +ztLvqvzZ0tQfP<*AnUOWp?R@*rjxh{zAl~oO)k;z!8%pVLfNk2Awxq +ze0Vk~V6E~>`h?)U)*dp6NxYbA+;hZ0aSSO=6`~jnt2e<5h2r8O&`~BG6W4P^OoW)4 +zZ#ZD2T3N|G$PKB%)G@9HLKUg<>lQxPgmDd5mQK>jiI3)sh7(n}%3^B0TXBCwl>3x> +zkX^S1KH3*FeGV=Mxl}vv_eC0?XdjXHxRpI8#b&neM2I!8Fut>4dd@YNPmSY(+O*ze +zFzDFyzQsKQscZ%*v8fdMJ$nIpY0YgDL7ibs_VTw=@pRpp?3N6Gb5&m3Xk;_l-7%A< +z{!o{?L4k`*7$6k3$vsi_8iV+qktfvPsELU4rB9JQ7wekYoRv>!L4AWMj1Vu*Fm&|Y +zseU5Hg6xLj*skM4OlQTNaz-Jx>ryfKfp>80nY7=&VjVS)o5Sp1`Knc5t03 +zhNi8q53(!Lm^~OhbP58xj7{oqj1CWlfz(&Mz2_f)=#H;dSjBCE3OA_n +zrW|%(Xgd7Vz)yfY#pLZ&*&g- +zm!h_nTarwUn7ILknnkz6b1l7C=9`J2wsL*!Gq0Bf9AR-V#^q$=UCEoGzo(wM>qp*w +z@fJPc({J@>bAgBW%K57AH&U2&K=D9g3m2VlektEtvK-eYMN=`9R;BsB969G$3=`2l +zvwDqHH{^ez5=pSt?cu#Dh8O-D+eT3U$x&jEA^hG=5l_fV(%;vihr-yhH!{|3?J%$P;O~O+(pMfY{8ewMaqIE>$1u}5= +zM}YqB@Y?5Y%Fzx%xo6smOfFjyS4e4Z$$gov;>&ND&4bDA9#T}Tsd4`0? +zumA>3_@Yd(6mIGrX{3R4$cn>_)~&`Zuig@UFR33D*@ad@-<1yY)YUU2$71Fi*_0BL +z(*iIgq!@^R=l$@K($eRrnHKz^i;4t<(M!_pu^61Fy8tg5lnnOf&$i^_pnGM|`=pXd +z(_O;b6{3aWT$qiwGU?O3ouym7$pWSaZPHn~%ps3m$AX>@#(a@-eGEmc*n2ZL_7kif +zWf$xnfVGs7>1Lt<5yf9>?04#lUXoAuh#^(nxUen^`Jnk3JNxb6L8K6XZ+x<^e2Uc} +zp^4z~q)ZN1oz$V4OrZWAaI%lDs8USuStllbKh%GE{8C-oEIk@Ey}WB}KaeOHP^% +z?}7d=?CXBz0+^jPcQ`q19zs)!ecDBy5nU{2Y=&1WjtV8pvbxK=})isZ_baG#^F`$hBf!Z +zBYgjzxlknEgh?Q_ED+8P)6*tpc?wz+vGKeJnNp?^73pMPZQe!z5o);S=Qs>K`1?2| +zsTwNpbEx7r7V@GY8L^ds0R)3|l@7+a=Ty0IE|`nfEJ?Xot(>O%R8U5~6VhT2fV177$gHYnUaf2#JgxKY(^E&`c)aKluKh`G(2ue=wQXjo8dG{cb}>gA2$a +z0s-?_FwJVp2?E(Tjp#d+Ya5O(y`2Xx%MhwSQnY5_eq!AwSTf@Q$2=-N+aTyIK=D6% +zqq&W?nR>jU*+QXmT@Vus1W(j-ysE6LrEZCt&ZKn#6bD_UbfKCh>ubsCeLWIB{sk0M +z_Q_}9CCYglYe4N-!hEFreFp2jTB=J +zne@fb5JI2)D4(*d2K1?$QgM{#F@IXPDt0~a+p<5fgdha<{`xMm;vEZACPl;ORrHQr +zj4%*&AzDnjA$vr~y&vPHD8rAHhLT=44v1Vmrx024_d=)Ewx^*DLf?M;VkBeGiDN*_v|Pl$qV@CM3Z& +zvJewrwCDcuo0-mCIzWGRG7r;2av3 +z!J?4&(R +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 +zK~WQ!Z7PHn#xT6P?pr8&7snrsqEO)BSM +z4|lf(hu}&2m=i=j=rzbyE`}-CJ-u9xtOR~1oTC8i2bjwln9v(OD2b6!umT~PRIqj= +zSV~_n6rBl&iej}%j1|vi$+X_8r692Roz4M*wp?6SRKbd~u5{e$y6l@cDCt@SeuYtP +z0keS5v_>j4i2ZOUhpj|*lqWQ)5Epx@wR%V<@Tq0B?iZwMPD^7;MCe1M?6!JPOPJ$Sq{aBl +z#Ti9SiE$(u5dcC>c-ilDcSXGS~j`aNEs>cXS~lAE}%MaqPM +z0F?-k1S8e++aEqPX1kmDE>-u2dcrU3ig@YRJV&1K>mnD%gPIXbKx<#Kh)fnGG>0#n +zSJC)Il<@xEmQdP}SIjE1Rke);)sPwaCuP#Cyynb4meAU&&QjxBKCLO9N3y{@9z%t2 +z?}2M*I#INor>ol>VqToZEFhM$w8Zybxek1o_>kWtaJ>Z+9;Y(@Bjf`Elfqo`IR-$z +zE1pU4dHjKN;z<%Uu$GuZ4-|fD7B(cdhp8+QhZz$Ful;Z4*WC6YP>byckl-@VMKgp| +z6c9ofOQu-_YOxq(!OJxyGFzP{(_Tpctv1i5}Z4nAPN8O)_m_<0D-P+71nFd;?ud0}Hd$ +zI_98w$#e&@m24NXe6+`lO{6UhZYF|7som8$No{&XCi2j!4!+a<7r-0?k*|@G)jLC5 +z5?o4rgxa#B3>tJBa1zaFqk$i_m2!2GN~#IxSP*3Iu`d`06_udE9*T8!fiH=B0^eEb +z)*NolWq6t|?!dqB$Cn)Jptf3$g%cAX;-fjeAJlZ<;e*7d2>CKU8da}~`ZR3}gHqqmdr2j-6bI +ze66)=f``h<2$23#e9y$?B06+b=?3_oQJ&#^B9AM!N+CZ9mwi)+6e+fCB?@%1R%{wJ +zp7R`|%{zNO!ayj^h1Iz+n|Xdm@(+${xk^8IJ{aeq$gMi`WJNb~GLLzQuE#^KIXKG5 +z?5f_VLp`YMe-DjQO(s9EFGyK9T7t`5x$j*CF7j)pE{b9@@Q?sf@L*^OKfV-+ +zhV0dig~_;V;y#@ldMWdQl<;p1LQ%5lC|Mb$5op1(JmrGvSHHs2+JC`7K-GG86iN5J +zMk?EN!)0hE-8>^|{d`64W#6aFNl@*V8$lkov06)g-ES`)!mIt&{p!teQ&00aRb*WT +zVRvBGb<0d^glc|905|qa(ZWV}AzFTk1i%SRY +zN7Gu)vqO1$HF*?f9ZQS~#1{I?Aas`@!~%yLq?h0|4w40d{lBNH<5PJL&3Cu<#E7`&7`W5FbIa(7Zl%@DO8wZW- +z(yrV82S*W%G*vVZ>v$f%q10D2P!XPb?f8p4h<_3Zo^ZT*Cy_h;Ps5_Ex@f46o^(q# +z-}q(h(wwK1IS5mT@lO!T*W3X+SIJHe5yZ(+4)mciwQka>Ln>)aA9ICk_FUI-1pkmF +zXc~M3Xipy0{36j1fTLYgVSSe__f7&QJz1=kjWKRT8Wz4fHtu|kv4!|t2}T;E +z@et59*s}c3umSKa-wZ4E%NE533F@hMcFd}WuG_=Iik#Y>Z%=l$Dj-9JqWGVnLZI}5 +z+|uXQU-!r7tu{KnZam!kRbRore=z}VC#*K;}B0CMo2TwbIZ&{^I=4&3QU*`06bF6yBscMPb8~kcp85|uo +z$<^L2C&`ZET3#$X+zFu}KV->hs;K78{8+dnds=kCUEy0;psJP7@JZZ54;HRIQL}~0 +zMcnMCoEeZUhVz;TY3!{0cmmV8d&vjg*W;JLVFU)S&yHo>B>Oq*_iz5jS&zpd!u;Tc +zk0OWKL)zg-bEro|^=fn=%>OEKJXwwZShwzsfnHnkxTQKaab_ +zX8jS!{lIJS<0a9Tm*s}x2=1#N;%e!ZL-Vfe6Brr2SKBGO1mq^a(C2bGVDYTvvR!h@yoJy;oKtfi=bld^z^$ub{=R +z{6XiXE^YYo>#=`=9wADUY7vXbJT>_DCy!)|M=kHP&kscMDKtF$H0&;$1!OV60JC9u +zx0?MV51iO;&G1Yyt?+){QCdKn>%UQkya@AIFB>cWt+Tw?_?E@u6nucRgm-yn-r3(_ +zKZgdM11mn%QZM!w7ZkR{3-`~ft(nn2J%)l;hKL{PdMLVEYAGosR(JUcBA@~+!$Y~g +z_r6T?w;x{e_gE5It=#z*c*dT{-nBNc&trw|h9Zo>wNC3!0j{ua;cPxps21(4@D43j +z8AkWUoFinzy04u6C*hp92&+dojxennmZ67%>P`}W+qMy2NMEWAZWjkxS3(}-_Fc^I +zr!h8n8Bc}1)$g!$`@e8`>8OlqZ{n5=&#PUoOegHX7Y8;7C5BcJ=?+GcBrk_ZYpTBF +z^o6DKkOb|AbiIw?vrT-lp+rb%3O03vGsQX6MW``9yyJz9#4{VqZkK=e)DYGX^irLT +z89GuZo2E|Tg0ep9n8bY)5or-K6ZI@Z-uXHL)q5QEDBFoAppMcbY{~?yL9NrFJE4BI +zYYNm6m4t$7c>U|#ZoXXx`_dr*XF5Sh2*WkOK_=Xn1Vc>xIN0^oL&=pzvq!$`;YIjw +z(Cc?nJNdL|5rtj0xJ@5L!^LP7fb@Q&*-Lazux(2)cFp^F5tjGuV-5+vhi_SE9Y}hu7;cjm!D* +z?<7DUDmJWC+q0xONcJVSS3ZF<&M^i+ju?$YjaH9sim{4ZVV9w +zybMk1WAfw*S7AaKY+Kxw?%E`1ak%oMSbB0zPQ`vn0m&%t=LrW{TUDL6sd$LGo(^=gk49sOu5fyS0Ex@p=h@u=Y +zvOrzruHuX`kzedAC+`zJTjCpS1Tr{j+;6?jW5P288grX(PfYUqcbmBg<;XT)VLOq* +z>3+@1`+a`{QeU$LCdQ-o7u07rdG3^M7kdYebR`^#s~+^CqtDCGis(X*GT8>25T>nh +z!G7Zkm48RP1KX=|6Bn_?H4kF +zzr#%M6~XstP6$@3{#dL7`nH|w94R^+34j6Ulirj3Br+-bWCG1|7BwyJgkrJIdi2Hr +zoD#u!pC`z1Y&h(s$Uq(9wCsPGo?RjID^60ALn05}Ya(@~!CX8Pa-;9v?FnxftIQPi +zCmpWA?rO$a{SlX+HKG0WI9=iQ^6$#06?M{-?8QnKjKw2t~%>baPCb +z^dzP9?aWwewAknBj?FXprm(U@yphs^iTBD1X)*n2YCFz^UW-)wXOQ4ja;(B_%cn>X +zZh$F0r0<>VOJ7U_2H3^S__1kg=(JOOIj=6C)-c( +z*Klv`DZ&~- +z8SYKTWfTD4Ezsk5ug?5iPFYIblvNEWt|uUhbIJ-`aLW(Nqgrbq7Ps3DZO`4J^vzQI +z!9dGv65RX9-~2TGkkcVHoCQ5zKeUtX9bOUzn2fAV$P4vq3x +zzhN6&uO=&N>yFu-&*8~rc2199?)Zmonj>%N1kqrl);bx@{t~@a97&+V&A0FN1gq>U +z^t7?JLC`*20#@Phw~C;Wqyl3dE*)i4#sMi7P +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 +z7olD@CY*X?LHR@#_PON~{@J4&Z&lf#)4jPsr +z^deY%k65fo%=hZP-Q+=lYzLgfEy&IwSOzx`WEv0Kr~_ZURDY^UvA!1+2j|yD3^Pw) +zw3q;fzJFm2FYFZ+;(ji#(b*Wt!^{G(i(7jK>Jl*lvxl8=Y|Ik^QQ{Gfm+y1b748&@ +zvuUQZ=HI4Z{28W=ow=l+l$$xI=QF)map!~m5=g#g8Ue3^G8p?24t_U%^(?OFsu=Ob +z*~z%QesbQ!9~DKZRg=0iSFY{^@v|vE0>9jYtxvX)#UMb3y-+aHc214O^^Z#y{EDh{ +z5IluYLAxje{C&*Pj0WjZ;PZVwadU%SnN`Fo96!2DCTYvbX%BQdcU*MkukVvK_8>&8 +zuw!m;A-=@WsjDfT)TlI)7E9(yTgtv?m +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 +zqvv12L{^@hesuGft2MIf;6Z +zOo^u07B~@M6CbYa@%7L$<7JafemuT}n@L9FDdy!^&!Vb4!O^aS29~9Ewyo?&q^*E6 +zp_I#oy8PWApej!DR{$l;Sojza9IUN~eJnwK4r%>V62uQ?;SIm|*4-cl7=Bl`vRA|i +zS2Hy#lQ@n{nMQA6I)MnMAu4XrCpam2+{2qh8RtgD)EvKVBjjhLyimKj3DPjwkGjFe +z9LkR@CvlTB@9;8C+V-DhG@&%=I>E*LcuUkd>%eQ?a;<;lkSg-a}HhGg| +zvMp%F;aFVQ_lY`R>>z<61TGCz?yZsMPcXHl`t_B(B7%3z<-l@g-7sZC)B^4c(q^4r +z8jW>AdPM?eM30(=Oy-{46mJk +z-bUBJ;|2ZG>OYQLU54LW2^ZSX2ibl%zn0~a_s~}X3c(E2-g&u;9UZr`X&9&#V5E^j +zWP4hjk8XH8u5@olM3R<3qM8yObO1r&}J`Vkj2`ujYgZ7 +z+J{z%aIdO4jMrrag#CyNFe_q`WygF06$F8-W>sx%XqC;_$ss`zqlybVfmjU{uR4ge +z2W{~XIIp|?8T4%e$TO($UW7}?SjwtBHZA-i1tqj9Zz#DTUb{!lz_?%x?*bR(MbGY? +zFheJ?eK;5E;DBK<3TJ`GS45Y%!+|_^pY>NFyXT+b$HNV1Oy3?lhZCQ1|089>p`3eVFWng}PR~+T&+E4VY@U!$y;`)Bn^9(wS^IPaoe(8bM +z2cygnb8rk887#q>MQ0}6jt;kHhevN%Xt#&AQ}-`icw00Qkx#r1j?BOZj#3!))P;Fb +z1?m%!H$c+8zzq9N>Ys>v>}39jrZ^7fV{T3RpiDJ{p$v9*C`v&3FhG=oWPzGV6_^+i +zKHqbk^njbi13JA@gq?aVI~aecGD2l-{C;SG7kBMmTUc|{xVQLD1cKR|1_SKQ-dR9A +z7)E&9n@Dc852wsH%UL#t9wf!ei#<6**?Zh=m|i+P@xF&gfTpL!Y$vi5k}VRxY7P#e>Quv(s_-^et8ju_YHc@AhVr2EQI!CxvKS^XRHVT3 +zv$T{j#AzZeL+t0bP_f7!_U}>oaSW;j=mFs~GQ}z1o_Cy#lrVZXU$?e2AHgIyojC7( +z7qC!X*W=JxOl$AE3*u{ZPyIcs?0oWH`u5Z9@$R*jVX$Az#)#QoB)r_;6Fynenx`WZ +zOkX~e6*}H;o^LUYs(3&Cx9s>3$s7DxZbOcFvGA3Xti+hy3@$~IkFWM`wl#aff{E$g< +zVR}m~)vR2vivmpA0C}pjOZfFl>Y;gCD?`QA#I$Kul`&X8O#~;#lHc|@bTjVczJBO` +z)HkVgFDOkFe(zy*KGQ&_EZ$UR+M~FRw!x= +zacMeh4OfYZXs}n;w<7ACQ_%bx=aJE}L|cou+Q}AfE+An~3Np1-u!xP63ON_RKpnk9 +zpF*%Ceox8-T&|>nc+H#THcKD7N=ZH|LuD@$vRIb7{~m`IjVvDG9_L_zTdr-*K}7pTM_>2;^BPF!6Laf!raQcASuV|WYPAXbO^3g +zJOJV;hD!qyC>V^NK|@t*G-6M`+Mn=D5-#4e>zO5g+U~4&DqhR!>h48Y7euTy#aGMf +zMv8bF(e`_{V{akQt%QP60dwMb;>cw^iJXn5z`k{sVbBHAC8StT#kNpslz@MTBt^eW +z60#pw9Sg5#<#+D{osz7D@l1A3PoairmE>?Wr$WS0d4RPL5X`;oyPWu4lb4VdxrV>-$609%^VkK=E3{mv2e&Wm|E5X<3>Ogp6+zV#KUWt +zT;=SDV?`w&#)aykl)my2wQBm?)3$CiNqa%tWo+CEZC=sKWDUUf`MAy!Wwp?Fo~O@?xTjQ;A#e1O%|lGeQG9R_`ED6QwP +za$+q>ry2aXM)+{(7rtuQ*0*NW7}|-86o^Q4!Osnibw~kVq7gLD3BBh9N>$938vO=oE~^<9$Hvr^PqLKoJIUambw$cvFjR67X$` +zx|ZIl{p=UkE>I(QSTD1L)7t6AtFmoZUYn1fy6XB^xo9JekSgHvj9>zcWG>1$WVkcx +zrcKtZHS6cO&t5@0@MQKl?K+2($c8MtMN}?PRY6<_fkkD3$Ub2)i;Dxu2*DcKmBTY; +zgMZz=SYLcS(7J~W?cz(t!IWrYm^yXPQqyfx^#IH60plvTX?>v)05UG*_d*`i1(GID +z#o#Kdj4i{>;7rXzK+Q*A*T}eTVUoIk7{3%jD!C??nzt_j#ujuT5%P^?aIbwFas0++ +zba-XH293&R`7JrW1+|vTP&Vun=*)+`dK%He@M^B*fRs-hB(Hn4MO9HkgEjS(!XAUC +zgo0KC!O>%{lfQjnXRdTG*9wbPG%d62M+-=EwwE#pZafBO4mT%0I(9tt`>^;{2^D5$ +zU6~0X4$G&XP#Bq67!b$Lu16ugXoDPvSwD_he0S=@kHpsQg3)em{6Rc1J^FyMA^k}h +z-F$R=uXFP=$5ilr<)kDflc>=W)1oAwdwON5m_Uua(a=0*uJNkRJ$~pKCN)32e(nud +z6W7GZN`UlIJ7O=H+DX~O9WYO(OVv!-K-d8N*Redn3T%u!Cb4eMjoohEnLo~9 +znoFUFJz|Nl!Aspy&HGFr+v~IJGsjW}uznIQzFb@nnoeqe{7IxwDSO$mCxxf-EMH%V +z*w^|Bljt7&(m2S?X1w&MEqIS!r0=pw`smk^wkS#)jt$!Yi>-f-O>^_=UAlJ-M2i#61)H{NicsNj`Eu_{!YiAGPIhuz1AiXH8&kj+nba#+l*Jm!EnXL +zo*AAeSg)Ir6zKVM@2CC +z+0agMihP@_uzN+3vUt*IsI(s$?AKy~m^m-_2Xzo)!Ien6q>pNV*2le{AWM2lbJ(sj +z#^YDJB12)s#{<<*B%Y;Aw@S4UJrm;SiBoT;+-5}q7{6<>QGV2G%w`y&Db_O^O_%xP +za>~8&6sK*f37=M>^Wf!QP~8@=;u^j>1(8IPheUc@a8TnIX3h+&j%m?s# +z;7vd?>^xXyhyP*={SVlzgVlN?90~y7?r+!gFSgJoZYD-9&UTLf+4KB6>`YV3Zj&9= +zXSQ~)gx|=_#)T`k-_);Q8X8nx8q6H^Qy!RsW}cO-p^T)QeW&2B6Ejz6lCXqpi%&RB +zeCX*%HdJH1DP6;sGFCT=haT4Du-K1Fy@j6p^FiY8C#IgNVwZ`Q+n+tXL6qoKlg}Rc +z%My~yVoygL1&tIahgdPR`{%|A$VCL#kK=;_Lb_t7gBy|KXf{{V72k@jSvy)_-qh68 +zuU8=wWV*Q$#2KygSteNWJGA}rd^0T<)n!BO68iY#20eNbCUABac?6P +zDkg7Reb#olx3gFyoCuYdde2tGXeE$%+Guw~8JP4x+vKO`1XvCsi^01THWHfaNk06v +zAE@<>@E%0QE?JS!3d)YP!t?4nHlCT;2MH~)>{UVK7VXsOYMS+kTuP|I)K}*EpL@V} +zfcF&iBa{iterX%4KWTA*k;(UlmHUCEYxNf_rl1UQ55~!5)$52b7Om5|Do3Fd`AMV8 +z5nuYvlIVGIDF}8KrelEKH@`9WkA#)#+w=6)i<-fP70S=VF}7^C-CW>rH{fS*v$UEUDPS+*= +zl-`Yd)S|S8L&c)i&u+KOfMvGqVr!)~Q7MN92CkbuG606TOfKj+J75DVFhtd^f6fM3 +z@>7h38NnBO61HmI$Dn9KSLESWII=v`>oPkq;2$;ETHEwByi82EjAM2|Gk`mZ+|cIp +zLl|8z`@oowAhPcG;im6QmX7Jt4}w-OgFUF^9pgenyK3wC;$QARKeLF?RwqtX==YIA +z(lF|^6ZjclWdS*2e-43|psnd3k09l>5&86hYY-`0R#G@ZbUKO4ku9*QO=_b6SyufD +z%eG-6Pv2_c%6f#h5h4e?%}>pvdpIhqBcK^ja=arJC5yNNYkYzsgzR9y8Y9HA$qwan +zh>DLD*{;hHu=5+whlLlPwWI~-20|#!i#8N5p5q9pp3k|K>Bt+yP|&Qdsm+DmOY^#{ +zQ&K6VXN08xZ{qSWl1`jM|Zyx85i8_#B+DYY)v6E$(tsy-^gvj#uRlvTbZ +za@P=Nia5GW>D3aRAaNqCKX2u7!F1Ky$|9F}8q70p={Kd|81Mo3;7}sxFIEaL__`-z +z@q-@=?rUdBpFnBL*L>1tmF*`ohkCxwF3)`9eo=;I^5sTKB4FRoO}(qv$*6|tr^Z@3 +zuPvzisaw1gZ|THR&LSh%f(jmcRK&Ztp-=fF)ssH&;7l-oA^Pp87?X){3A>?F7P6wP +zB{9k9;FdaVcWp-=bdBHk@FKb{$guTFgbwN5518u|`9m;(1p;=g_9>9U9;)e-#FWie +z>Efw!sQD9S;Fb8SqAH&5{Jf;W%e3NR2JnC2N&sLrQ*F^e_{1)PMqv~vSX#!?56LAq;sk3_^I +ztIORLLuf=Y?5xdRf|D0;OLGG->uzhZGxb1+xt_Axp6X6cCgKu!w2CI3!3By1);qYX +z$xM4Qs>UYau{2-`*j+Y15zM6nUl};6r#+#5+^xFCZAepALqPBwDdx)d+R%UhptrX< +zsgfZQ8dL<;X>wVv1~*D2W-QP$Urq7PGD2ncoA}ikOMT#VL8tw+{g6K>na3?+FaNto +zHu`dQJU|vycVRiTuy8xv)vG8FVvqwj;W%w^Q(^lh*_E$p({3^saX1jZ}tWw1MV@@!H>S4mz(fqBpiqAY_a}6 +z95qEbppYC6&_4-N)BFB+jDM~g*@<{m=hAxV_7}NnAC@J6m<6wD^Nlh^)I%%RZ*1pM +zb=U@!!LjhWt;j{Zbd%IafuQaLZgQPPt%fw)$YjHd;CuENKsI$6P8v?^dF8S{(~DN) +z1PDb=w3^RA2=v^Uazwfsp$$#uymK}kWYg0_9JmFW!l(-b3W|UvH;c;xr=X(R593ZL +z&}oz+#YH*=6oMf0HAT#cZNuN|?Rx0X_*wFmH}Y;A9^OR?u3U9(7l1`Hys12MwJ6H@ +zJL53iEw`@CaEb+y*CDeMYjB}gVArlm^N>VslLR3Z)rYFZ9|2^zTxLGGpaUn-kUb!wQ9$aIS3{)PH7L~ +zGX(Tce*AVY?r>&(!0(D$`Zi+bo37VVLb0<(1MO|W#65;G){Q@&BWvOe_UY!!7uFB8`mxf_`Dgw{o_Grk +zSXs^!KKpm5x7jftX!SgnfD_D#^$~Lk33bI=B>`0Ir8IZSkU0!2^SHDP0Siz_A|S&; +zgtsJv@7Qi|*ysx^^1-S11wf&1?Qo9dt8>CgZl;5qh5yk0{ENrfKOXglE_5jWNG1A& +z`EMR$)^-NQCjYM|eX`nS%qAD0~GL@H6*hDZa`mw$b_jH +zqDq41D#gbwkI=;pu+6uj6n0K~R=SX8YIA)~l-AkXWcFw7nBX${^4Aru3~qEUn+%E8;>cwIQ-b9#Hfp4ZD){MHKY01o#Fg*p$n$i-od#{{ +z{-^707fO1mIi*Ssl@89Cx$F@P%9>sZp!9_kjFEDlI8pxW8w$vGoj~8DHlrm1ycwzW5R{OCG#WKA-GR2PR%lTq)mmcCTxDfsv{m+Lz=qe6_ +z&%YvH`$VlgR7OxO+Ow4OgGCMIYUrIF`#~0;ilbCa;UnALg)OVEoufhM&dcQYBLPd&;6OK$_d7ARG4mBOHjD{?bNn1`xHM7i) +zX +zd4fM?=KFD`3a`m(@OidewAGm%=$-Z=>JTz%Sb5nn|XLTtI^c8x|Z)gP7Gbm!F1Y5w3+&h2x}pHr2=DPukpuN$6_x}VD$ +zb2cwzLB`MRH=rwoutgN1atX6pm)i)*>FgP`D{?;$q%-!;5vadC3fR-uO-P?4rixOk +zbn)GpHStF2H6`Qgb*`QP_N85bXRZ+A#~df{M-45Pxp=f6`zi^Vj#&#iH?LZpt +zf`HmW2w>sPgQhtHplT{XgKyk8n~<}E^GdpMP2=p{Y#x=U%g!Y;$oh?7iX~$x-ODd( +z_4L#p3Ka=x)%1siC#)88U-CGahUF}v+8lDLmbW_{X9md*UUuq0;QfkN?edRf`aZ)% +zMgv8V*s9R5DOe!xxMMDYXOxAD)V^e^=1eQPITr^mDsAbr+K72WB$#kKPtAtUU%IKCK@;n(-I~4c7_~XW7!6^$4wtyEZAw^ +z@w@yU1oZ;rM#=zE6yL}X@z;@gdU{D*7Hlm|U==>r9wh#8(`F)eAHZR#xovS +zl;4_q3cWT4CL1Snpfohu^e{mfl_k}6A-I->-D8HO(rOmLoVgE^m9V}~&Kx@?ebsZV +zQPN?Bj1ko~U_H&KiplZm2^=ijeo9|#>Yt>PU2se{oiGD?Gr$n6Q4+ +ze|3la$H~2g(vleYH=%3v?|}U$i^;~o;=dBQ{tXl(6C(gQKo2kS>J&kcNuIQY&&77L +z6^H=VI!D+wA|y1D0iK0XqI4D*Bd@P +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 +z0=e_(5$)&QJ4=ZFkkBVV2Ao0z;=WZ +zJ05aP;T$R*AZrDWWpnz +zz5V1YG7V&6b3j{%=LxFCY|1OADr??3ZPlUnW +zFHGI{W$U95L0Tqup!#fnQ_M>?$q~AuqV*R5egs9^bjYc_m +zlokwXV$i&ZW?$MpcF(dcv4fN(OrKP(=}b(B$C+_-9ZV0Zl{Lc(lLU^yu|rth@I20| +z80r#+#A5T(0_f~dq-d_05|8tH+kg6!!sEAiNdNv6U7iUwc>o^mr&qw1+{J?j#)SLK +zY<60HDN?jJ#m$IiImS6Q615m%73qsP^n<$byL;b91!C)PNy9`^HkLl?ZV<=41miwo +zZ&{2ntFV6gJe)VMkI9XpQC!=qUtgJN$?Q5Qt)`Q}-b;lGR$V!)K#=)A_2q +z7|XPJZPgQ`L9MU&4{VjkVrv#v&J=ZtpE+KtCzR!RxLJkve+LJy=)P +zQmlIG$%RBIpz=Kid4GdV7KUkslMRb +z1)C!EIb8=bcef)1WQoHd!U~gzZz6QpEZ_9=YW@fmysqDER^D$e5xj&2esd<=JAc08 +z`Q~3iFtls~I6RkkY7L*&*7$kPc-{@g9JWPp*3g(jxZdlTd$pZ(=d62-S`+LJ>uFMd +zylKaua&sPz4r5v0y@3zmK*yYW8$mYL{Ey5ngR2Ib+Se##zNs@}*I+_8`<;KA(OXm> +zci-R8oDsqKhEDYQ$OwPjvA0<+^xvhFryD +zse3jdlR|6lN3j_efQk5~WIs=xkW>q&*oMZc$-x82`4gUh`3LuW9Kv8*pWo=QVv*ED +zSP!c1gl4)pDiZGsC+Ut@WQLqm>)x%BC8)Y|nxt2<8p`1nKhbynRDw{1eSs&w8D-_e +zb3zj{V#VfyFkR2C7p^WefWt!gRE)TO1^1-eu>Zl)qZB3lYJF%+NME`>>OpU-Pw6xv +z879!)CHsXY7AJIWM^F-|b1&pMt4iqMQ5BB2>e)4}LD*}e|BhRfU{Q94oV_mcI33>}k9>tfMn_PvMk-=1p(c^sg~#-`+t_bdniV1nY7DqM@mFEFsb +zd@vyP-PL5hz$&9YE0on*pO0=|dUojjTO6OA40t+L-5N&jebwb)0mpj&0lO*tYHDi*4JsZQHi(q+@h!ot%ra&tA{7 +ze!{$%b5_-;FXlfbSkG@}<#>ZyARoXv^l`Q_Pcf^e_pnGUm^OZRp +zndRi>E?(O<1$akfg!s`&HO^G7gjo{X*z8#q;$b;D5*w$7N3fB@xFx0%plI~LaFg*8 +zJIv!WR&EfXP=hy$>`>6E1shK6-cVp9rS`eDtBP)2-#yB1ornM{g=Hux*1A#!s#TyC +zceqxDEB-SGS&}Ss&}qJrAFQidc5nZk(3%+W%x#jEpfl??pnrr$VI|pdwsdg@vdVtzaGk6C`} +zC%vSyji&z*GBjQzTqItJV$2DU()=TYV2+htt7(Vk#K>(%@3<9l)KG4t^6MCp3*+tD +zAv~pCTQ-`CEqwRHkPCFXewxOog5i9qLNjixYP3NJHFIeO5r&=J=c>5P&fb>m0x1kl +zfNLJ~kDHPOeNUH?+$j7t0@2>Xhe}F54xjhyyF2v#Q$za3ZKDPYp7n3QSTLN^VH&|O +zYkDBZceF>|DJL)F?bP1Rwc4${+7n;qRCg +zN|>;~b>P{6o^z>%d^Ua-yl+XVYLWbIdiv*qq~;VIu_WqOIw} +zaXDiehz0764h6m52Yq*X+EIJ22zb&Q1k2Tpm{!_qL;48a@v)NJ!^=Q*m8=C%QRFqt +zA&`^WAlv#W(T!xNc9y{p#gpJa2r>l&Co0hP7I+d(%~0Uf4fv=0B>XWVmi{#6zu*>t +zNjmV~CKzQQOuW$i;%S}_)#WJ#M`z(2X2qw;a02D{1zE4}<&!U|X$?{JrR>a&7#Xat +zqMxf3Oi_5+fX;gqTuT=|)$EHdeFFUOb@z7udc;b@-^*WQ{BLhDQ(tf=&xf~|$BjK> +z+~?NToirg=$Ktm6<$(wFgx~;4Z#-<^24xKQD!3?f%?;c@IC*aw%WwcygfX(c6LD9_ +z*#AaVkc_2SJJ6zxXcx3*CT?Sggs-(lGteFta!v*AHpDt#3>Fkd3QO|TM +z?MixO1yvqSpQZx4?#LP2@B3KS!16p;w#+i3mJomJp)#XhA6nfgDepzJ;e_lN=#TR| +z4<`}~#lIhmaNhRr*=-v1Bc=X}1gDoyi)s>iNZJDxM?pFM1xlKu-D+?P90i#8tXWy( +z*J>ioJ-=N;iup>-d|P$EZqcAOiJPP=e~06<2Sgd=9N-e(+z@0!1(f_-KKzGKt%)_n +zNs{yn;umTXwS@Gd4O=3^N1Ox!1(#rqAJ~wUP!I~gU>$%&0-O%5aWjHQGgtzX5mwZd +znU0l)a;%9zS_wZ#$IZSjIqI?OKF{G37Jf`BCRPs3(2(6@s9nGb7X~Tc?u`22l51Et +zXy1*~=H{jTwt(7AT|)VQ(N)*ecQ7J5Y=D9&lK@2ItUr?DTLkT;-OYhSdhXC(Ea3e4 +zC1uPR4J3|AIU8kkelUvd(GP17=`S5tSdrr*uW@u%w}F;&kP +zMD7|9gbAs`wLgd8H5|?yM9iEvAf1&4vE#L0vfF1b&*kbFIIx+12!V&|TQ61PTmC0e +zF0wpX8Gk{#9P8VT^|7jV)n+~_6Fr5$w+l9W)&n4xDPKR2*?ZF;O!5Zfeep~aSYtX> +z@5!kDx|}@)!)1R{U5zpovQ3>-uP#+sKukz6)_$}gZv+L>q+~Q|p+(ILcSS__&#Zj4 +ztvSGVsUx6%x_@)X4Oo~5;=I+LxrUqjOi&)*7bPGFrLd{1)d+}4I0CwAwFJIJN0tC| +z_hqM_zQ)HR;6x(7k5=;P24B?e49L{`mpt-e>~t!tA!mEVM#up_e_>73xZ1G646C*7 +z3d2bL+-cSrt&mX1*RLp(#IU^$B0sA_xWH!>-dZ+C>XdY-gN39gUhWQi`cFGqdU97w +z1W(1hYQo)>a-JAI=opWUF0c^oa5=3w@Dtd +zm<14oSV5Gc5LK`+_WZa`+Gx-7qc+9$NY8_I#6N0TZ#Qm;ktvkTFu>?>fZf+L*3y=L +z9;JD@lK}Y+wXK?8W697}ngx(RtSOH%@Yt-%Lt5&;4vQi{!P1chDfTojDTp&#f$&2n +z$?~545me8yHX*f{BsQ_25fm3@EqD}Jm@HD7cR8N|jmod!$if^qe<0KojI<95XL~E< +zN#bA61LbmhxC}fA19U^&o-;F$S2eNXpPxPjo~swyYhN?tK)?O)c8jFP*BgLaKNa&4 +zplGmrEz;z?mUIUhQG}SIW|=&9A~Pa#oWu#5R@g}&gNzmSBr*c8K=qDnjYuRH_9CGUC9Wgb7;bY;*kja)11B!}naBAnVuk^% +zkXGPGQSj2OdZrpMWHBDCU@?RrFZ${5Nsx1|0B18#lfNxXiGDClvOsw^O7h&rBb)PQ +zQJ}KZrAVW{hWrlZZ4hN(s=ZGm!N@&g+z550x(&J{UelNyTqA%(!WrEv3F^SwutxBK +z@8^|lmAx5!4VUa0u1DTj1pjUa7u#GH`aD&O>Dy#(lU)}R791Y7&5nAaH!!FobJQGn +zL0g3h$g4XxU%5W&Dy&Yt@ItMt?tq4i4<4~(17$=YpXC7$=z}z5t2GX1A2!9L_*I*O6wM&BAtm(s@)d%pQw4a +zR-`}sw0_r`<^9Di$hNT1e-?GMU*Wi)pCQc&r +zoCzKEV>IE`kz7K|nE-V`Vqj`@olD(LG)}BST4z@fy$cLx<1rJ7)t|(*VDcBht$fRb +zm&%j;0q2zB1L+TuA&BWqM()fG?`20uY~WlPWpr&ekTFNL0_kP;sB!4RWNeGQ(XA(R +z7Mn>ufbpj5!Ej78(8cXQnt;|hQ&@OGzjMmP?wXX+=G&eeTuk#+Kj*Y`Ll~RWsnw?J +zfQRnN^xF_U?hl6Xh!jIy?T^!&ghMrpVuO0WS?AJ8^T3qDm3od9;vXDnF00TIvX5A{ +z*uW~e0Vq-Ju8o{A4NdAlJ>;&${VM3!7nyh5ZUV}lL$KavFa0{**y#Wd_eYq^0H)Q{ +z(Hy+{O+gkcif`>yFoBYUz7!KrbAY`t=)Eva<8U4E?}*9OE$^9z1`s2@u_0e88W|;< +z5=uFp9zAmS4=j2La<+~VWWB1fABOL5589}4kHV@BBEn$f<~!8Q{Ty+)R*i(K +zEBR@pdY~QQ)WbGHjtyBnA|Wy_oW+=?I9Z~&o71f@NCMRt$dY^GC1s-a$2ZZx +zASCQdn*t~-Io1$x_-<*U3=V4|XCV*I|M;lIZ(4B|yIhY^0_5B#7?3j)!uEImq^>bz +zTgN?o^&X13v6Oc8x$0p?VOTqXU;?h6XPc>@%-*G*{^kDE!j{>#cdTTEU=EYhGOlAd +zx)X9+&CEvMN&y{AUP7x7CIzmJ%(;uc**8ZTHkWhDQpR7@6FqwyVl`jn+sy_Aru6Ct +z&q-3@Ek!8gGwm&nDC0(lFV~c=%(;Bs-K7&8cCG(-!by2JWY1@>p3^%1z5v}VaPKY? +zz1;mT$64l2&V~#29(g?!5YQaTe>l$UtPPw@Z5;srZ^Yp8-%~cS)i)hB$B?{F)E*`f +zz)+y5-S6v)ldOo97BW+h|78g?& +zw@cj}9GKc@gEY!Zc$S-`?^JT+3lt?pzGzrY8sg{m>&6^pKFnCkWkiZ4Hw69_@x9ixSr6gOLl!-qG-fH(7)2eT_mwiUiLs`2^}~% +z6~z)%{@UoWDFou3Im7p>C;#QrMWuxZwghGHc~MItd9*bCE@1(ed48$Im5cy0g(DHd +zE=58eqM2C-8?dDmGE8^G_0wbOyO~Z5M82KLVjo#7qujQaOaNT<=VrUbMo>1AB62I& +zU}M}~SQCX{N488!wMplOmQ~Lm-fP6Bl%{NtTMsSAvL%gW)CltMuk!+#08dQE{i+T +zgjgGNy)f2@yg|vPV1vEK1%iu{#<-pD=x1VO0BH){^eQrDj8h=jM^QYMC*`(k!4%4f +zo|AswVhF21%n&zwo%~D?o&VZjRANz2t13Uf$YGR=YZVbB^`naxgKD4pi!31us2Aax +z#@ty^LhvW!t^R}`XV^!#{?z7{Iy2@zsU$D~YwpL6Ss5S8749?=!%$70nL<;;k#_jQza92*j+sqrn`>V=n%0M4qoGS5{MbhXuX +zi;4vS;N@&_P70IwDBiJvNMk?~`YSFv;VdlW$}*T~2;!KE5xi160&5+hEUYKr_PYE6 +zNevlExI)NVA##Vhmlx+3*(kfsXk9RD%=Y!$KkK^^bZi8T-mFAZK)6El@*$6OSrA+q +zI^Ba&me$iK;|hKWPX}N7OGMQ~%bYu$@2nsjnh*y25qW3cd$__ih5i*QM_K1cIvB|q +zW^gCx%-Y`DUopI4qvOhI6HMzW@8iI}+Zt|FG?rKSkr&hbF-m{j+)Nt3Ia@8;=|s)! +z(tOq}^i&7hS-01JwVLq9-^a14(YhrV-#OA~?f@RFMntb^<0dGjN53<#XMQ50p8_LT +zUP0bo9W23=9#XVPLTvDkpypi#cw3BqJZQ=OxbUPOl1*VmOMqv*#pX|0EomW7#@g{*hHl&rX&2^5+YCf60CTr? +zt(oUl4nxLOMdoT6+mWT`A#r$URuwKL9B?T)uL10mxNdSs8s7|zVpk8IuOK51qkplX +zBWwC~(6G3>6YJtv8@%#<6vbTjlm0zCe((%cqd_8htSE^--UgsaQRe`XT1`R!Mx5GqIh$=w3x)l+H+40H4} +zi9P>Z?+Pg@dJiLfn^7ht5?=FB|0cGOG6;ob988X_AzY_6o>C2ja)LO+zA!`BBWIPlYgDWi%?Spz +z0R8s{vihNy3;D8AA0~JIYlFO5eFw;`Fz@go?ZZE_4=ojumbbHFt#*+o8}MPh4lG`BBjkDK0Tc5ZaO +zs+Wxi=HvgQz3q{I6Y!)t02t-P=fTriN^b~UnBv=&UMfV$ejtp@V^Cl2yri7B~9S2 +z+*x3lk#m}pbv1oDstTH29`TD_Kq=1O-?%Xu(XcygqYW&`oT=OfxXvVXpD@# +z`@4+AUMIRD(Zhy^ju;=%JN#m_W9E(3Ub|Px{i-Fd_u1Qcdifhvn@R;tHOfT)>HK1y +z{hc{#aSPO~U8FTSy5YqeyX)K9r18qm1E +zt11lwhM{sh&wTzs5ajs^*Tntz0GKHeK-+`ShNl{i0%=m~)^7e*d%I>eCNyDImaA2B +zLyv4nyyBa(m^JJd*#8$<3V({9HHHy1SWF-wd*%OtZ`e85 +znmHK!PwnIX-)o;{?ahQuj)Wg;PTo;i%3;b{&35bg{FH}xx_#89%L~)zUUeJ+(vRs$ +z9FAV4`0A$DHfJciXkz)o-Z`{V7${trDBhhZeyN2D!LWU+MMkUqNYOadNQ1S2wm~Mn +z!X$^aBEg*(Wt55<>s-w`qlyNPhXJ>-fbVCi{ZC5sXC+_zA8hB%E#^CA^q+%OpTUu8w2{teD1f&!W~B +z3v+Kc)>>9L`Em_!SplQbQuRht^vP5E>tzP}Ih6lW%iy7ZUT$n~N3gc?+XowQh1?{M +z8cqAE$$f!!qhu0M*Kx!5rCrN--aqn|k0fo5Cjpf)~Or2U=9k*>>>TR0!)`%MMYuGDNk@s~$! +zl?q)>ue)fvn~oh>dsf(y%M?5Pgxo$`qF!^k!XkG3wn-DoR>qktr3l~o&TpJo$+?AE +znJSDp-qcyi#7a}S9)&*q(2NvI!?)zBS+G2~N@RPCDA`?uLXqvbIfs!Ii%J|)YJNpH +zp+rHYKZ}J_HIw=>)%+=9Z6b}2(L_9~s$8Xq)r&N|EE%L)s@vy7Th-FDy+Th88W`70|ek8@Sby_YdQ|eX*o}!gCHnRU8;GHw(FQWX!r{e;ATF +zRg+BD?CH()x^raZKKHsuqr<1P)<|b_9yTR}?U8U6aw!ubO&FUklXdcZcTFLIMU*PZ +zQKlqW1s|zV6t3zYZ|5)aF=mEYF};I#Irkc?3*wSg&b;QBj +z5=CoTJB~3H>mjs|mrQrHOz&R?mDfbpM08EbrH%Y=P6WIa!=_D~gFB*QuQ&ro}^&~bR?KBEk +zh1EKJzKU(2J}n0gKwSWdzWmfOFjtS}?F!xz +zL{YIuep=KD*tR3lSq5w9q9+ie3Tu$y%b2`*8~`p0BFvcsI|2KGG$oQ5~16hx2-9=YB&LxXlbpoh+cqme)dr +zlf2s^b{A5eZ_?5%8hkd<%)_WB`MQE;JyrRH^x26%5?28JT*9tPac&_o#y#&`OB1^# +z)txxo3RBbjDC`fat-1yW7)$&%9sMd3`K|;b94(qyv&|}(IFE5BugI-2KD9#;stFz{ +zL3qA%Txs(gj4x0YUB;123Y%v=;x`gcQ7rM>wZc;9rUZG47}tyXh~P(QtKcLlZj@=e +zG4Ua~!PLV6>u}IlIkyMsEO)FE&7%7cTS-dSu5?`VJun(81umAlCKc5j(72hmAI0bm +z9S5tYS(b?jf@+vaqzS!jAT5*BW2i(Q84XQyO6pp|(cX~I9@UadQ^orM7A%b(@sgk; +z2myHDXcxzeRjmvfaO8u5dY+#{%Z +z5I#6cISy$W>l^6VMB&P=FK@Yhml2HtR6pHmT6iAI_;|%p)P2Ja2k4Kpe(={V5>Hp03%P#*6G&T6=L|%^j>ftM4C_A&3C%Y26qx8S6*OGE%m^ +z>Sz0z+4BpoFb^-R__&mqfVP{#wN16W7|FfpS1TjM6D=BUT`{bNzrZy)r5uG(XxawM +zq_LU^M-5msAU;GgLtx8dTL4Wugzx$1z{UA#E>!Uy-=QK6viK(e0ab;urwJ>!a9AH4RSaHCjCl +zGB_Fkfg&`9+vjkIk(W9!-#pg*SK81VFuc2*H_{?59S&MeUWmK6qJK;iJDzGuL9vjK +z!abUU(IB2R@~&~$VLqz1Aqjq*@MqD!Ybrf#dmv7Ld0zysH~ +zq2@o=`;^?bsRzjwb*~?9P$$GG5YW~L84oGd@%jba9mV^OsKwUk8^Rb{>s;o8{wGE` +zHx^MC);`~s^^FGzSa5c=q2(9RUx%2TC7Uf#1$>5OuXe=%c%BHLT +z3{4*I9{=ooe7+)!4!S@YAeo(2{~`gi5ct)V;VlgbPsUFXaPwp!-PidZ{9I{|-owf` +zosDLchqjHw(K=8dkAe|~E$CeeKQD%?cg;kFg>yj)cg;fp@^69?3xf(7E|PD7z>ynm +z_UH-w{sn<#|3+yVVh+Z`+Qp{I^|^N}j4*|l^P%RXhuZ&!WX`&}8N}}ob53k@eLvQp +zka%KXBut9scLJN7bYQZw^oyk>DxtKUHkuldkCw}fiU7{kcct*~B>NY8C5Yto +z>uoypB$ex|2|Qsu%Nm1bCyDl#Xu^mx*UXq5;Us5DChUqHc#}<0yM;99rf3_oUHUcn +z=g<%yZsTsRTewX&Vfi0$%E8K@^>6?~^osY0nlL!#s{q${J!dv`*_b6TWoUSp8~L~urY%)7pTc?TD65@1gKQ?PsL0kU5l +zy3TW22|$o#lWwG%mxslt-$EM1H}M?M*ecAzLS+2QaFelyY!MzKO?c2;SlG;uV~kuZ +z1>?}TtEJTtK&E?9LggN_B^ep3K?3#lyqemBNbQni7Fs6I?cmue#qq~@N1+100Uh;~ +z5TUT_WOM!&y~`9kL+t754hz*_3*T>7*&vxtQ)s&%a-!Euq9&_QZDVNpEWf0CLus+<_{T!X*-ZwZ(z +z$ec)=ZD2qYSOuWpas`OWnfqLno{Z<%iJmpYeZhR>Cm_dc(V;e|uAEieV?VsZ4r1Tu +zIvnvqBhb4=tqRCaktQ*bBy%%3`RQeLB4shMR1!!=1nQSX;Ng$a&<1f!6t2O(b+T8A +zs}@q{PAhm_T!wF57{R&ttt}Ktj=eoCpTe}Aze=n#v7V`pRJ%OCo>c6B3#ubsOZ~&9 +z3^~4*UiBN&j&p!0d;h(j?E7GH(Zyt?)f)m=xHpb$8`~jp@AJe1<0qfT8QG-vlmIqN +zp1}9tJ>&|+xGCK}5L1_yvXmME<6a2_QwAX%?yfuI3g=cvt^;c!)rMRkh9%Q}(X)|p)4wEBL|%hUS3B&M +zLj>r>@qwcnGeN9Aujn#L=z-`jZpBPU*G(>SU2-u0s-1T#%c=ui2MEv2iakcB?q52u +zn}Pki%ZL)=0u?l1F$r)F3wGw1KA#<=;RVB>mYk>7%ad8+)TIwje=(CnOlOa?O^Ij{ +zij*=P3KCBsmughLxyrg!$~|+mHJf8+$2Dy0>&p{!kV4ywFJH79D3n4ko +zoR^T3h(M&0Az#Vn0jiVph+G>w+>nyvm@-NK1t$Dxqm9o&@i=49S2iQjCufJ{0b2i1 +zoGN;lC< +zEL+VnZ88w#$dsjX4d`RxS>-v|V}nScTC3GCEB$9s2JvLNCTU@Xt-?1mqp8Kl~QBFXv$LXqs2exVEGs +z!+O~5b!=~xM5vD)!>>&ST)QG;VRrGUvL%k(+D^q9C+wTTHz=h-Zc($tH6x-jt5Lc* +z^lnJ(mSdl=-$Tj?T#7-1Q7*3a3+Rs{AUwMKi?9e%dsV*RbH +z4ZADT);f$P)ascSQ`6QM0$cr#2%+WDHA&BVhlOz_hr0bU-y+QHl}rS6a7Kp3Euz(B +zpj6C6gJav9CkF8bT8hPUj%;Z|4QvdTZsZnMTeXY`*rh7+JDw+28z4l2iZFt|5mDL| +z5l87)XNsyjW&o!4$XFWPIu^zDwUB#w9iX8hVoOJ4#FR8$Ae@q4dx)gXC2U)-$xN@Y +zUksl1*YT>AqS~UpwKTcNn5GHLOqb){qvrIt +zae}Gux~} +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 +z_}ETPFr_m1NPGeXF$_J(k*mAYU~IL<)M&f +z6Wv1fLGL&`Q0|o^ES2aTAHZ2G=21e_S@5IOILW2eS;_pN3$7I^q?`QTy2*t{7S*3A +zdBmwb5{q>dpBJE%@j{`bi$SUi`6X(~Sf`79MTKI6>OwWe5rMa;w0RDXMT;+r-R +z`Pb+>R!v=kPMzxROh~^%H2?5)Pr7FOco*G#35Vg5P8539ts$M`!U{(o;>5rDjohyL +z+rxF2CgyW>_2gVan4^Ef5qGJ!RFM65NqrwJWU@GlDtrKx$c2f>!1ob6f{RP3(vM$0 +zqkY)o{>im=@CM8j}h8-kg|pQ{&$ +z8fStChsH|0Tvy_1tB%M-^ND}t&uH26djq!`uag!rE=X{~p$+8I+1SKE=GRmO{T?JA +z{OBnR<(<*BH}qe1Cj@aVF4n3f(L91YuboJlZv&+T9d2|NZb1eR&x +z#*2u&FEnPeW-NqHIxp`p-HNA61YsE+9Qf~0493I(glL?Or+y^X5yg;QEUw@mPtGpR +zkE<34ZTlvBylUtdm%Sn;Crb(Chw)Pl-Uchiz=yIIr~V}nC_+fhL>V?%CaJlDmdzh^ +zf0NZwt{~x;%=av81R)Vbx6rieC)4rZ`R{pTWh9RchF~9o#G@@a%0r)`@-3+*f$iSV +zP^E+c#^&fx$#VfRA@gPer;vH$XR(UVYMK6z%N2|+%+PS8O{^bh{4!BQE2D<>mAU;J +zi1e~FxmXyfaNVHjE-zSDl)$38q9oxrgV5wrZr5Rv=@N!db=8O$|A(z}Y7Z=2&v0zp +zwr$(V#Ky$7ZQHhO+qP}n?)0GDJ?M}4uJx`*w=N=293gBF3^IeB{baj}NH_@6Pt5$_ +z$NZah?ldq`rEK#B84*dXYa#SMXeKeDA_15YFhn`>gb7cak}?XrCAwu|YM$f>p$C_|04b7Q_ER|a?;-O=5LO5mbKP5J#-SrRd95{q|MLkhi8N9N +z)B$Mn2jT-$_DmVS!650wI=)Ia>5TM~BX4cWrvZDxxvr-Fb~Q4^m&c;&=01f)tHT~2 +z9vLs0JJ6p9fzD{-oHF1gUgdZ?MmVQu`@D;o?w0d`6%71am_^pGV&@S6Uz|I=K-Ye4 +zT@P#+`=o4mmq90$JN6P0h#c9Ge1U>pu-g$RwmJ&%+X0!FR0Qi3mGhFeT1Zk9G+tIW +zbX#UmgAj3F&xhkwem?J)V|+G#-uLuhw+7A(sf1MCjz1g+q)g91(CMlIzu034nsN`2jg5{NqkOaA-3gqiY{(@c;~B +zNPfiRU|!Ra9Fsm53qeIty3kpRN047ILk2#)9L<5w`2LfTJ+1X*SqWDVzV|h!u<>nA +zaso8-O8ld~c$OtIWWKfggR}8>Nr8;w7wsoRCr%F;CjC&B$pVQ*1)!leCs&m_gqnlx +z7#s_;8d+RmqYw)$+{F3EAZfEUh_LB7J~`b4sBwnL5+;7=JiYNvoT+OkPg}FdZcWS* +zq3XS>0&GPd1p|ZLumo#hCs2$`%n3eE3$}+>SnI&1_0`kOR +z@S&8MGTt>Rs~0zB4 +zadF&3Fi)Ah9N^<**?w#)6)i!=zh=(DJ}qc$&0bb*1B1&8lDnY(XP^BT3CUmpiOugh +z$dmW_y9anNjgwxa&ypIc`;F&SSuZywp16pMd&55%EcZUzgne(@#x~YsxULV^FEq=J +z_&;^X{eP+heGd +zK{y)ya2+TUg|ACo)z?=LY+=dRhu3>eSz^oXUm;tCUhB%a@zROs&u4+&pK5_p0xqpZ +z(Zi%Lqii?yCx}Z*YQDI|>~Y(dbXKS@Snkt#ib~wIqyQk*X0N390rOROPM8?dLZ^S9 +z7O@Hn7EZVLxaum{Mcp4=(ID+a>A8*% +z%b%DN0oGRve{=1Fjn%w@nnBk3y^f$s8~AUhgAJon`N$?^^$w(O=d}c&Da}bjXSjEu +zGm?W$Q%pwzm4eR$8({f$vI8p$qhCV#fZg4*bTefA+b-J!GNO3ZH^NvMPI;F*<3TtO +zRut-pI!Q+%7xG-q>IcB$ep|XdCsDFRIqJZS|3sfl*$n)R@%5Fw3RQ~BpG60$s7 +z9o5}$uP)lS#kIh{*CF*`9t5`y*)53%xt*Y4-dP0LU6z1Hb6GmB8SzWjMKBA0$%P?r +zkXtZKtCOn6q#JYEMLbFYnY~ZLGVN_B1W0OA43y5GeA-dk8^dzbi}Lxa;--mY7o9Y` +zh;pbnsE9W%9Z@v!6d~0!{!gnvYOPlDH3--2(wu^jJZCpX7?eD59s+)l#5LZe#c<0jhSrEE& +zUX(V3I?6c~l|HE-R0-8MmIAEjKaQxL?NNdmTsk0A3_Y4bLHtHoZQ0q)OL36gpMEao +zw49Rk$~Ys3iu+dns^UdA{!4#Sd@cE;2*nR3>G^;r9bV0rs0{HSo-%518ZK_j)7c-L=tY?K%q=P3szSzvJ5y0Rj9yyi2G +z(fQ7k2?&Mr06aE{s{8N*jnxios^FXaZL+YUabBDl9fTlW%5*pB&S3258+0f0nMrF+ +zbv1qZwTF+$dFH$a*`E2>B4|K1eUSe`6)LG$BKyff-v8JpMSLZN9C&}%R9!-S@<$*P +z(Lj&{$S(}i?<`n9?#~W +z6y#SubiW;hh!6vON@;wTra5Rd3bS-8Rfq}Gf7ba8 +z#WMKimiWCwe_^+NhzUa(2W^3hyZDlnK>& +z1lu{*a_Ki^_-LO;L<21mhA>h8mq@$;EWe~5waa)sN%7RFe_wkA**D(+6 +zxTxWTqSV7484L%J>Deg;%*h~5NyGN$n{#2@S)RB}Drv_=$B!*wh{yNCF0Cn6?vO@q +zG>|fU?6|4zMD0RBh|$*Q12z%GZlhr;He(GUC3(Q??6R6gUIe{Dt->QXA6JmAn^xq) +zzP%|5DK@PlAaGVM!Al>Gx;USPeBye5Wd{ov0$Z$qI;uu+lh~aW$&9;f5~#&AH{XI# +zG0C;eg+y9Bv$djkjS(pLg5y}S+06?7S~_bfBIIj*vtE%w++mWd220m@I +zFfmLJ3@!km0VqDDJI3O*<%sIsi&C8yO?!WwHP45DhQ1BvHCHIX=j!OY97+hPa?pKpnesvfxhY>QZ7z`goG?L9<` +z-C)7BA}3xCJsx~6)-s*(gt1Q=A7DSkoUD(7WN#o#u#aHq?~fl`k@7rlh1r`18enNG +zUN?XW@kQI|qF7DlNSA$}fQtkkA&Y#xjb|$83?k0N`sZwH7{lN|Ktnl0L0zz07eI#C +z?LZz4reP}iuW8E(7k&T;9R33u^z +zaoJ;a3+@k-N^<|>l(iIG?xTOb-o^9hL{F)SR=v8xb5X1H-I185?0(kd-^s8p?Kgi)8+8u +zJ&+zuF~evii)^MuOe%WGUS$^q4nr0UEJl*)1gu79?iJ;CJny+{pi_dYiSWAaW6A8@ +zO1EHreIx*;?aXHnu4l`m0c_*v9PH2tGgAptaidBy(Vm29NXFy}4x5Yu*X=-*mR9E- +ziW`Bpg-L`8wgqD7Q1iHaK3c%mEo-*VCb=9`x0JvGaKKA$jf~DjhgMyD-dSC(cfOlG +z;uOZ(KK`{FzDCOA-u&KrM*r_sqmUrpA;Sk60AB>ik*c38%Xrkii^}ca+ +z+Em7!@~ulY5d)dOo3+H_Zhg#=Q~^GwM85~;k^@k3Unzwl8(I0~91_-roeKU%zgp%x +zKIW8j^_6p{F75`A=yEj7*ky7q33OHp9kIQq~GKv7*9AQk2YDH=&3UsbK>Ht{F{9B+w7q|3K$n=ggHLXe%i9$A_V)9JlsY%45L;Z&{)AMEaSYhpH +z^1;<)z@PA*v0y)`Gc{^1aIoDFE>T*raaP)1FLvV2-Mq2N?3%LWIJb=!Isw2!DUiz_ +z?tU(;GmV?#4nV`xYY)bGisf!7qjPJH*yZn`)f#h1rb@{pUmoo*M?oh +z>X{L$Ue#f}6^c6%wv%d`ei!nm*Y#7kg0l;Oh>9c%7Zq5Vc@%P@xpe&w8$rD{o<44A +z3BMD}%X5(jxkIKHj^|wl)0p-8P1=cejxmsi?cHgrB}wKA3;9ygxbdw(Rv7g@mq~lb +z#c|sDbn6Hf$A3QDg8{BRD|ngb6s=`U`Dc`_cJKi!=t^bd^!l-;auiSDY%?C7d*0@~ +zP2Pm`YJO7%;*EER2DpWzB^p#zGq|COf;3l(Hczk2*1IbAQtjd>%#Pyk@`hMd)!G!X +zlATHt%LyW^caXccV?dHfQbbODhBM>fb +ze1$(DgY3-OiR_>bUru4Oj_%^zl}bV*GijJ1Cx&y1XkvBb@cklv?^oj6&P7F+L2T!e +zqD8^C`p}pF5+~~g+8$=-5!JnH!SG*e8wB>QyAj78Iq{X($pPUS +z&#$u29qLluTV?fZH8aWY9Z!wj+r{QWA8v^Kk*tC~zptIs-+g8cxEDN1gMp*wo1Z^!jb53G4VL>h@=Kz|k!X +zEU@CN-kHMVz=p*%j*<-FJAwoln%LEMOaQJ!r&5wbmhtul?HdVC59_u+xoS5L56$vw +zTF`0vfjF~XdNWVHUl6I81f%75CZuOYEl#q0T0TXbWK2G(ck|<5dYcW_l3=z@wIY>c +zE`<=rr-#(Yt*_epoaGPdHY8qqfGZuVa0dT+pjGo$J +zzzPNN@q*uiTMzR=KmV>Xx6@9)CN0;@!ZkG|UHhk=nWG;E`t%1@tdHVE^he6MbBWKg +zEsVgG%Uis`bb@{oHv`WLO@yEIIg{jEjK#j{KWPb%^n@ptsfKf%%URr +z6LXgg;N`Zo5$miO_H}1UxZSQ8HEDO<+_!o+ynOvCxK|F=0fVzPvh^!_NJRp8jA(+q +z;ZbJ+hM7fvKii(w+}HvMk%YxJLpaQ~B^y*RT+SDSfbZnmxJQbuk(Nz35Au;-j#a4+ +z1F^@Mv#uf9>S$~e17MBtWmeDE++sJDUvzI}*i*)-H%&{56U%CBT63bSZ|N&T;Z0(5 +z0i)1%@`qek=Y48Q#aCrZdKzp8Gc@JK4#7a~p=u|lS~5GrE!wt8Y}L?Iu`W6-W>=Fs +z+7}|=TQ3JkFTSuBp@ilk5qUztms8@NmefT^$Ih1ONjAj+#UMJfBenP^q}Ivb2=Ys1 +zAYwH&CEJY7-0nptHq7b_rhsFyn$q8C4OS;=)KnHvL{qA9o9A-BSkryVZijk&OR46) +zoF@PD!?yAr+B7@t`k~yZRMwOE6F2hD(+${kbqD$2u|S?gmMxt0dWEk9Dgtzutv#Nr +z5)Xp_@H`z7r>1tY6fbO=P)(i}^o%tN@!K^?+3|frI}k1_V7C?^ldvD;s*^svlj~~j +zaTj3sLt^`%YT|SQs9?#y;G6rxyF#PwZfx;Rp*Fv1(cv~gLq3SiKPAu)MnQiB4sb{| +zKzMdEc2jsWpldQMY3fsyYT_N74v^@$y0=#^(zIWSvRJy>mz;5BRI8Hw?!JP5iH*TZGJ>^VoY~LGFBTW@NnEg;-2qz#kW!C}K;tjO;2PWzcfj!2b?A|H +znwOuyz74eRYx$!bTg_Ph~oLT}*qo@28= +z=;dLXQ9o%u(O8PSw7BZ6XQ3H#uuY(0vQFgD;gzM|P+KeQKyy=_dLxymt* +z(y3P8b?qvy0FUi*PK`yuJMcZ}CdJ%{Dlr2?RI(TxkX-}EG+Tjaw-i*Z!I%v>wO0IP +zm&dLr9|)N(L~VxjYO%d)Yx|Fn;_^M~aqr970t*%IutRIR$<2d8nme0g>^jE*VUHds +zzG7}<#q(jdN$p2u1(PrG4WN>U_sWCn2J(sCI@-Ut#@|f~kJ|ceyOmNC_bGx)xCF~r +zfvyc|Jx+q{Uh_&xw|#9pOY$%#sB`<(|BU4F;1l0AWsoxCb@FiC(#fe|o+TD42uDR> +zIWv>4?i_juarNj-2lV2#B>H)|lq$dWD_6P)PLlhoFCnCsZfk{?TwC4+ +zI3|vytAHHVZuahBTyM1yK7x}R%Zno;E(ivW9j|Dcn9x&q8>90lDCngvRo5IzJV$RZ +zi9Yc!`If9oyO^vUBipo%;a9UJ+dR?T2Pi^sgB;#USDn2xA+5(I!E(;#&xnq1Y$N9E +zQXYkP#l@JSBY0~S0%knzK{IB2AcgzkrU|wG5v#?|Qb9<{-bW|kt +zvrBFRyI)Y>GxNJX$H)D7=UMW={Fpe`{(Jc4qfT$qN{|OFxy_5Yq3Fjep(%1F)3K`6 +zrh;-LE;2J^-?qf)*lLxr-Cx5Zw+R2mZkd+3Nwu5A&Y;v1!jpEM>7j0^tutHv9Ix~1 +z=9u8Y@m!^&K7UAJw=@0fXzHSaEr#R#il4bkXf+zc(@+o~QcG~M-R&~=Fcl`%Z_kI@ +z?GWlcndwx{lB%E7qE^?UJsZDj9Fcme)ra##9;`*8 +zKeO8*3oY7$?@X0QKLd~|il69$T5OlN4~a|f7N2ufyJh_})A#QOkWT!tljVx@{|>%- +zhb>Ir#`K+GKSNE&3H>>=IA}@^oRd9BimhIZEg)n1D}Ou&lR_1buzdTeqK$}KEj}F& +zWL04i)$S7%Q%&JWAxzKxsx1Zo9WkRNqOWzW>zlfh)SGu=dtond7QBh*w2n)(K50p^ +zJ%n7G?iw{m?KPTsyjxtfg=PcJNunN}GBdA%0NB>!Z*+M3#qd7H5pRZp&ha_x>ig>z +zyEciIBhO6jinQ)k)+aIvM|zm5?I>zWdk@FwRf1gdwy}iF&4+RK+!lC*88_kQ8kW;E +zlM`S%O4gcTs-8v2YM*A2No7Gvg3yzcf5FpKgTlI~+WZ3r>|Gs2A^h=lrC6(bB0b1r +zW5S%vb#B*ZMT#c`sI%c+F5GP=Q8ee6$oVUanJrQ#!j`%eImHUI+S=@YI&PXLiJ42$ +z2uyF{3Xnr^oaZPX?uCHL^G`Qp7P)*$g;SKx>rIG%8mCa?8Y^4-HghuYK9)uZ{6<*K +z!D$7(QwMfGeFm8RS0ezD_i>Yq0yg60X#)1)Elu1dFc+_r6Ge^9)V{;PmceTl3C+`} +zb$}Wxm}aVp`!p{f6$d;}joo +z+jct@ORM~5E=#4+uIWx>ZWmd9Yr!7N@u6{qjL;jbJM~9W+Iv)W3c|eu9K1UToaY%4*tmQK&Y@%1ZPACIwD}876K00AzBh +zrPtv0gM`+s+`qAW=)$5b{flln-iBu2B&8=i5=n3F9ZAAk{nZa2EeEL`saFZO&gRmc +zVA*dw@Zw0TeK>L?UW1}m?weBQ8Cz6{0!rZa2emx&ADBhSX~87$pxF>!L$lKbgWdsX +zb^UxO#hW2_1Q*@ZGo(7_B}rI|?y?yf>VK>1Kvv6*h1UYxmg##8H(ht<;hI~7hF`au +zUGY0ADP8%r?`Qywl=`jz;mz8Y)}>1=%*1lvRd6~|qvgazFfa_V+`j(&h+E=$9kUzU +zG3t_fQPmmqv9jDFt`mNQNI$OV7xF?WUR8EPXDV#qLpWp&-JR7g&pemtn6TXoT?pGV +zS0u92xk}$!95f!Xd9@9AwX)?tOFxJf^np(3900ejE& +z;7qKH=y;LSN^#bUR6BCep6@!>Q^wWJ-W1e$5mf^m+b|TraNyPG4u0#- +z>2Xk`PlCoTu!zr9on^)|kFjh_Q;s6LbZ#wKY09pEnBk_strvkOG+;Fm9#u;5UGgnH +zqTRG@I*5{X)R_us=IV_0(0IZs=qJDQ)w^8eg$sx}*LZ(oZ0!m{XsEU6%{f(a-R`Ho +zmBzntadX|$mJbj1w>~i=RA5ts^5y?JFCM8yHt5JlTFlpk3holqBfI|$(foUZzTWRI +z2g}F4(bsw2#aR3-NZj4Xqi?|*)AWzc{}Z+1ynxm{G+6a*Icd;BX+*D$MFJGpvia97e3UB9Pn +z5UnlN<+b5sO`K +zSMrnhJz2P?KKAP#U9sAisy0Ga9hH#JhiOBR+Aas@Kn+rfTDs;6P5K>R?YCGI`*(KZ +zN01eutJN(h?Ns|z*;DqsNa7Ztjnr64T$H6k4r{H$lk&7X^WYremIz|o$2*_Cvwt-YBP?rXxh7M-92cYtbDA*r)x~Sx?Y~_ +z^^&i(98mt%aa+g@@RXT1tRm~bb{&eQz54|?2@gzr$>eR>96Mp!bne-2b$S=lqkx;j +z;ho)Co1?bAebn{RRt@eZ47{Z!*I=UG$XM**WTb%rH@s1ljKFFF(Vlb7VtcHYwvQhv +z9!niIa(>w>zh}F@f7;yzeQE?}g_&y}6;q&a8@x4nlDjI@;J$l#;`avNEf}0L*6R^J +z9|Uv~(TOK}TkjC1OQD>vuWP$}^n=mzP5#O(%<%A-3tt=pa$gaQPjGmIJ6S=t8v~=VUN)7&(Etz92sk1qT>v1(Y#~pwI&*D$Dm{gK_PS-pvB9-ZJ~>_H=i$*S?Ng#eO>9M>5w7er+!az!0S=zTeP06 +zM^i9e!bKX0+;w>rbyGK|Cm?L?x;W=Jdk)hz{2HbaD$N3JDJH0Ox=tI +z9++EtJ5(Q8c&jR>vv3(3YE0aSs7`9Mru9AmwF=}O3>;+};2wW#D!P&YG`{Mc&v(um +zKC>owxHaMFn0R}!TKXM%GxtQ}KaNf|m9l@9s`12FR8c3|){0|Y=r4-%IZHd>XFkKe +z`38cY&SE{3k&lAca~jW$2fJH&EqFU#q+6=}?hlayEr=nf*ROhEHpMcA|KXxMUw-@0 +zNR;k!OGJR*Y55s~m3ixtycAiG!%O86W?|B7HM;M)Pe-nNnNZz9l3e +zjCxUY^K>?HOj}RTcm?E&J7QzRKf!JORZorr;n{$0PNsAXe^b{E#l`3U`FMk|S2|Yn +zhF1g-{QmRsJ$PP3YGu6lxo>N2pJC_z=%$+K)LN#&lV~-j52+cYO)d!NjYt;!C%2iSy~i +zOCQ`>l{CIf6b9(;2v%KWh)nX!>X_$<%6nZ*Jj1$Zm|g{8jOgr(?>Jb+Spphh^*OkE +zr!_p3$PJUo*L&d0!T)`~+nQ-FZOqNJBtYTb4N#G5yp6XeM#1U>9uBsNvJ=}U3%2JY +z(Q!I8fprm$4}{E8V6lr`U6tEItI_&M6b1veB(~#FE2k +zNqe=OX|fyEK1|IAXx&?fUXAlrqU$})&0Ehd_F}*v^Suk<(fq%i!nY7Qvl1)w|K?b; +zc|Xq2XJgNZJ0IY^MYfn)!EU^7e~;(U*2 +zZlvkZIGjXaVd*;8eWP7myL&|9)XaXKkDETv{wP9vKbMw1nP+^x;m-wesh!kY&cm`s +zkO5ghqM>FLM*uJDu5lacg{3Te7lwZP#RyXGa)s~OcmxA@?(NUC-i5%7@rK|g*u8vj +zjQjlk&thfQziQ<>bY)c!Dgb~m9{>RSf3RRq29B2hTdZvQ|BIE?+?M|?^p@RkYBH$0 +z0_||QTOY?tn@-6_*cR&mGEsX2j0FiCjVMaRP5i85y;*N&>{i`$Glh+j3fb9q3`T?pIrxr(Y_%H_n#PyU#kFtG6=SvMQ?=8>)Us1+~zNnqA~L +zDmDlXKPSGw9^UzTv3^O}=A9+wRltwp?Rv_x-lkTr22_@Otu0Y#)YggJ=;-pb%UXS< +zRkFny%bsnW7SnhN`Jt{-zy;pU%9T>9uNQ)9-G9~tRVI&TWvx>tC4KH6FNY&<^?Y70 +zV{ezV^^K4Lrl*8PjK7=)%!tVEeox2N8%T~y8UbA-r}Gaafs +z8_FL+E|i{co+DRRCtM)#NXD?7ef_fgE~9U#0UE&&s%uK@F2K|mS?j9&jh(pv^a)NDl!6rOJ7Q|H4JI>i{0fN)%u +zj2}9+jXQ2LhK+XDkZeOZ7LYqZu$vdwpKNo4GKITRQ+q&>~gzR0a4t)sM84 +zuFt{RJ(i=ZH(qJ|STcJimYu~A{4nZDt0ryg$EwQNlsuw*D@!;+Y@2t|IUl%q7F5dj +z3m&bIazK;7BQv{sV$`6iPv}`8Y(Y4>FTZp)#cl6Zf%gbl2FatZL(YJfytP)w?TI2~ +zru1OyY+<)V{2yoIsW_2Jl*}j$n>EdxIGoDcs!p5pqO{)7L%3D9JHTbC*{<-R} +z#`#Yb=iCCVfc0lm#8ywGG-v>(^?rdjGNqk0(_xSnH1s2P_B@z3{f@J?2ke15^2^X< +z-1jIXl|V{=64QfjU8Dkh*0AEl>(ug$HcM&&o>|jj-LM3az$##|L=-6N9LiEJODY7i +z{r7>j!~e~TQg{ff{Syi%@!E?B98>Jk{AOXu031RlHmi+tWo4L +ztX`&WW$@|EW!Q=y#O|Erfy8x(qbSHlkbo-48+{QqXtY=k8;KGGV1#4$@jn&EM+Gkj +z;!=IyT^$-F1v1<^rRHxO-DWn-9Be0!Bkazvo^39Ln#ym);`e;LMS}}$X2sl}yaAnE +z%GvIP0a(b`9^jt1nii=9%0#rZb0e+iOuh4ZOV?8GjRT2k`>wAR)u6$oS75`0u^~W@ +zrCWfCSprL=U6$DOU~S#yEO!e%ri#xssA1bl8X`;!Lr2b~HdYdkJAk=Fv>cVe<%Q@o +z05W%igGfC+3-m3G`2_+IoV?;etmC4(sjg|YKnc}*IS7HUt;9gQUDF`WRll7QdO7&& +zaIc|RPs{1<;fq=62cl!=lf_d%Qpf_7`Wou>`8YICd;i5-HGvl-UHd^&H}0|BEcqt6 +znfjEdYT2-n2fZ)%0H*Qhm`rF$P$L*X8ng%mgsFq2K^ck@t?@B)t%vC{=Yl!(TNQ^` +zjjmZQ=@O3(2MW1Cd>$!K5`QzS5#%s1I;*H2_ +z@FChNdjDwQ1PMrp`#8_j1Dk`cDM^14bDQfF)q1&MqbN!-FQ+rDJZN +z7}7-|+{Qj@Q%Z-QRusi(9oZ5p6Xrft1T53w5*g2Nrp9g@`Nz|fUBwju?+T@aB71%~ +z@ej~Ua8Wg#f1hTx|4tn2n$NJoTfsWb61Rk@gcweoGPhZ!ULs(~Oo0L35gm&pO}_E! +zm1GVqJZ{sgA2xv3MrQ+&`klw-KH!wW_H>FQLjXqSGn#nLaOkQPx}h%*c53R +zAcR;<5vC^UF;=pEyGZjG*<|sCzo89O%&DQwj?h75d3as`_Ojem%_z9@tf)f5t_Yi| +zpb73mE9nt_Ggjty4|mM)J-S^#QWY|JDvCOAPnzrDDFlLko;^HBXBV;+tCXE%8Tq>g +z7rzR>pLPG11kNCgRUZrmljF;rdzKlrbo-*oT$XkFfMK07gV;*U{f%BeH!#wz({>_h +z#iaOi@pYCPs9PT`VictqE~%hJJR-Qa$br8%*|{IXjQ%Lq`u +zP*!$YEIVkaCl12Sk1&$9d#zHK;nN8ek)0hfH(H6M6B2t3Ww|lB!@&|LVC#fn_qh6T +zl6zE7Q%P@|TfN*i1C4(g=7JM?Xqsy-FSPzJWlsh4quU7K$A=%|ljNWGVd*Fw;4Y=+tRsOtHAqvn>QnrI9eH02@mA$I~TL(t9Ab~S +z4vqX)k|KXci1!yG(!47);vQW{IpHRhPn;?akv2?GR$)PiNu=Etzo8}jOhTfWcHia&W|!+JUm&n7rKv9_eG;tp^PH;r@Mt!L9yrz>gX@G*9` +za_}6IgSj6Y6-Q~v$$}-I0}u}`#ZVA1{-b7}bM*kG1r3!62Zt;HAqKZK?s-Umo7nsS +zcIysW)jc&JTu@Yt%g5aq#hTouUsRAbAt!B)&!c_o;>!6vVKQ=c|i3@D&Bwyy7jnZ_U~ytYpQTuNiX~ +zM#6GsoSmm;!$7J9t0-?$9LY`5Ca5(%Ev$SQmiZ(V{(=r{EOOcpor28PVudB95o+VY +zAehw8^sXCt`Cw@WK6H+)TUJ-nkyHaXjHG$^^EMpoR2#IuD|1M~g5K+v0xtAkp6k}u +zL86Mkp&`l8@xu^Zz=Ty=jy+Ct0V!ZebZc7t(YB`yG}M%WYd~uBOCE_q<%v4zq!22& +z3S{B(jGF6p?~4A*?%Th4cExVpZg_Zf51v{{Jr{q#L7a`eJ>wERECn}E=waj#`rHC2 +z9W#Ry6$&*%JivHHAKyHQM<@w%c8~ja;HR6uz6$0K&VCB#>97IdKp+A3WF~ek>vztq +ziI`ZOOzOx0oAcMqxqKkL3r2B+US{A>vK(IHq%=A4l_vk8veL2$OBc9lTcW@5o3oz& +zHg5^;e8431$)2Go!l!JkF}kvjHWNS+Zp*O;v>+b|?sLZl^o_e%lwwOil^<*GYnkyb +z?4Z>?GX$6X^j`dbGjLDGtQ>_ULTy6`u<rOeziIOLi#z5s(U;@65W4M@age^mBHF*1YBGMcx~^PLavzZLr9h9d$@7sTZS|$Hf$x%fNj&u(tgHxtVYFako#hMJyZbM+lak%sX(zXxhGO- +z{;RtWTJaqzFxpCLB(n4ClgD9z(J9iU4YTzO)AhnEI7Is$0yEmspC(=0irv|NqqJyR +z=aa3SzEr@`{FcCABqoLYAsh~otg2^&m&3l&lrunM +zS>#09`7&u^_hPiZ71*p}(mP@b(fvv45h!qQ8=3xTC?Ft?p~}TgEK))oonE|IlzjzB +zD#knmhAgl*XE7rjbxCL1TA~^CY&a2aq|w6>(_9K0!=n+QcmWulUG&B0WKkhw-yCm$ +zsSc;l8nTJWlg(tl`` +z*fw!Z0StP$7dnm|A~bh1#%L&45D16{N!AON3Dl66lsS%XWdmhVOCl$LjDp6NWjS`G +z=>hs}(5KHikRoUZL2+5c)T*u!>Gw_mx@g0s{ubAch4}ElSc{06vtqL`ejSU6wHGW; +z=cfb>jU+|Us%F#@_0$CPJwr6~n?phlVhrxb3K$FhdCovUp7BB*@dtofpg@2a)PDdB +zjPM}Q=V1vd2N<4e5&i?5_=;^AvAD5tL+x#pETqb41S)5@>l$BfCP^@6f}rOE5?zl1 +zf;f?YY49dO>lR4s(O|CJw1TYwg1$tMpIuwBeWqPzK5C5auW$sJU2cjmEn)z060=zw +z%77J@klFb|K@<-VYt6;w=PnGs&c?R|-FWzrx +zrYqB@KoHDVM8~5r{S910oVr6E>dyKgVZkPxd37}c=!vl2W38&cM^JCHod=sIlU&Z3 +zG-P%!%{3it+#~%Dl(L*KWbtec94+?zmC+PPZU!WU;c8ItDTP}eJ{WFMA-N8nqPQQ~ +z55sjI`0*}z#WgU8I8Tm*taB~~lZ**E4+IffBfGeH!C@8_VwX1Co1fjf#aTC=9dve6 +zpxv@=xB4ZPSC6T>`4QGL3yd)vuvEV|vdY_Jxu7qg8Q|!Q63t32h&*Tn8tpiCw +zX*w!hIAICHo}pNP!yftirbyMF+cpuc+;Es&Q^sPPjGQG8mW8yn*jk55OLd#1QR>FI +zoK|u%DBf<$O@i5A0Ttd=I*1_dg;E_R9v9*^|ME~cFTq6T)=~(p3>4jAr=VB +z^LcpcBcewJ2L~IAS_;NT~2jNyBko4**JEP%GlaKOY6`aY~-yYhNrq6&|0f=3^A|SDAlQuAetr +z%)ON~=bGJao9APHWdn?toy2#^CJ!Lg0e@;oRb2grH+#NG8t}elD;eRlz*qAJzj?tP +z2M2t<9yc9F7X@I_dDDG}suATX#D8```6Nb(0L~)~hfWYA^XeK7L(1k`@;&5`yNaYb +zDpE>xN^R81iaTBWCM^buoKKyRAyq}ihtwpSyL3-UCV@B_;x2!TnP(mCZ8hcdo|PMjcu%ff;h_=v-O +zZukU*L}ay8GuD7u{l7%~Hzz3a_a2i-fYPf9V*aq$!63B`51Q3< +zKPQnh$5<#}HvHxsnx6b_#aAv>f;Q`CP(As;zn>xHjpCHa9yi(vCZ!!Ry1mn$M}@rb +z)Tb_nD}g+|7+c|WH6fUhW7 +zOT?fXLB>#59FLx8+t-<3{hQ_Z6s+>5lNa4!BnRNIb}m6NJA5hTiwK>^3=L_S0Z7ao +zxxj~12x|i24``IyADfj`%DX35=%%?`WTeJ7keYQr^4@~J#7O*_pDH-Ddp9acMl!?!>ZO04j!Z2=Oa;zpg-Mz$SW^sa$MN8@!03*?}z!OvP&I?nQ_+ScD +zVYIRgrn=_$1Z{7-a$l4=4h4e^n-5-BgRfOi_&9+=!DwmkJ!F;O*J0V)OJ^^!d-?+ggjm$Yy +z+&a|^_L&=PKJ5W79EjO}T;jMJC!12rjaEEJ5u`U5dV^ +zl2}iMgWNV7fOMSPz?H(X0)UwCK-?4cfTpoO&e`018uf^({s=_5nm_{@(BP9dB>r13GwM(wI)$LcCm) +zv|}`qv5eT1KufM^0mjSM9(-U6$Xq0KzcO3EtaWo +z-cnE!+{`zr2lZpSS-?(rJ22uMwOp^RVN++Yy8dJ>9i9SdOKWJbChqD>q;QxFYtbbt +zl4C>SsIGqIVI|OW!Czi9rf|z-T*JqAxs@2OwRdC0(>_ +zo4ak>ws)JmZQHhO+qP}&wr$&fedf-^;JlgnBkGT;sECZr62?Z=4M(LUhN{Uf_%Ve +z)PQcPse;~2=wh~)vR>jR_C^lfjjsU2L= +zSNCAfkdYN(QFSR`>x!-?hR!Q4tUTKumMr|VBjT$plT?Z|64RKJREv#R*z=qZE6Gg` +zY2huVG4Kt3i~omL5gQ<5zrud$Koe^9V>85OFPDOGr5 +z`O|1Qcs;#fPvZz$V47055HL1?>_7F3n`JvYlf|W7-vG6e3 +zpv@P9ExMSP1YNd`R*jDR;KNJD$ePiD`_kNT<&J9#8}ql46QAwzd$$UPF>Yqqz`FOZ +z{#}M_(?gq=F)b;3U7cj|4uWyj?dlfF;paU)0ew86Km8KVWXk=wNy^SEaRsc^cfSFl +z3=vItnBoWIZo4*=$D5?~8+M{ocn+h(9Z`F%65}*tv>!e6s)3!zKFtf5F*5)m{gkrF3N7@l=3} +zYQE26=GLHSKKd%vRY6zzmi+mSkUWWC)|c_rSk&YqKR_E)Z3Yx7KrExHR0K(^Xja-O +zC=-=_l4&y_MagD{K^Iy@n+<#%Vzj0ZWJharg?+lJis69eI)}6ofl|iD(2W{FQaJVy +ziuSfjEI6Z+?%2nndW#JU5X!`jY`fd}h!Zfd5V^$y72xqt(M{NhXna!8;_EBYOesAF +zFO1mx%QC1U76OZD-u8DVml4=6YGT(>k7F79u&gBoR5pvT$eL2uea9WkPGI6Ld-XTU +zDGWEzu}iatw3MabTJu(-Dj27!$jn&;&Pp- +z%`6fzaF?sSBurJ25A@r&te0$OYsb3@^!G@*WRpAz`W6z0eJ(U1>W8oLNtz{DbeRm{ +zv?yiPOM-m&6v5|U{6y)UH_moHA*vvpp>w=Yt1w#GuZtvnz}E +zyKY~Uu?%NWp0e9ymW+}J2jX&4j51*%a&I#m=z~lO +zy46|4oqk+nNt}BH0t=~|&u?C+W&)|*#D*lQ0R6&uAk9s7>%BTJIoS}hV(_L(OuB1I +z9vO@};2#3QjV}8cKhkBjzD{8j#XXO5eB3W0Gyw653S{ecGDUcecu{x$oD%_$cAJcY +zoXK?&E#KF%aDxJ>FvmL%x&FYNrb{gvV8S>v8b6X66i2Z&IyaeE;uH6kp8EYP%;GLF +z9tb1!`@(E1+mXbT7*oC5VWV3#g?pH4c8Y~mQ@W}ULyyEOwL1_ox313h6L699+-x|Q +zPz$(*lxfr_>S>|C8E5zI5~x2`CD;%HV830;Lu7Sx4-cbYeMH +zpxXu0Bh)KF)A-CGQ+NYs?#7pC%?+quJVuFCZ*thM!r6HO7AnhPxwk1m>Go*Qo2@QI +zm332Bt*1Eqf@k~lueEe1@@(MhUd#2@&ezmdQ&At7k4Kn^2{QJrWNM#6cJ^I$PeadK*&8mSErGGR~)*KOu2v>vrkvT51bQPDOLBJM@aBxM%rhuShEZjQh#CbX1C=X9Wa-o_8JMEZ# +ztF=IpZK$4rR+T5+>=0OcPY)M|F&TE5QLd&x4sz)_? +zL{}JmO<|UpP1ZKX@?M5AhuiX9ilfvB9cM97T|=uWNt>U+pI0>{ZhN;}MegiOGVXGB +z(^Mtt9U2_vf7aK;AkVzX)RC>`DmNBdH9N?tUqAhGKEeNy+J*x7$4@*+LgHU;{`K$w +zv1y!*tPO4K>6Dcq0f5C--&D2LWYnBop#cCvo&W&=Kz=*RAMK4x1JAk} +zm5B`fFnycmX7&o@+zf>^%$csD1;rm5pI#iBnVOK0nwU@?my-yJ3g(x;5I>jC9TYjZ +zHy9sJ7+V$GxRQBdwk}C6zyLJ4 +z{s$0AY&9avaSja^0e?p9?w!EO9Xg0Pwqq%}k1_de{`>&WEAagZu-nVa9q5uvn0q1S +zH?zU_a|~>xvNLBpLMC1x=*gv_OO0)6I>Q>R-=`tX3s0jL6RsuDtPoRo|ClTw_y;Yk +zwc|b@mZ^M|VGjsRqy{5N+$-5b6ey!D-2ecCex}>T)fpLBq78bLr&i+TJ#W +zpO?1|q6!7RAT~-03&JW8S=BdV=!_}G8MQ^sBzCk(U~s6*qQ&Q#7Ntw=3Qhd4FlJKf +zv@GSJf{90n%80@ohG#OU10SB6Pp2B4~mn1BppQWcypsg0JO=Jj`5J=|MC~ubV&6 +zlXGTR&37)h>nK?{Rly&N+Ep73nKfn%^tA45%xaJH>iUgyYuma@)1x +zm3Q+vsIds6+F*5C{6t4VErRxx2SNbRv1h|&?UbK!E1vzCP=JNWivHT@^lY(w)|I-o +zR8+t_xlp^id2T)_be?sn9-BD%)1kN|f6CuAgT|}oVu*CV-JS0ht{E#+j&{ +z&J|a7%rrg^B=4r~yjMouP>#tuDZX|q(98F(VdFPpQh~=eQoMajdKxKXx-)3#G>{xJ +zp*~8@Y(Bv6p}k=!N#02QC>kaZV0Q_V#fm8L_2xmy@enWI?Ys-N(aa;@*;CSApCWV> +zR^C(^7*ah>boyHFEO?yYo-am0bAT +z4rRfidqg&8Jnh9SX!5s#t-R8?MOIYIur`r84t%x@fX2=bjP*tB;*iSb21cx`_G>8} +z$4JP0Mgy?@P;RG^QNY7}1Q96W7K$R|xk7k_c{?O2Vju2dKahf<(EQ6vV-w1mirIwf +z?(TZc&Q?c2h;?`F>BJ2argXF>(%}0$bIE^B1B6c4^tB0iDF!!~xd;laqZ*j$5d;d( +zGDMY%nVf$nCt$|E5aKIktmIbZq>Cas?!wIhfO6FF5_psL=Q{zwLWQ1293o!;nU^B| +z5v7@4WxJr#sXJf+_*;{e{9&+-d$uof7KX3MhwobV#7N$Z409AFYU`>Z*L4~T2B05z^_m98j~CQ~Q;;Wu1--;ty?yFCKyQEc{8v-Y06!|y(L^5} +z;ias^;}vc1cQ1IxMx>p +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 +zj +zs$q*_R(6UUo$X{bAE^}l8jMkT)Cc`r)q!l!vvapP{_3Oj+Z1Pnz`q({p9~S&CtrV8~ESuGT?TPA2~mfa@?@|>4}umDtt!=2IhNu5_{hfq7hNg;|x^OqOj6Hy&5XKd^T3W +zBs3bbXdGZ!$#LlBF(ELdOhR9Lnx^Q2Ks2TxtVDpUairGZi91Il)|Ii_ihKJ+?=T*( +zc&2YM#uS_0uLnIDvU0R{fX-FK<H7E-Y&H#wc+8u;%u!T?dVu!26!3r4w +z$rKD)&Y8W{tR9ye_G{HiNjhdqm@k?C$>V$P4cydgyN#ZCI4>Z}n;z`J(=Vo6^rpjw +zqw2c!kzLkM-(1|d*YfJqq19b3_c_7B3aQVRa|6PoADTX;(CayckkINmitES6Mi&f3 +z*Xx8PNZE`V{KTWNwO@UGa;^PD3E1Wxvm(aTvXe +z&lo$1dM44&LckD*vKpq?sSj1Dsk{)4zgaDqlZL%on3c#`AEXYg2r%nD|7F2w=BsaC +zD^{GB4kSYQfKiA<0r?`cA~BxGO9ZGLi6U5V1CnuIk(EQw{I;W*wA~O>=a$5+<4cUV +zF`-dKAC*}Vu07i#pqLdXS;3gVv^Yq^fbv0(yfE5qQG+a%wy*ghS0zq +zMA6xJF15|*Yp}jN^&xZmkfRZ1m4cvx6|m_jesE4NDY|2|U@im2FT$BbHFMfEj9Yo% +zqkf`9ef1rM#4-qTL;lrVG`2>ng*?D*Q?^X@b5o9sP^CIGZO=N14dNXj*XCjT_XX*W +zW!E=ZeM2)i8@uV1|0nXc|5KuorY56dfC~T+?*#BO#1IF-%-Y$;!ier)(-013uKzpO +z_V%9_YE=HQN?2!&|0&e)OR$(w$iKO$Yrs=TTZl3_Q0usNj_;KH%QciQZaN4fLJ7jo +zG`{%xd~OXu{8FBBt29pNdj%dP>QVq1FW$5LO+7%KNPupcR(L@XbpDe4D$$#!DXk1y=O8e=&l!; +z?}oo&3~kF#r3yeCU*vhGARF%{NbG$ANC=I@r+KgDj^v>gj2LN$ubTO2l3u`9Xbp5F +z;tRf~Utwk_M-a&l3~Ng;KO>fii0~?pmWxx&0kxcW1-vSO3$z2kj!X}i=sdV2fk1nk +zGQY#RN(eP%KIo6ohn^NF>6gSC69+wfmqU-|FGh}~y8|;D3Jn`U!;=Mn_i+^ +zPa8ujoR+MPCDfAH>JOqM*7eor6~;1JE1gg7rof5-P7p2iAw16(qGA)rZI2b7{=^=K +zUl$L2tgk3tXN^Qf7rx?eF(t5^Yf{5pj$(v>bb`Z%BI)!x>LA#@nRAnx+CYhTG3keBsC +zu3GeB@8axYnZa%o04^E7Wz3RW +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 +z8WBNVj(I1ZKdIK#}Uo7TJRLyOV +zIhfi@dB_|Rd(OK4JdpSDVRb<^+OyrF=B-zpak +z^O{wIR>)cvYcgvGO@(X9=FzjD3mfpR;D>Q?#qD@=1=Vwl_Kp-Qw>2yOyvR8d +zulRSts0hH8{s0&LO`O0D5`gDSM1lv=6( +zZ$}DL@N@5ou7*~RFsw(lD=Mx5=TC;doZfuNHygjseT +zV`d(lQt0_Ryz*Tk9xpfg-D{PfBJ=fpj#CMdX}5>Yc|CaKEErnh5T6>`fOgp%s9ri~ +zYJ}zjqNcMCo!=|?VmHkE57SIT){R1|wt4+Qvr0A+GTm6D3REwLhVQ3lS+#B+f%sK5%yP_}0)GOV?RXIMkCfz*o +zuk`buK*{B3{2e_>%eP0r-_qH&V^%D%*tAddN~K6p{-D3qT*EOxIRkIcmyW5~v6t6~ +zJ%%=U{tQOkza_T#FfO2%G|-NkE{qcvgYuvEnjt$ecPhjAMPG!r2#Z^!a9#R96Z+vQk@v03Vo@(O0;w*oBQ*&S%tYshH40$Y)VS +z1l~~Vi#1#J=-yn?`BN1uGvi{g>Pj(I7?NT>^;OT19$*c_N$C9+9+<&Z<0$|&JQ-? +zHxbM3Bify~JB9nKwd*4bJAlhY8{?IxW`3%vKf4@jAN?Y&O=nN7@n0C@RWCi4`g! +zI6-bYdAGyQv)Q<-Woc}4tTjLWT_6lM?}s{wwR+L3a`@8{5&#Q9$*nSKam*x)xt&)~K3hL<6jQ7e=#^5w2(p}3k?jo@|A+ll5Nb@WdF+I`ox>rWuG?4 +z3)AzT^H$EL5CdKhuuCF8+;2tM3>M(FS*f+qF_D2&o->@z;e@(LL^pdTj3u=lUWje$ +z9Srar6kR8rXf0+d3<@yri^3hu(I6)<6@jISKr*Zs{>vlZzx0~u)x6PDQAj%IxG~YkYUmk5(O!%at<0fQSZc*@FcfM~w(E)Gz~|Cvcf?V)<)c|eZAIkrdaA_M +z?-YjZUA=zxs9@+?=CrIA`M5J2m1qlVXZv3O3w}M)W~td_F($>u=yQU}C{O? +zMx=OF7`kK$6)vsA0b|iGMnudo~d@k)YjMIcrO1 +zI=|GwvfEGF_onLia%4LbupVYcIeh8RVXZwM6dJguo(gd +z%MR$7w-(fZ?3|DS#9g=r^pZK~m?j(f+cwwrtxzGcjn~PqizZ>XkbzxW@#t-k{YxDl +zRM@{elpbFX$U7k)6WTFJa@o@D;EK8HzjcwVpn?U%;fNSedqZ>Jf7;YD3lA|V$RwT+ +z$E)5`%VLOrdk_K3x`ccoibv|AwqZj-Z*@cLET`KqI86C~<~-nl>Z~|Wy!reEki~BE7ljPAetrnjd +z7gJeGm7X+q%G*!$*6xh*D@n1Twa3X{hKosd=2RmuMiS0XLNL{7O&a7OVc_v1vDTa5 +z=HQ*kWv&G~Gp +z|6+*O&sL|0orbF-Yh!NNH-^&RJdnv1IiEht8nyx7?V19MIV_`E;~kntV-KpIF2BP1 +zlMTQAv|Y2Ts+zL=6QDg>&hZh7Psy-D=*y#t%Qh{Z8>zrs#-p_XYU>d71LElxSQ9-4 +zovv+moHA12tER=K(aemad3ujrFCdLAPcG`sF}zm{zHm-eAEzE-!?b=A5&{T- +zU+;79XcJW}{@q>vd`VsCV?s}aTl@WMy}g+h>&TeBV8SsX)h~XrnOH8%$1NnV?$TqnbV)b%n +z&GMe_uzU@|;8z+9wx_a%j4qDkU@bHO +z^8DbO(c<~Z;Kg>oxJptPk6fB~mIby`lF8gqnGnVO()eu|C1!4$t9sf8@w**Ji!^ir +zG9g?+o=G|Br7VTQ>=Z}*NFCq~$t=gK#N6x*Q5K3#`IS6XZ`4l}WLiXKO$aJ`u8R7L +z*&o&` +z@AI5uEFqY~W&ps4hE7*Q*ryW!6g#TZCgLh4s6b6UOEJ59KEJq@bd6N*2qJwcSRtm* +z9yYn0?5e`%6{}C-a!qVqE*SD5!n-p^n-)WpCyJeCyENT!zB`viTH1~=^T+0oQ=2=> +zprs|qC0@%;D2FRM6eVd>?MNU!XA@0#5poecE;I<+aBMv)6j;RSIFx2wbgB+fgM}gK@o|HgC +zNJwcHf|K1)<&v0Pc!P1xQ*K0y71%7J%5h5&=q=^&y&SqwPmK9u3Bw9LWzZOO8^M8F +zIi}Pe3aO$A2MdVyj8O(Qye0V{k|doB7KmVsFX;E#sFckIQ&l=(d<2|W9qai=xx#4|mzs?0H6K+)Vki?`gepRU!zmX7yF*5J5rnzDBAjY=RQlZ84z{s>zXcL +z$GSx;ytl}{u~kK-NrTjw67J2-E73YL8(JE#ir&*X9!;jRXP;Rh82^5`KVH!|+(oK_ +zuAjA=+6(L})wkcy&>;PZ8JUyYInI#RX+-CH#x4s)*EztB+csw2a$FkUF(C`0^xuf8 +z6&MF3qpDUiQKC~7Z>Wj_ps4qpVCl@%e*;hC*5!&0Ebahs8qUxz;=r(gEwTCE5lGD(ZhhX13owLoRiB6rUgt`b +zjk|jW*KvM{NuNOkyJS4bxcglOPnvq!Ol~XP4+`n3E07ARCIc#K!z<(3k7DpYWQ6%- +zHF}p@Q9K5ma6VY@e50|%)ZoLR3TtKSIY-`l6MScqldCmK$<&+6+?*KA_wDfj +zpycl<9_lMm9??+rUS2sYxxkZqhl!QK&HiK7WqBc67HSDh)SLWzw3=ccnSC}(dizPi +zZGByr7+Am7fQ)Q$)G?`Ch*R~tdbxb^Dw&gKyzh0nxOz1FN}l`Va_@2*2dKo$0GPE-t6>wGw}nCwlc +zjA`{R>9@h%4~oj8b|HEmUV}2!jW8Z?rpBz|f0^lO>jpiVej?NtD;9cJE%;UY&eYyN +zY#`Z+W6n; +z%qfsYyic`vUBY8hrQy+v6*j#K_~^^#>*=DK^kc@6=#nzdPLXrFL0-G%z^I=FGvUq` +zldNOsh~gJWVS@rTDgnX^Ze{_qE!oG`+iX;N|J#ysu+IDu-;%f^s&BuNSnhdz2dotx +z!h3Khj$YvTOWij7PHys;9boF6uZ(-_m<<0`d??5Emh6r#OE4z>MP3uH*8uOs&FP!s +z0cZvWCh}w_?ulN|fZtZ$~5!P^6=C`3g=zPA{xA5nDH~t;6NiDtru8 +zg$~1`w~G>LmwW`hgfW*R5MN|_wTB&5SWFYH +zh$%6@pn1dEJ%5GSq=hk&#>697wD$(2uzyDQjAnyAj_ExKnRFu&1O)K^BkY}`Y>T=r +z&9H6TI$_(66DMrjwr$(CZQHhOTNPPZ*{bqZ-u6GNwx8C^o^#CE))-&!y+NZ+XvH={ +zvgi(&b46D@kP|;-!z1a33(mg>wMsbD`M#@yyKXk!R$IWW +z@k04a1YwJ|91D`biq+uCd0GA@(rF|S-1Nc65pNn{vBF$uj9qr-0REi<##2ukX)We+ +zXyS-T^YkSVeqMMs0B)69JB7T*Z2SGJ+RXLh(gmS=;>s!*pg{eKR65A7RHN1!lQm|Q +z+APJ+K+@e|@IP7p@O;I64o9OzzTin|wEN-%0`1_J)bA&d;!D(S;LBzEO<^8MLQM*E!S9BJ1!0l$FJ=${i;GOV4R= +zQ)I4WkN!F&M;)m?3P8>N?Qd1%>K(zC-iY2N6&3@137p1-X=+-5%A*!FdQ5fjzYoIR +za%-MLxP5*CTBLDytrP!%6X30{dK8oJ*^8DJks8a)@A(*vbun^KrNR*S>!-o$#Bc?o +z#XJEX&s2UZ2G9A5&qT$e702O$X%Zvvj1Ns*8h<6{vYn&B^4`+15t$UYn?hL +zh-Qr}vmRW^;=l+FC?}|t%@8F2Rq+BS +z2r=D!YZ>oFc(o+|2g`Vr#_F&X{q2Zm_6&qhXVcY|92J)w0z}h1>3^f*I43mdcHA|2 +zqmdPzhgb3Sh8lNRQlBR~)1+&H_XtvkUM0x(eHFNf8U!jU_{%35Y$_`!_|?D3MHa~> +zoSkrm-I+R&aE+Zvi4jenyTVPj*8&(9LxW>)9%M}R1SmR2K|b?TW|Bi>UWIM~H&Cxr +zoVEzCCZm-YLUqPv!?Q~?3L(_q@Peq5)G!n>;zhRzU~>ILcj0^_O*-9um06BV2ds~s +z;0@DjUVy*wId6SbB1BWsSh(S!Ix7FY`V#n&EVm0`)m^xzGK}^}FtX(zVywILC0KJo +z%Q9O#GE`({6C^7+=9?4q4syDja{x9bCA +zFSjrIBlm&zw(_|vNXe<#MT8OV6AUInv%=fMSk(3E +z6A{@9^a!N^VQQ_c?k5`h5{CGPc&3`p2ER$c^BYsqIrF}K==#htzu4C?${a9M>KX-L +zT}YZZoGGlhE;lX>VL%>_znN3!x7(*L8WtyUjlW9G+Qyu`Xxs)Iqa+hzknT`!A=UNE +zaPB;yb={IrmH4Ahv_eUwO@A7{l)o7IBU_9Gge5Gy*)my|AMbhdyKdxw$-_A3<*tKF +z5j78AA)$e2i6M@7>DTIN7jg3!y!G`YD1?Cq^ZN~xCy30E?l3W>{N1PUj1f{ +z;2x87a2p(Sa_rzU%lkX48RH&>3kCM^;7)S1v6$RMN@G%AH@U-Sa36=k_8mn?u$2w@?>HC3ZP8l? +zHQPc>J-2%^&toT6(I%a@kPC_2_QQ%|=tV1}vOH9FyxcAHA`RQ=-!|Ez_Y2{l^RV>c +zZxQ={)il;7?5wjc2L?bctjcdxYK$TAX;nsUekzuE)|sE8#2*d;v%k7Q<)op?eHld` +zcIz_K)_;qIW|s4K5;F)7j&xvFO(LZ@K@azJCmHdO8OvgV-rfUNRvQBPKov<8KD!(& +z$V2YUDvt-MJ>5nfdaal45IuRQ^C=t$-$cT5!Rq$E97>!1ZsDH@zlW0k|H<~@e|{AF +zN~0Vc?VSu9ZT{1_bcJgXvoU=4@dY(pEpS$;Fm0_z;y)vbO(NnuOpgF=T2Trkr@2*E +zES^^AshsfnGCj%LDi$qs|oZZ3H&>(e8BQc*Z9)C)eKH?*R{j8+KYSpFH +ztb{9<|N7x*#D>W?L-cwoqqD-&_-A@r*{LZS<2(HRsknUOG_xg1X*j+5N?Oy%Y+~JW +zN^!tRyLgImhFpG%OuoT_Oul2I_W06LtxyJeO;~KF=t(2_>tJT*E6O{@WN;Hq*9cvz +zSKjH%rr8oOmcZq#T|9Z~3M}#dp=PVpNqBU%CAm5df^*{;3QXrcwOl)K5z +z3&X1;d|gAifg{aA?|KLpD?d +z8{8XJCbX7}w_KQU^J!0LG2g)@BLJxrc@US{Kx0od~ +zE>Ap*yrM|5ZUu8AWtoMs7_!YdEwtqB>GD3`6!!5jOeP$1Itp@45q@Pkt@MFa*(?7I +zI;H;L0C2TAPKr}TB_T}I%W75yv~d0zg#-`rxAaRgIUDGzPu^}H~f0^SG%t{jM)+bqT(Z9h) +z(m8R{DReC61dF&^WjCS}hkz{O-0JOLpG}2Xe=t4>^{3>+4Tm|`-Sij%pzr%AKB{%* +z-8&7<>}woG(1U^ulGIHiU!*kn7KU_u+Y0lu6TBX1fxmyJCa8 +z(Hf4Bs6&*02EhH;yVgK#PE>z98bVl-@2euR#b&ppD1a_n=dSi-Pq0s#!C}#!LSPwL +z?wSGkmKuU@+tCP0S>K5DAFXmDM6ikmASSi@fs_yX^N1>DVF($HXYgBtsQt|qV8h2M +z!4$70MmcnXZ9nb5oC_=LJAm&}Hw2#jYo?h`RewTFQ{U?zxriz@pJWAZ-SeXb$n7p($AFbNJB!03)Umb&+NMediyDV>XUQp;A=T7ed?Pt72>eddGE6L1ZEI_5_ +zh}_llP{L#p +zqf&O^xg`B9l#Lqc`iRN31=4#VNUn0-$5PA!7T>b4`8<_}S3(}JrDaOqQue+#9E!Y? +z<7}Zb-x(atTAl;1gkz@%KIU}^Z3ohyv61&NR&qi^Pce7@tGWXw$odA4bR+j>?%&`m +zqkqUeefCp$TxQOZE!!IoY^BxTMWh5y0Eu8(TzEX~<*`rJ +z74CfX=R5VcUVlYhd`mctzoDqcc?{GJ&Z*XiZ5f2DWGmi{?;r2)&m!GNzCSm0uu$`z +zGT9gA=VLcibZ{t-s=^zs)Xv7_nWHMzyauGfQ&KPojJ}GLc#f`Sh$~GU!?C!src=D0vr+Rg12{_bUK#axkb%$sF +z0-_zgz0D@vj>PZ^;P=8YFN)-FYTWn;yIl!qy{%Y=Q6?*RdK4v8Eh2j{4d1CfCQOg* +z<>7&gVjlKTw%9kS$TO!{EjeWCS@lS4uV`7`(Be$q3)hW;f7_27%O%5Rt`)14_mh{; +zR&uTXV)R%8L1*&fB9<0sb9|+tE#X#(8Q7Esr;ZAFjD?3Jnm%1IvaRMoB*AEeeF+Q&mRfu0e?Mst=k&fHTpJewrxi$lB&p8Ms1*ps}kmT4aX&54L=iH9C9soSe=6={qXonUzBuW?7QDxs(t7Igw +z_#x;SGXwUF-eT*0Ow|X{E~8s8Rye$ZsEtI=-u7E{hn*&?&#G?RESi&K@fi#v;rO@0 +z^iaK<2Dytf$3-vv@JZ$FjH#HQ252APt5Vbte0)R|hs3St(|M?PXfCk*aasOhS6$6~ +zQ%x|#BHpf5TtsA>XYIB9)XPL~bB-JQ(CknDoh;z%(=w%o-6g3bQa}`vT+{t&&kfri +zs3c#MGfXM|@|fWWr|{Q&#**kx=HT{Dp9nAjG*;KgXnLT8xMiL^Gj?HTB=OLFj=bp4 +z0ZwP#(oV%pu@-8O89=-{aMTi}>!d(83Zy +zpGSc-6i@l!Za*e8yKd71TQaHvMYk4(r%-C-8E?T0_k_huuw{hY(saM{W$a!qMpNqx +z_UIoL_qX@A=%S~Hx1*sH=!*$0R4szfcB&1Hs(}dr&E)I%JkdWUw>IC!TAz$D-j?Vw +zUIaBcJIWjF_;Zn~;}cdB+K+W|wy>tnlkR)I75F6(9pwtDV@jkUy!bIX@48`c4uLkm +z5sEoIw-F#C9}#au2ScjNmnp!uQy{g}Q?I1o9&h0E*xAQf-#pxYi}dVXa8(P=Zq#~L +zdn`pA#5+{lw7?LC?g}a!uxQ~dCBX-D{RmVzf=n@Wl*=%JLR0>Ethf-vX`n!~oW2wg +zV;!a;u6s`~_n(g_e?&*80w3rFsBlGv_2v#$S(e9qS#K6g1McQb{q96@rQw8$U*hXv +z+($o}HP!jh9eF*Bhw-za^!gNmq6$vtm}V&9!=4$8nh4`568aa{B2XQ-2YLO-F;N^w +z&_pH)q?GbmnvLty1?XBE0nrrcrnKXkSYU1=u*Ad*+mIH?`r8+K|70yNMl)}NNR+q> +zps&}my2YVS*JcWH${{lplEqcHarOf-bBDyzRTCwAW)o8%nv^?X*)qWQwbN#ZxVR=} +z7~|uzp^pR*e5v^}t0I|`&9P_{Ye1y9VY4B;Q^Ag@)cCaa{!5qA;*Jb>& +z<{`Y>>Hui`Cv*P00f4Z?Fz4;omDK%_M5_Q+V7@WT`<}fyhtubPL%9zZj?XtYsw(Ex +zE_C^)wtz$2TLZku<;On$_Q!=(i|3F4go!qaxxA}F7 +z^d;+laz<0Wpn%gM@%zS$^dNw}Hr6Lb`eHf3!5md9ck{SZ@>`Ub@Y!`={knfR1CVj0 +z5ev;=ZFLPkdqGGi!9{P^jaXB7*Xv|*rpAV|)1AoWT}yNVPeXK}%vC@PRFF*qE}Pdu +zba>Q6gNP@U$mT1KEMlHq5rowD9thy8qPyZUo@`vN+%lEY82l`CUyh_w5e8#*_32qGf_p +zs}J2#aZ58kv91jkHWiGS2T$2^q^exKkAK(hxvy0vc~T>&9P?Fjl>qQ=HWOMQA6o~M +zYT>n^{C(k&HZkB8Oj~9?l$x(%ODx2hDQ3As?(VklP$y1V;v(4F +zz%xg??*-J7&@H>nT!gvzsmZ!UDDN@-N=+!^NItEYW4{QIOx|v3w_tg$9_xfPi%HK> +zUQoUSrnp6(hN?5c2-?464KKD;PGac`K?)T7abtb4QOIQ +zro=l4^4)s!P_>|q5ES2$xX-r`%_0css8(p&JOF_N6@{x&JDS2^C1AbF(KCmn8qUlv +zV&Jg~f<{vtM4GFjJ)b622*DYs-UQMn7%1I_Zu15h{G|+d%(ZLsK{YxDN*SM}l@M%r +z3vLpCeGG)Oa>kCi!BFRGP3$0& +z*LcSs?(HZY>{VoV9x~A(TpirVZ{9N~WZ-Zs*n`{v9x{Y-W#?Rlt6G +zf2(QFyEW&4DS(Nll{F9 +zxf1pak#a7At?>K-30W69!%Zj04B}PSM^Ox}KQM5bQLbH5wT&c_<25YrIW0daj3v-^ +zJTvFWiRxt0c`j`^Bh^E`=#~3uYfPE7a+^g-sSVQNf>HrDAoX@^@b-zy-Ir!OQ`dzi +z?yFn(FytE>f^V2K16kG1=kI4E1n^$x=dUlyOg_-pe}h0!(UzY{xyx#`l`Wkb2D#h`8m3Z$B+~2O#zNl%brTqdZAZuqpgrjHjTA22jq{+~e#u&h`n%ydACx)T}E#t4YZ=|x8 +zz-^zNjO}`cJkeSJgY>hQ`k<*OGDX;q3npA|6f{@VL@wh$E^Rd}T_&I{MbR`{OHvmH +z=V_J#lZ7m>L8yd*MpB1_Y7Lf%4ugJ18j=!C6a-v(QkBA|_5}RsjoR8jWyVfb`AZy0 +zS%P)Y1f+a2N&;X8(@SaK!ksn}ReRWpfj6LuN;y1tcQd|lW`LNbQy8E5gqSpAT8ILt +z^bMbQZK8MOzRohcn!R&L!Do&p%2`}D&jpr2-A9OTz=FJCODIVLiJqApxJEKgtq|qZ +z+&C}CBynugXStTfE#J0)apa3HZ8XCK*$W$gFBTEBk~|mxtDG!bAh2cNp<<(EeJYIBaJM6H%89ynE(H8LGV8}hM|*{?SGsBTq#V+4iX@AzfiVb +zEi?1}?ui5TaGw#H7m9A~r>=A_s_uvoz1x(kvsUih09__7S`K92;~MPrvw!G|nF +zp!}6?PM^>?4Mmb7E9{N8O9{-cUtm89PI^<7sQ2B7&zed|x*3)S@M=l$swVNRjiach +zL+jRvCVf6&G)fjlSdxT{2ot5$tkqND2}L8v-{bGr388*HBN@VdKj`M;#q-^hH84OS +zqV+dGdng<31fQ@YFYSzF)$T>FfF=+ktvVENsu^hNIhGGV)tbh#(JCzJQeI|Y39=F+ +z8su@1t4`avI6VJCY;)GX8eB)KL!)jdwEq&T%u?hkW0fwiLOh56-&gQmd9^bY>$hE6 +z_J49_{hupnWM$^)_@9@M$IN9zG=AradVQISfL2n-%!M_2xn8WUth3X^qciz|u_GQ2 +zI4(961cVvHR3pLA=R^4^h_@W@uS(|NYcBAXmX?-_3cjj_Nr9WX(wILf{+|R1rTRm! +z_<@>znl{M?Xyqv+XKh2ZJHVd~)xih1&Q%Q!6Qd^aeU6Qd4HL1O?Pxe{E8H$F*NFgobNX`qBS`&Ftst~DL~bh;}<=gt`El>D?6j-Jlmw-r+o7wtEN`m +z(G8Z|#vhmL@rU7H(qA0vG~ptS?aj{J6vs4uKdPKPG-#+FIRPqv-0EnM3zLFkmY2!Y +zG?OO-7ZzmRJuG0drAqUt@MQLBv~dpYoRx}=YS=#z*~uvjw;<=?e3ydJ+=+KP&T)7MC>K3fRM)$?#UJ73x{+Ef^xN_> +zH14OO%NpfrA`DxT?%2Y!qEm6j>YT10=mi4-H`?9UJzTHGpT}3Hw;Mi@(YaqUSg(xk +z2z5N)7P_64`w?9guGv=T5fOdYwyhoH7G>r=)u2N$*r9+KgKlvpz3X2qpb+>{50s^T +zpC>|`RmS@^f||**aF6oKBG1t?0H<=FL0DM47niuJlxFecYIT#vmS{b4t#9e%OO>g~ +zmm%;GjgsEFcDzO@B5K7ZYp5T4+#1OZ_|3R+D6!9^;td7za1?o*cp|IwmLoJEDPff74QqU@@2q;JAT4z=#7be2jh=KMj2ZrtBYw38$X8uHN%O9|sL +z*Sr6e6+zBH8yyL|6c(4%EE1V!G>tujW8tAgANy!jO|GxzGJZ=w)k#Ev!13P=+s0Ad +zG=yM&UYjM7j8hpCt9cNSS3C%#2p|iwQ|3IDaH~01pEqjq1*!oeJ199EuQ1W)j6lBw +zF{GufaQsIE8kecd?pve@RbrWceu)XYmV69VDjQC-)*K<=st^nDtOq8ktUa7?h^a=H +zQYlz`UOkx}`6NB;@ORg$W%h_S0Do7zpsVy}60heGSm@)ba5VX0(?^FG(&z?lA`l7g +zQY#tj)r$|<0cbqjaY0}T2qVpZeD9V&pxzNkXsHMQ?ovmIy}GJZ`rZymONLSz7cIg~ +zQ^}N}SOT>qjaqxXTF~N)?TDrjb?I90+eU>4n2T{)vaSMp!C~xAwFlQ_*!v6Q2Ievn +zk}vT$f&0EQJ{31!I!{>3YTWBZ8TiATb*UU^+z*+1Wf!=jx+uAu;j?9;{|sNSJuMbp(dAr*h&|>Xe$jnTM&Pv*GWu>?U(IoK +z12w7sybFGZd}2f`AWuZ?`{?m=DhHYNYW}A_Y}H-{#?)d`)u=#lFt3HmPMPt-e`vUw +z2oRq{JR12q=YBC6hLE~}Rn5NqV?b6$YdUjPYo)nK34l;80@n0YILoZ5nzNJHrTG@V +zYy2r1iuu;r01PKXM3@oEUA%;Z<8x`Ve*P^-jjXAz*ox&8O{>dq0F%UFj(|SOhu+*rkDvW`WUIE(Pe2AXx1;LXix3 +z@v_xfL5U17Xm1r}U3;~~E!+YLM@IFc=HzAE!fs?`YDkC$@Fmzf7*Ds9XKMm&v@!2{ +zGGZqJEe%^irYHi$5Y*76Dh4>&bN0tevk;A;ws+bRx8RH8&l^izRaF3H8Jc(VrOok-Eb%GZHv#L>9hl2? +z_4nO+4`>`PUSZ%GbP-txr*Bk(wvy%%?vEZ1&JbRiC&06ltJV6MII-2wZlr)s5Sz4v +zs4#($o8l!E5C~EsIc~a;z~@xJ*#`}@-#u_2ra|skSAx}t)A1*R1C~9}%Q52{0diFr#uZrQWx#*^{BK^_ +zobCq?3|a~IP=6v_C~BYKo$73M1BjvGXlC}^WAfbdGwWy*hN%NGv*#C>RXoN!&xC(n +z=5zYowQ4dVv6ZlxUuJ(JoagLEp^39#X&mzKgNx!a>~Ud=O4NXG{D%R`z3O^f4efA4I{`(Mjt#IUJ#6; +zMqxzzQrt`k5tyC$>lnqhBTE8OPhX8&b@vY=up+H9*X@UBeO +zhSV}>(f95L_`n#bEsy57?uwDu8e+&ogU)K3l4EeThwF55s!*-56;Squ^bGlOH=oS| +z3xs$NYUrCNeOj$LyJl?s6Kb{v4SF}^hbnxOtvbaNp2aeeU@#>!M~kV +znSaX;JKlAQ*N%Evksa-`vOPG`yeBdPk-pB8wB}}>Dw?dZ@sh4;bev~X18AZqa;wG1 +zhg8eZW&KkDJcxW+1>^bKkM)6&dDB`zjNXZeQ23XCa*ivkN0nDzLC6*DiAF6reX+Or +zH3TK}$&xf`u*+uKWw$o^SZk>Ml}2X8o`=MM8N6DP%38z4W7V9BvfZ99EV!VDt0KVr +zn&oS2OkaM7kvml~T{(VX$4!US=iiBToM>F3LDM>}zf^*e9$^8c;pHQgUp&+zwM)z> +z^v0Cr*L8$MbG#&e%O5V*a%VeYrn0<@5GSSBvsnL&Dp{~(v&uVI10*^snzsZ>Q?gB# +zns6WaC*>@w~<+^9GZMGNtI&jH0hML8bKfFBOJ-E@j>F#3_I8JGutif0*s;)8Ml@Zq!>-h!s+<6bXE;9~9 +zfj1AMb3QIHW!7(*1l4@wkSGAfisCJRJZ-FI5Q3-kqLGhugRM;^$7fXyPKcFz0gMT> +zC;|wNs!WKb4w>!*r|lqvfl;;(s0+4C`7DRnFjS@YI$> +z-uiYT4bN5F?HiEH{U{v`;sXhf79m(50_kD_*$O +z(Pu->8gl9W#h@*v$g*{E1~|3*v=ARsI&4ELEFpl7Ps@0GwnrKsoHm4mz0DJD03LrF +zC;R~X%?F9-$U&O~Mdqb#{QosYwz!8Ve}z|IZHyEM#EA(F_;Wo@#DDtZbJb5XfR2`} +zTxeF?Y*3pX7F%j>@Ei-#&6!m_!h@#OES+bB1Y7wXL9o4~ZI52}Ub$enr)&W(mRAXp +zk4;AlGp6o{655fB6Tx4z)OK9UVpQBaHB*$ud4}*6HIGFH1*_p1k6a+$|zp>Gjluu|TqDx}X)DJa +zF*~h7%g1@W8)S3rXSG+Qbf}Aj&0HM95%Cg{UfzWs1F$oSEUn~S}JG{kPX!kEM8msLXL2cdL^m$^Tv`?1(_3VZB +zDWLDKt@K`9YiM(LY}ZT*7WYgSZeBB&>!FgaA(nt98Kl$Vw@b)L5iV^OxFcPmD!38HeHMRGz +zJazMK$#_9+VyvWF*os?Q+D)EUUyO$*LRTU?)4yjUA0qV~?-dJDvXE~&g6!_aPdwF> +zJFnD;s;3(UIOL&;55bAvjoz70@0O#zJKR$~>Ep#K?le5hF@@MI4G=kHNZ^&i4K5}A +zs4Jgn#1wQ3sBMb!rY)5y#<)BKh|QeY&eTN)BYM=wJmu_Sbng>uh=MO~{3O{JTwL|! +z&$amm_ik95*9S=uk1?gS70Ub1^D|OL-e=NMy4~Yc +z#&Wx1Ehli_b!T9VD)h_5v!&5K!9xY^B@0uf1`@sh^mi*!{;ZcYc-e)+`6=4=1FGT#S5i3w`v2v#i= +zoy14W)uMUt4e3Lr&$bi<%!YEsC~jPuevANlW}1p;n3qwqyFD!qW;6sl77o!om{FOzgXSA! +zNvdnDjL^*t$$K@Fd(is1QSEf`@b|b>!}32%z=ETdt7jt)XN<8INi}{+f~twrx}KI+ +zqnGl;<#qi_iMtHkS`xk;F0#hHEn3f=turG#FRw_YTseifO`Jm{?a7JULqr7tpKX~a +zhv+{y+mf~n6`~1wOGDlSFl#$lJLbKNQ{ekO4J6DFcmWcUZ{}f3alYh99FKYqXZKNl +zgeZ4CrqMB!HI&L6&E$=`*F2#wABRGEBIqFRu+!e6d95|S2667%?Y_C0yAF3w`UWx! +zhOw-vlh3a?wFMROQpS3VN%E;oT7Sfn5$-LC6v-tBx^A}B+x49i(~{TO9lQ7^0XOC% +zx~#&q=Lmgy#LXzvRX;)BJNfuCS{rzSCalDBBdJqkML;CEyyM^1JX9hHG~4o!qs2Gt +zY+{eoI_AMaMndR?yOdONkZSr|RBM-LB{Fi5D{ZI&6oj!ly2<>fV+{fCp$CaY1EFAp +zgg+Z(j+UAW{R;6eOYQjUrTOKKkEO2ey3;sinjz}`np3>g?_YIbgS=?sdgL$;4Bgiml2ml%a +z#iXG(iNEljc5v8ub-E#fUVbiUjc2=Y619YgoET&oisIT2uag3)t;%UGps!zxHM0Pi +z<b71{p4HL`?&6=Wip1Gt2=mU4R=34lfFGSaE!Bg!CiUP_4ibCOgk@Gm;7H +z%$to{8=V_2b)Cz=epdYU2z&1xTykItjns+UQ+x~77|D1QKMdEF5>*+JN?8i~WA9(n$Eu3@2s^o&bSt}P;*H45r;9;5U1a +zUejR-r1d(51c2p@`ALh{0U&WO6GL#=mT3?@cEJatvUmy79+^eT#e{-?(t03i>Y0eV +z*1Z`27+^=w!o1JZr-px%Y0%!>{DHpYV684W@o0MaWa4R +zdNW_T+6J=KaZ36frb&otwlo?3`lKn!j}_~N^&WVp4kf)-2SRWg`02=??`LSCVVB7{ +zBBE}>+aoG%#@Gb4=N*ePh96_Y6ft6uDZpIOpt<}4x1gX`97Z#&KHN`MrWGTQD3lwm +zX*RCdBQ9V)zQ)M28Sq{)IxUD6zGM2?%1;zW0Cu|moQu7dG?a-bMqSx4B!ELM0@Lvk +z%rkX6`#Aoy+;0T=ScNumCiM7O((w~iV4}+un6ngzFbUqY`g1^NSfLzl%5|wQ`FAaB +zck@C_{1T*l#3=tJ40{$|r31p-Apd~1E|+F4nk<;y_;>P*maElurrIs1oAsJo{} +z5R1Z=M;L@Z*qtWwAII#^m%3kumv>`S(P}DhXakG*WYy+f`A{~>4NE(>dP_Ij8l8Uo +zr_%<*+^EoBrPJ>?%Z#`vCfR+mu6rF)Uk&l>LNUD`|5^=Vb2wcbNwvSP_wY|-KLJtJ +z1c~GMX-DTy0yaj$@?oHbBVDJvb~{z#UpHsnLyioEeWbfPUGXvtP3+e!M5t8ZQMSfK +zzfnnD*VHC^HWLE=UfEaYl-dC={RI&ex3v_G3%bVTd8%}FrDJ<7B{4ev*Prn>zpBb` +zriQ6r^wUb;+V7F-->-!kx4xO;@}EoP`)y>mQW!fsXGkQS9_TRy4!Tbf=zy&lQc)oI +zoUO$%)qNyD!?-4)v7VyAo>vjpqGY(IKFp^q!KWc^4|3P3wA-%ZCPLEYS;^b;4KI|~ +zU2sHPd3O@N=~g2EGHaz-d~K3$xW&osCDl%#3|J`0reXDSsd_N$P6vDFX5omJ6VKNg +zff`Ov?RlyrfhZpXf;-9yk7eYa1d8Qi%{rglVFBo=VXz%ZYF6CTq#G*&RtHZnJ1}#f +z$AQfZDbM!+oZF578{N*yb(6WrwOBcSfOq6rAYV+cK<8fgR1B0IFgk0`)dEhYKLzYz +zpBWW{(qWqOq-a$eE&7l(2e;t(-Gj%tdBFE8T&e%OU`P8-vS%vUd8o} +z6D!#`SX|aoq+8`WTnKB=PR+Onu@tAuHKZyNq7>Icqw|To<5yTXXz~^dd6z2wWVK2U +zqZ|*EWa!VtPfgRi^6tW!R)$0IPWht;yEa4ARu%MytYNQyp57%rBqB;D9r^p+HSIUM|AF$yb75jWu#tOdEp>Aus +z9cgzBpSa{_Io9ayjkj4$Zc%QcP^M;_h>Fd?Z$}Q>h}mf4apQMkb$Pph;QmzZ?S>0C +zmVD!h0yi8U+)Bv@Z*XSCkCY!y(!NP1DO!*DQH^cI*C>s7v>dikakxlnm&OMv)EDHD +zI*$#w7v8ogIqIWivESnTDD`<9<>ThCV-YlN=-&uCGB$Ge5uTkt3-oUq%4H$B)G{Crnlm~GhB{vrMGtB6 +zZ%`kTcXIi+;_$Zx5nYmd1Ex`42Je&{#_-~`HEI=xg)e*3G{2$?>K|3WhGAwlFQ*s@ +zXRoNhIfPdm!)FZKEZ3wQ!nzCgz)02XH$;ES*$|jeUV1U@<~m|?J{*{9b%K!Fcpr1l +z10zJ!TEMzSr&*5~FE-2=PDSmQ(W|;~u>>er!!sNFBd4_D=O9Vf*wDn^`K3}fBZ?9| +ztaQF$IHxFGd`>_bQIUnheq2=ckHd)g2Id+`Vmii8(mzOIU5{K6u0MyQ_6Dw4*^)>l +z}PW-2X7{^Z1e8ur)L$kS6+23jpVS|&?2avOUO9QQBh+CDzhC%7CrhtZj+ +zjV7Q|Z`?wiBO7VS^CJStMf14_Kf$H)j3aIS1_8j^k|VIuTCHfJ(dvS8 +z0zgGGvtRJiV=Vs${hFG~mG=aa>J=OIIP$6!EOMe05aYc3j*z?Q8*fo%S-JLXedc)iJ#wOG&^t)xEDPiAj3XCkdp%s%Chy_Hqf +z8oLE6&*wEpmhC4ys>d`6eKg-(c`_D6+G1EZG>2gYPM?pQjMP-(jV&Z<#g)?yYCIjj +zuajVTFb^diw07;jt%KHO4PA}(;*H8a)k2^doRo>{YNC7`+=Z;ft78(K7L|uWtPv&x +z{4%p0+zMXfexp04n8;G2wLL|JjvTD)E7(6x(<0LQdyysA@-hyJFQY+E>8JheEYR@W&M&9(ae{a9aXu!{urZw@A$NTaH+mZ6GwMUe +zN!+^1A`+Sz1jakC@$R`p*oxgKs7pUeKJ>=~HpUjI +z626`$u*#2~iQRbA5Wc0%B}ud&qb&*pls6t!EvBtX^A6A2F%Wre^W$-emqKSJrowUqBO&KHHPTdk6F%&`?G&ux$hDSb| +zT%cGcj2i(g>0?z#V$QOZUSGGS7BQ1TEuxT61}s`bP+muT%Z0i%Y`*DnIID1^EzyRK +z*bPd8fUomW5!C&$BYlxwE14sA3<+XB*Ab-vFz6R=R +zkk!2Ne)J`@4}{5ZlesZuu!+QCkaraF)(_m}4C6SZM}JlZt`K$9%-t89vdgIOpICSY +zHUPP$Q};zLS^F$NzBvV(${+jWaI +zuCn&5nF()FU1E`Xazcen+k%>&yhp%tncy|?Kl{N|zVb0k|0?Qu96BhEnq>*r;0p-1 +z4wX4x!PCa>9HwuNkm?B|)%G{uNqvxS&2^eN$08qoy7u1}>-qxw->C#7>W-w%-)IlZ +zUxn#^x63ST^bG$al&}>i1LseV5cC`9aml~n3V_U3@1$pFHd*M$N=TX5xby(sINB56 +zI$xd2ro&}}2NEX{1_9m*CT9F9QI~k(D~P}}SMj+Lg^;HNm%c)3=vLMkRD~?lAFiLD +zES(XST3Vi&0B!KZQ)j+8w@LAKszVTFIL!a6`eZJq7@P(Va$rgqJS>?Dj&3KEZGCNF +z^AODCq(t{kMNJcdflcujbn~-s +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 +z8QQ%`_i@_D~Z};LT!^vU?0L$m#<*m=W%iDo) +zb|qCvS&^+!a8P$G0IIkOfZRu0o4jxfO~2%#68pd}jb2wLh!>Fmy}4 +zU+o*G;)&rE$DJ%=I=G2+g&?pbXS>#thU%DQ+OBRXFVC8zUSO$iRn?GFV#{sge?-&x +zeXJoYgM0Mc4^>5MG+=iQecnR8cx3^+R^Vp!9jX-hQIVrdEs`{58>irr@XNaNY3!hr +zxS7Y7y_#{6m==y{zcK%3(vitEN^1BI{i*ryx0U}C4mq0qe}qF#>YjER0F*=7Qb +zG-J}rN!r0dz_Ta@cV^C_K6kYbzi!(?nrT(9Rp5^Un~FgA^WRlv%h|_YXUjBe=o+d# +z2`QjzABMC|2*pVf97X&0QXAxrho7Tw!fAJ(x>u=~sA+qiDiPYW<8P9ES;X6d^=3(C +z4286@)BMbJPs(^Oj)` +zlN4u+;|nlq5yz$_(eU$p`SOqYOYuohuSNzZn2|eCD`?}A(`Zt`9}_& +zLN}0@{vG3cd3GgoRRgeG5ADh$eQj_unZyQQEU)mseaDgx0Pd_`4DxFB3@dL~`d1dE +z1Z0Jrl_R69wnFeH8%)1HOs0t+`;mY_q$kcL55-8%mlh<|4@Z0zrSD|!zoC*irBQs8 +zt%YZ`Pg1;o%I>~?_PR21dv3~Fg!>I2Fy)hEJ%XK=YBX@Up&4+2@I$6bRpTz*145Bf +zbOJ@+;rh)P`xUyu`|dr(gKUuH0-s2Tf+6Z!9^%c2(v&4W5`uOSB3}@q_~XTE#hy@H +zEr!iB-h1hh36z>vMwapYx3h*^wZIRXbWd*1OJ&Nx-Y3`igs^!;Cp>w6Q8%Luun-2( +zfewJ0Kh}+EnH#U>5D$%zOu=i?YZ3)XbO*`jTaVd{Z^=ER3eFBU0jmV?4mYWdDNEsFsun-dd{x2&r!!{77*O`MCTG)f-c?OL=L)JOUcQ+1{pmE4dG}R +zqZ2jj&U1~~y4G{2o`u7+p;)Y|kQay8B~#4DqXb^`J3sE0EpnRE42HdYX*Qyzyt9M& +z7?I7JxA({WoQr!$hc}_0FE*@m#9AhVF&>cJCaB53Ovcoo!VO1)vES=@mpJpN$f@dl{TMi6<}l +zRIz!!uUrKl4dEpQAG3emftc9dovs=mp;M%;x0C>kbJ`-aoflEM;#P%DV&C0~EoXKyeE& +zXV$0Jvl7#3N2>=OoznZx^AaM<+ptPDOtkDE>2mN7K`pmY(MA3>@(4XPbU&;nx2iRU +zH)C;&cEwsxqpTbS7x3OoM;;rrOT0dzq{_TkSqHHh4c!Cgni+EyO@yx=%bvPZ9JYPO +z??-DQbDC4U=wJ0O!8?9y@46;y+cx;@Eu~b}eC=}!+%^ccYxEIcSWI3v?2kK}i^bYo +z&$b17_^Wgmh%EzFo((=v){O0lHe?GQ>$>{J3pJzo)m+vXEY@E$A +zvQUc`{QR#Jn?6EvR~a4%=v?GK>W-1{Ob?@YqfKwZDY6Diu?~7Dwu;TabfM` +zvfdaB3_03_NhI)x-h&e?*ox|&VP!c>bse>>uI1NH7mKiz7jn^2yJSTE31TWbri>{5)M*MxHK2AHuYMUc0E3HAaVeN>%CyS+R!`{KEu@r_lZHDpd +zqN2sOP_F?N#R60rzmB7-vF$DX7a0=DQMI~SZ{7_?ZiMgWgi}>ZG94(GNZlcA%87~2 +zIFCGdjp`q38hr_d!|MIi9X2)&n~L(b7Ouw1*H9xCGb($B?14gvxqOc3Ya*6#ljG01mG6bIL8l>JSv_U)^pdWqMzHyzp +zC>|KQ5kFaf$6B;)H#FhlPj#qSN29 +zC4*E7qSV7;5LJn%ZsK!U8k+VdM|$&O`GR~xgM#6~h@&;ai9%0s+Y9M6dDYp7zQs38 +zQ$?mM$n=e|zj(R#WjE^8QBE$!wzO0EEx0ms5wf8BX1ymnunVeMvzyy8_JaeV%DkDdaZ7&L8JO%UwQmPps+@^(-BG=|G^W^YWNf9ft%rlWLcsaXl +zWeoQQcPOrkF%fk+64hKyu^2qG4t@Ajg$iP)N71*?J}@@Ji9?z(;+Y>KX9s?{F5K@^ +zmY4yA=>hADCyCf|B|}Fl3Sr{4{Amsnq!{-vk@+<1H@n0tf<9WQuwEoQpkWLqu?M6% +zC@mxSJNh4yd{}4`MZ7C$B(s~VJDD2Zwr&D#3I63Ms6#2_ugJf`4CJW-VVcd-ri#Sa +z1W6|1fJe^r47M-BP+z0jQeCH*pFGT}RUsf=v9{o<0II@$B98g4%36sTz9MFUOu$p{ +z=B9aQ!f{&{U8Jz1ll&M<7)h9uR!rGSE;w+dI{RFcK3|1%>o&sKk2~T;xOL$|i_;BG +zgTC92VPVZF?aiFH{NS^dn<7maJp{b7e&D7)KEHJ~?G0k+Zq}>L`lvLdx>dWiWc9Jq +z|ByiEz}8MYO?j{s7g0H{$3GMOnUFE9EKLw~(vn@uo~=Y#(qRQHNV-E0;#nZ_A-nwF$k +zSFpNbaHxLvgnAQ7jpj+tY~`A#3GKVP$r=(_KR3=Mr)_3Uc0u`{j!TlDG_GL{VBO2T +zOf*@j+6iB*RAvKGeFLJ@QE6d!GBY!st+%*QKEI^$VV9vG4q!^ON#o4@Sn{_$$`V_< +z4jFMJsz+qsJ%_2Inizb7h!8a=AyP3!m`8-|j=zKie}#hi{U +z5FvM=JON_a6i@VAQL(}F^+Ua+o6v^%o=~Ws?Gq`dZiOH+%zud9#T&0Eomy`-9IaGhmwu+I&YNc +zSM7)tyiY9Oav7VhXMs7}dT@7P$7PAEWU$=u5hsbU(Ixz;$Ms#9km6=ioR>BH0!4au(K +zsPZF*>v>YLX6r-o^_OXZUeI8r3h0r=6cVWA!8Q(vWZOY +zf!lXR_Nvk77gTY0iVBa5ho{yaK1fR>+o^QaRmhL?{>Jz1{5Dpxeyk&&v1PB>}P +zYsx!j_{W4@D8m)BOKOL{m401dFB%^VW6=bHrFcezeRDLvh}Ks56@jy55DV&~r>Ch` +zOWD8qo6tSm>{iIAY`;ij^o5=pyC?*)N?HBuNS!IZOf*IFv2) +z|2-cF6oJE;7w!F>f*e?4Nr8MX$c{`-Xo`=F5mq)pIXKXT3mWRg)I3=*>qjTXepSly +ziA+v5iyslmZ&(-44w7^3^cz>fvq585rDqY>3=_e(ED&EEa^rFrOWpTNnv-r^A%J~O +z{+&^2HhqO8N@J+yDNSW?O+B+1fleg)VK^dD$+D)%T^^1l+TglNO_*wg7a`6%t+Z0RKwsGv=EMI`GUF5snvA;Kpe-!@#o%)_kZ1%53L3=h{&!Fb4%wCa^wemX +z^-_OWAOKLYB3+8k=bTdTkG8q>=iIpZ4doPVr5%g=Nm!2I;Bj>g}O--EU!H9 +z-^Z_9bd-)to(i38O%$=P*D1gQLegx#U_4<0s27hF-Ac=4{Hi#;}?e;KUV0 +z&FXrTL@T9rY +zqAz5?@?WWR^QOI7eK7mA85!c#R5d1BCVDy%(+YZq&t^jY_{X$EE$T$Iv@mt&nsbP( +zk88FOHi3wZ23NZoEj>8cgsI?SD~8zH%yGo^YlNkB0*~@1A{<^%fT+MzVKeMywCrH# +zgJH6_kGGd(?}WlVp^k%X3yNz`nNu5`?FtZxSyY%^(Qpx^&}&m#);8}Kg=HSV46g>W +z44D+_@Qi)0!|qH%&sw)NdlFlO7ikp;#Ed8wQrr~s0j7Vq#DK>$Ny+!jH^}ETqvIR< +zmtMM#`f#5iH4XmtJMgd*ub_D5hzG;z92?+L#r@6rc*%?EDokYY&V#~%;l!mWlDhiw +z?y-woo6VvkuJi=(Ebmt4^k(X4cQf!d!0Pcl^8^~a9{|)j@G^E))}a2Kop`6AYX5FK +z;rs?EAfD6yfOy$pX!se4yVRJoDL5*mE9~;={YaBeyif<};7!U@Sa}QHXlV&D2!%E) +zOc?TQaWmJh!!s9-y|cF)>v#)c7L<0Tz{fjXm}Bo6_vboihH2Q=iPUK6fj7Jqc`h7Y +zAnTSOIijLSlD^8=GAe#4MT0xum;S&`DBC6!n0?V*$C7t^G1hOMfI_(Rw6Om{dqflT +z>rMrFFB!#S%w1zBi0rP-Y_H@w&3D`(H~A#rJp|uHi(=Tl5}r5q?Cs2GXH4=yHAh79 +zscwOU#QX&!X52G!`L$Id;#CEIni$~IKu;^t$`AXjb^pW^M?(W95UQt!(1Wl8x!smZ +z<(Bnv?Z5cb5L-~-`@eQQ!8nQyy`X@A;7I?oFH6Hja_$;z;D2dGfH;@B*uxMy=ewa_!f?CfWy&Rdw`u +zK~k)k?x`tug+eivb$3o@XCdi@@!)P^Z=7Xe5EwDUrY)bu*bORirb7G3l|XuGPtnnm)DwI0MRx=|Am@heJ&P$2r*~`j>kw=_)L0ri6oPw4s6jzX4GXHV +zYFOGxnwVO(v*G}u#7BL?*8(MV>WRSD!u`thEl@R5q&^bVV-XJtX4XjSmp +zWlVWz?A0dwW{lVv?hWvU^wD!ifAqRcy;4HRQV)NJxV$R{ESnVJkx95fxbIoKMmm*3 +zG+1}#=$6$pYR_!~(ay&PO!Jg}dTIj8+1Suo+}^i2A#T;Bn6{eVSq@$Ki}1|dOWR(K +z$buWM|oh_q7?$Al?%Uia1PXXhdI1n_%lT*O5vXYzotNs1PSRjcs +z=SXrx^io+%u|&AxZdb$L +z{9R?rbb3(z3pB|%`W5(pf}4^k8AEZm0Z&2Tm!78+oXi +zEiRSJe>8z7Y2;MrQW<*n%~dXJ)#K-!`p3P?U_;mk18X^C8Lw@JCpQG7k)SC_W|Ym6 +z5U!$OY{FP4TeJs;C~V#toIO>pw{91<6(rA%b!j?hP&M5hmd>~H%GzS3|16vvc{oW= +zBgSH`i>8T~M*y?}C>#=8QH`z^OLlJ#Z|RrJX^+4@x@-0L9XBeTl2fMJ;6PIR+^O@l%%eIbxyNXA({+S1U&~cfuH<~{`H(_8$vubvwu~@D= +z*Fvw~YCglvazV}a5VX{VNX{>^#0=2ks`83p@l5H($ZZRj_%Pelv(qUNr>82=bJu!z +zo!eUa`!oc9#mu%%=2y44aT*V9iP(c1;T-HUhW7wEn<+*|b910}-B=*Lh!s^RQ+0<(nG($ +zVSG&8J2j7LZeKn(-zR(Lft-Bma9mo|g46F_`O?EHmfSzb{n)Q1k8Ao@(LtYQ5D_uV +zUSCdcJHUN7BfT%y8DeiCav1)JeE_x7kEW_9klfO7h8naIB#|}a70ZHC`H;Ko5OqYF +zN0_hN+$+D5IpHHWsdCyHyx9M0bP)r109-!bL#cY+UsEkRYw|H5ba@?vyQdJ9yby@b +z>YiZ2-DiX#_Tq1!>M*8++n@-Jh-!%g4?gPHb?Bv8S#QRA)$Se>++E?_UUwj?5_HG7 +z2f=l2`RnGW2usM%E!&<{=`*JnTSrAca0}c>r9bd$9AY9nEbl(`^d#!S;^)wFR7YfH3zKWmsg5vA4|vy7rfxu#k04%mb{fw +zk}%Q~>v^*2+vvSu{cK!=1z*AvLZFKf&ufTgCCStM1r$rkdP}%NC7K8h^I?MT$T5~c +zU2B|=kT0@}yULZPI@>N8@&LHxOpvu8wDU>y&1&z8kFD1{oo)Jeh+biqhkpaM)J +zkadJDJmhw!iw_;smpZ&!+MTSu_w7wr44m(Doy6|tjC|BQyCA;!u3wbw(cwVc3>YKB +z-ws4>t;{@U6P{bX2baE{o<(jEt{^^t-+A1;U1u|p>+)~7zd8QAAuenEz8hP>gjP)`SNXj5Pq8J +zyPjUae{;Z^Ok|V`75uGB{)*n+6VWfpIBnWMMIT8d7ycT-abO*gIU1;uBHl+vGXS6# +zQ@B~ab4Vi_8R=8KEvA%y3Gsjb^J}J|^W}NDs(~j19Qhk_u)#egwXcEE7gP4T?ctId +z9fh3i!X&=NZVQLf;5Qha=1w_awG+LUCDgGUuqdBTSWw~P@-EHFV1k@0hz4GQ9Hyk8 +z5WlPA1b&|)ZVk|F_pC+pqR+<0Hhn3;WH?4>h@) +z_&V?zcG^$*X)@PAl9R(wnP_EY^>Is#koc;H94<$!AGI6F;7qytE3g$RH+l%}U0rTK +zaY8*!P6BFgCK4bSVDZWaBZM+W7d#y4kR&H`s0c2Xl4HDhNH#rDu}c#8?RSqQUvS60 +zh)dNHtIJaWF|6^EV>F`;t-%7u4>nu>8^jpUMgO7**#BVN@4zw?e@@UH=j=@jxfHSB1x!f4!4;E*eDS +z2aYPrP&5OJXWL+-1VM={d5xOuJSd1y0%15S@FdD5R}Uia>~n#P)5X#C+ujjkkXJ7M +zWGayINlk()Zy9~K}A(qD-FN5%_$pXk5qwXOmm82q%1$GKkloZM4sEUGuHLd#2 +zE|G3XaEa|+)urN931jTQ0$61rkCf24)^h0}d7TEfBF`fvGR(K|x8NV_0PZdlnx|&Z +zFk>Es3A&SX(;H?o0Rh|P%VMUK<&~A_A4w36J6VHhER$yF$hcJIZaat`aI@nm@;)#^ +z^}ZbNg)&5aQmA;pwrjW(tF%05t+bnlT|*c`eYb2uj=U|-M?TT6DL)li_&Vdidc;k6 +zECOzp2ATd;Xb*5u;8ImB8o~{r!#RfO^e9Asz!p%6Xt2VzK+GiF7))D9_~Z{7u*B58AdFRm6T#?!PN_g43#}=-*ouWSzhc0D#l&N3 +z^%-F&nbu=~1wWTumNdh75 +z2ltc5`Wk9}ikR#ltHV(ka!5;RkjN4|L?{Y8OrqS|F%7dfO**(gZ7h#H5)Nd&p>{zvdCb>XW1PcseozFr$z=5+5;`xBf=dR3fuRKcc8+%VovzJiT%;gs=xC9Kj;IL?> +z-r4rzLRA63Y3WQq;X0`;WScuQl4el5n_OzZtdEtMKb0{S&KNGx1zK!K4ngj#BNBPK +z@F4U&U8uMuYct|>c~o-aXC>#|fNtIRh#a!l>HAN$t4+`|a<+``gm8|5jb>`D?7KqKZXfa3+GY}&%5eV9=3sphliJ3|7s63yd8!!j5K3bB+<0atp +zG5!qMN@WjF+!2>GOZ`Zg-J}tT^n^K%Vgx-zQF>RH)s%RYDpjQs)idx2FpVk06>5h! +z`cV;)uD)y>HBa*>86_VbY5K#S5$bL{dAQk+?A^~9kuor9rVbA(Y!;s>kHtvG6Fxk9 +zkWY|rw^S=(;LpQ}G4wuZWw84`ufAx>T^9iEv~5UhS#ZynhDfd#vo1aN8g*_59=Sc& +zrQ4;*tZE80u>z+_&6zHDLuv==Is~KYlvMm%XM0)HcA>JgCos{vQ-BzaJrL91853AT +zlAc|wu6Dd77bNJOgt=Ti$b17EJ`^?d7eUXeay8Z!gb*lAaWWWF9QJtTPb8(MeD1^Y$f#9`k%m&_eFR>$mMh-GlueIL +z{RtcC`kAZe1YydBv(^C7VJ(`St>bEyB#6pNn)-*c#gqz(vYNl0@DSDINH88pG{Bl0 +zFx$AZ%#MEFLXGJbU1iHvrC9asV!|{N1RfNF`HQ9iHaQE)YML-74a%@ +ziIHjWwt<=|o}2B($Z2Y@_7ACNmJIA@yJaG-tW-v5c$DgQ0NM<0Cr$IMmt4G|A8&ZN +z@d(Vz1*&$N<6a~@J=znT_%^w6EOnUDzM@hnF;G!R>Mk0lTE$L{WR$F)6ve0G^d#3? +zpNggri=?L0g}d3BL9U{_hgywfNJ;=E6;CxG2X>H2jH=P~md2m(s}O;0Gxs_mISG6m +z_79q&!?1<2OO98!hTq27=xI^kGP2TJ?S(0lzmyq*cl!$}-l}%NEqJ@VUi}LawNZGC +zEfuvR+72;uzxrT&fekn2YO=mUxqh=Y?n#2BDsiq*K#hH)9MnN(hG}x}LVFv#;3K6u +zm<0l12$2*%z>pw%j6{46|GvHQ(IZqR#z6KBMzELD`9-i?GxmUlM|KdwMLXnbpwBu!Jf$uH +zib`|{f}ZQgcttI +zH2Xza06)H|bAA}5N08f3hz7w6`{%DSqik@SMLJau!pnq$aUsl`m~SsvA=d2)?g95a +zO~Q)=Ynt1@UgTyh!_#AAAJl{I$Hr49#HSbv7x^IBvSV~7Gn +zh5edK9P3VK%R|RR-Bh58dO^|p^TY1zO+69B;GShe3E9GcEfDKpTG60rW$v-Q6aM&> +zd1$ZGbj*s49%#=F^DQBio}S>+h#LErnU{E=&_ +zQ$a|W$+Dor%VaW8tC0!Ii3Ssi9@n+|Px~pv`wB#mNrst~nf^g8e0i$(H?Q(oah%UL2t*hQ5n%G&3*U=g^S~p_EdXXbn +z4xN24hR>C#8y1o}V>&sYKRp*yzTB-?3M@wcMF3&qw9#<8bBFmeB2qoEjqRVwxCv`} +zxxdaBYk?WC{8AiVnPfCqNa$td18YKSdL&edxdfvG*hH4ZkGl#2)XMH?X77;>=+mRL +z<7kAo`~x_zcRrz|j0Qb$zPk__m_5%!+aNwl7KD|?3bI)aNN&Ydo#tPqMV69?(r|2b +z4k*v*lKk{a0~ZLRm^@d9W2kC73{5w;26OkJI^8Je=`KxIAiteXYVnJ&xBiux)E}Q^ +z%dlTn9RV|jw&e{D-cDts5XT0$vRP{$h2eOhJM3gijaY?TJOI6{r6)OF6t*Q%$kYN8 +z`5L(l#nGgRNzYh?jOT3o^97pF)IHa#($Q^Tql^hu?W5M7c%+KSZYsv$1g+Z+%ohOU +zylXhXh>3(qje#STzt0=F(3-|YJjhJ9j+v@p1FR){u#;AY_vAn&MjvDAU0f1>t%3)> +zK|ly_V&SNw-+yaPe&SR3sq<2ap7pr3E~SUmhL+{mPz)+q=6MN*K;wTMN*6CCXfX%t6LQzwM_lsEwJIqu9#oCG +zP^&^@V^nkV=6-w<5-XzpXNHj9z48Z}BO1-VdcA0Tlo2qu8ZFzPTQi52hLq2u9T#a> +zo-_G7N@qXTGT5XBincr=j`yb{gyfdsi$Q9ewlXy~PVDz{CaZ|MODv90l(MgBp6zl* +zbFIDw!)(*|LU@CEH$3(Dl&tzw`jGmm@=Sfdb +zy;dE@*Q{Vtm*pBK%!S0gxaG+V=?F>6kdYCGRmdJ2Yd>W?!4wUb-zVprzpy=G8SUh` +zEzcgX7=sdl!wE=Lp#_^{Ofh3Tllw5qI_{eR-ISkkEBe1UVWw>Cj5^vFZl(Em@PN&; +z_rbzklQF*)oxa>Y1of6nyA@;e*`NH7p2?UCx#vd?&Y25HOPX<5!+ZSe^#Fn!C$vFEN#X%`?(4v{(vE;;;f{U&tKc +zj?y%B`qUhuhQ-y+`js5zI>1GyX4gEk-`QuKr*?z4vghGh1Kd5~3*W`=HS=;jxN|;!2^QgOi&68k26mnAHQ`b^ +zw8x3^Oza<&;V8JxPdKMuft@&sF=p#*+DT*R?_M{6-k=bhz&B7TwHS4{$;$ehp2Ign +z%9}~bz&&zfMhH+mXN@aqPwo;SACE$QL>!Mz5cFmu2(wTTd27jfO2&+EFleiMomGm? +zdH6Sg`O< +z!6;kdym7`N_cLg}D=M4>;klIp23%-d7LyxnMkF`mW#i(cK&*OipXK?+OW^$Z`p}{P +z|FZG6p&blalm{&@O+hjN{R;ZEObFhaRl`eR{Zy#O5-QNFT)Q0a`tufA0;I}|j#nGW +zc`$dtNbY56ui9g!+a5APSkimkyLIWK>%_xx$e`+Im&a)@ZOEKffJ~dUXnFd9WV+)N +zU-|bBtn&pH9Gbx@zWeF?>`^Jt)N;L=(fwZO3A0ePsus+2x;TSdnTJcn!`S}e27HCt +zauR-Q-Q%rRgeO9u_Hj6>Cjq@pHhiZ6%w01K^6&KX)?n0gSaeR}kGOw#xPeC=Lydal +zZ<5M>VV851P$mVmj2@h8u$kM!7=f!n2*>$&_j|e`J{gI-AXBL9tc3mp;@^Ut8RuI+ +zU};N+toxzKjN{Nd=V|B;G{6hey5VAOvx$7zG!MzP;4fTcmUpH#w?x)p0oE`K+nDgqSSp92B}Vd8FVl<`iNj71dwI7De_7GwaL*!zOV)B6s7eERF1 +zjvxH5&+e1EX_wP({^8h>PHP%l;0o1s_kb5puATZRH5+aTjjOl6ws^pztQ?wijZ6j| +zU~qwiFW+k%_3y|8U01q+ly#!Fw7sU=J+i>7E4(Yv4FL!%oC+3MM@~4)34|Laz^m7+ +z42t|JsoZkbs^fe-fqzZUxJ)7pPX-Gn99TIVe5if{k&si0S)D|ZDXyi#TR^=B6fToR8R>Fx_?>BHx`Ov&8fbFiwnDwL +zqMY%%$IeRzHCPNjC~!-b&=~~YoI+S&G(25R?Paw58&4mWR$6?0-M}i_KUz?0xPm1c +z?MCgmZ?i!~cuP}1=(1ozYPi>+hD!flO>O%Xksy7G*z&s7|J7*bXpaJEQaAu7 +z*T?E+Mxn@}_&2+5vq=TKCNKgoShUa$H=6}eeT9ZKl0ItB*+rw$PjUa?M|AKPld**2 +z-OtK-J{M;m1RFxeG`*j1!a6dIU6Z`Hzfjphb1*TCeAH!zYB940+C`vlc8;R|w`Uhi +zK$uzb+Pl#>*zcR|1LupeWs8rM0Id5nLq=9x)ly45DEfTV;RPE= +zjQidj$WZQBf_7-~&S$T^Ow~AXRe|vPXzMKo(KLXf#%}8%_U+)Lw7ub=WlpKcj2i(A +zckY>?u~EuFk|s$2oa8}x!~8IpWC2tF>jXs_hFd?v@a=*~sxg7)@m4NHcsfPug$()# +zg-w3=fY!Tu5p;Og<$e_e!A4#2?`>>^{TRrg4IHHoeA(5Q(zth#BNv(RDY +zx;|raEx`a3xP5M0pOWiIy?0K?@ +zU<8VaFPJxGmuJ^*9|nexAId$ihE?Qt%-wU((Z;OXM-4&$-Fm`!xfXwOE6JUcBW4Q` +ztxBSs4SjbzUe}89{~_$1qC^Rrh0E^Kwr$(CZQHhOTc>T?wr$(C?KyYWJj{3J-udgH +zR=rfcR8~f1WW?TjOl?OhHdY^5K50{>`xeS0MiY8~sxq*CmYLE;9jFyowl}7`1Eofw +zv?^QZu@^rmQK+Hvn|M~KFoY-_4XED44Vfi~8RW$m>xgtj5^Q?#lgsXsTcLV<&3nH)n)b`h)OQRYqU8^F +zwB^~_M{Mg@^0uB6y8+L+AME_nsv`4B(F%U`uuIjc(-*kRbs`oCiG`DDD31|G5k{f* +z>LZaz`_oiqMIb|BmUqtCtI_69v8Kyzau#h|l$7W1AF=7I+6C4^WIcrg+pVj4sA1|{ +zbI=hH^WIo!8v~kZV`54yfiGe9t26nhm8rpi3fbLLx{!oCNi~e&8li;{P_~4+!n(e` +z-U}C}W49)f)kBJuCwr<6rF!de;&=sXH#eVj%sLAkn9e3qyW#)z8kI>{74>ibDTUY{ +z`TX0Bmb=7sw0*n1gWdD(|2g_QZhW(srOH68LK{Z1)k~eF*{}886FPhWlet>tkE`yz +zG>K~jHHc98sXEa!oNhNRm_qWUV5B6eOnr*H6H+g2slqKxbmD7XK3Ek8$ApIGIW=6| +zO_;$OgW<+=%8l3`SxmTssa~|^zgpyMwVR{%By4c8E0VtwF(XRJ8;CSn3v_4EwmH1n +z(b!ss+fa~2=fVWZOVbF++o2IwOoDHh__*@TPcjp^!F%f`(&__uZw +zyytv4C&RcIO=Rv?b5Y2h))hQ84I6XDmHaH56v_x89Kb%FV?sq&#yEyb4ZwwiD$Sc) +zr|9;m`XZx2W)bNYxCXQiO-bE~J=OUmC2?IkYq2P+i7ACMoQjUxx9F?B&tQ}%S4V;Iu5FK| +zYa-{hM=@i`(bC122MVw`UZx>z5@n5!>u9WQ$94A~W#|G0=Liet&p&2~ZEcvNz)ln= +z3`GEHLTqXq=+@ymDvKH)8sb=9K$D-o7pvAw6uzQGwx0mGBPLOUjhJu~k|Dr&uYH14 +zMmT@AA#_XU>Kzk8O_H=>BvMrgVx4I{T`_a%8bXr8{Vl!r%Aqcgaa4h$PrKYoK~rsb +zg`_DxuCPj3U1)}nyMBQ|d6h>I6f-45(>`K&+EdWD@6q~4lW3j8J5JZWO_Q`bFDju5 +z13v1mfXX+#@Jd8#<7sKqUWb+EO8uoVxTCA?03vCHpfjkr72I)aS|3J%K>V8El}q1W +z?W|YrC)%*{py-@I_>?gBSR77eftqaPzW|0b17#pP%{{|~clC8JPA8I70to|ziGao{ +zcd_K__wpE4Od|EsCxi*o)-v@dg-y=@&p8Yt2CUgWNTL>d{MpeVgTGtKtvH~Rn);1d +zplg$DuB8^%@{^~anR%P4bi_e?8g0j$bt7JS?!~zW^Nrl8D)(vAu~Mv>$Fy+TS{;7j +z{Js`*e;=~lG^WLaHeV0Rxjdn<=6Rw@b7ZC1Y%yJ)2KJPykE5Z4Dwwhb15TW&U5viH +zdbT=Stu@i@0#IwEX%aTUw*uPOMAkA1FI=>>-<(m*&4g8VFGY(1^!OO>Ef|3?Ma_ju +zd4sHsB^b(t`Ck*zZxO-{LRmSU5m?OTci6qYen~&fc7=Oes(cbY&zf(d;?@K1+q)_G +z!s<6}_Gp}o#c2O-U|ekS-)Ad5RE^*ac&N&6%UZm#SPw?`F{DaehDH0jtWVO?=%u@G +zB(1Qosq`9EO0X7T@ss16L#yYzQemM24SzZj?(NvTl +zeE*2%S~YC*Dd3)*f?6!91-ty|7?W4K7-IO%`^6x3QC{eJcwfwq7HOzipw;k)RcdP4sbi93A-kB`r>h$`r +zomN}`+U4RLblhf%-ng6kCp2^j5HWI->!< +z3f{RQ6K~IPJ_=1AdiM6ugZrH60jUbtTp`W5O1=lR+Vf>MQ2AZs0TbORD%^3sd=-Mw +zo=du}mlintn=|wDD+!lRzPvIYKo1o$PCJ@?GImJP)@PPcO81rHA6ggM}~*?cg!pMfswGOdB<{CO$_{Wd`_Vd +z?vCb3T^XHiw!?F+)^S*KCOf<^nT&zBcw9|c58(q&_PrpYoQCOmE!hb7Z_u>*JFihu +z`{Vj8kET!eTM2hgP!W|wE+x6vJx|0WmqCA>fz`^K2|akgXpRaZj{x|!LN|uP*JEJk +zeQIfC;})c&#c~{PVw^D0YHK*5#Hawir0(Ju<}LcQHzye$C5xha_l#Erbs4Ve4p=-U +zSNm~lf@!W()SVd89Bt;-jHo}yoMKm{@VzVqd~?Ojo#M$?D(%UZg`*iEgS!+)2pjq| +z-hxv3FL`BFFiY0+pQfGw3ZeyQZ0M;y$1s`a#4MAMQK*TMQW41wV>!($$gDj_|JtI2 +zqZC>;<(;n@D)u*<`+;;`XSEKXHUfb%jB>Z;O$Lk()D8hH(Y-?lZy(dn-C3x2mSqr2 +z7K0hc;EFRUAKWQgBS4BA3(^3EPc#g(m6ub(dMHbgrUjM0a8C4afZ5Ulo=HU+d2h^J +zapYu#Iu5c&xo)ANE$W`A)xH^#URl2Cq~xST9~4v!B*@4?^QGzeBlPMEv%?y4vf&rG +zdCW_q9<}|;5Wn|()s>Ivx0BfL?Ov_D>0vK*kf(zXNk7~ulNJ+85*!^0iI&8n&fo0E +zc_M$dBU;&}J@XRT5&o?N +zJQrX$P&+{SS}Yk3<{~`GW +zG2^}-t+=lA#Q0P>wqSS$n9sByJ}iHWE+Yb3Ikb$MuMr*S^Y-ZD!e^pg5vA2x(YM(H +zbd@dLen~Q%r^Rkdo4zNi)5}w?cJa`#tqqCcov|=hSEK#9=Le+50-|O(PB-Z5txEi- +z1Ib0Vw!}MrwS`ENao0dJP^A(&dUCUfYBfZLRF$D`XSMca9=I#~uQATW%$th=&U7Ok +zVc3FR@Al5urkd97*Xo_S^QT`g@5M8i-Of%m*Jjh%dTzx}4J&?Os40xhUu=J9?5OVc +zf?II`G`!PiC-L_I6;E{!;_@B(lL~}Zz>iijvu8sZJ*9&%q +zcd5`FFEELq7;6yQt=HZ1`aty$jZH7kW5hhV-43ZbtiPA+&Eoh~KZ?(Ht0shO^oVVPXkD%YHKRnNVG67B10HGeNsPGEeDuDOBrE{+V +z8fRXO@bQGm|bA6?OOg*SCEh#VMc@m^}4(+2)hIcf75$%|9a>}7f#ErU36Hthvf +z-KzbzOaJ`Nw`#)5%cSrsB3qQlb%7$-8-Z)O +z@+u;v=`^5r4QsL%0$*H4qY~Dm>n&2t+JD!W4(~JBW^;}?45()dsXMZ2{gnpJ?Dig+ +z8-7xy<>#hd0uqaIuFcztK+Qp|E?(a@x}M3_pC_wO?ucQBLcVjr?pl}|jV +z{i_$jD+*{WT(YxQQHnMyy!Kot%>!Bl5oxu84B#skptk|gnrfzUiB|>vGNzUbQ#dVw +zkG3t-G+#039amIJX8K^krp8%EckAoNQLm>~*Mw~VPnRdNk#QbOc2XFke!g5}#8td2 +zJp?!+N+%gL!#Gzh``~iNwmh&AySq2^Blm3Vo~2RD|70? +z<>(sVJ>fRls!STlfT6K4Ma1Z(o2az7tVjHS*g;dlZm`k@q|>r`aoJ}ZLWS|#Ej~wf +z&qj?QF`4w;>F6+H3D2IiBKTViDr%=+9!;u&6Pd(P5=)eAHlq(=E<$vpQ!kVyOH{{m +zwJatYj+)Bveg)@f=6iy!b7ddIV@8-CQy-#s5(TDrDdVac^t``gZdT!RiN-n*_7`0S +z@U0~16Vo*5dMeLBN+_b}w5#4V3U~xNxpi(gs+RT!$85AZvUd|DrmfJHm-) +z&Q2rK$zXGliwH}$H7^J_5pt4p6r~wA?y4z8_Q>aNjx1Lazy4i_+iFyT`WeHffxP<6 +z3M#{(RJpQ!@a04cM&Ru)LL}BDV?bK^_XE)=&&^(f+Pgrm>QQoijE|8BE+vdfiie^w +zD#-#65bo}>@({NFloRWGHucXW{N2tQ<-mW*y@H`}%oLHYwNZOXjAe`IF^a#a`oZKO +z9=BV_l&=GZcAGOLwi$VQz!T6266MewyJq1WC{eBPP1UN8@goGHD(!kR>XSAaSxp8z +z(TWtt-`fVk{)Yu-V(HojkL353P-`Dg!|3o`nq=p3>|vsu2s*U_hb-;XoZmtx$ZQup +zEj!iuz=AO+?!SNZ0-q^)^#354!nMvwe6dzh(4Qd@&_5WryMy@l +zRaTTwJ&R`Oj$TVu0_J~D?b9j{pQIxpWw|Iq^ML0CLFOo=At%$+K^HA +zQiNuex+`$tI9*ikoLhrSB`5ox2U$TYChgZ&GfhHl$9-$1I7LdG=M>T@kYai6?i%;# +z$TZYp$V-tw-I&LPeJ<;fk>31he$cSJ^YCcS;@S&&*M=`?Dfg4P@s>CsWHirA1R|bp +zpcxc?Wjsffo+IcMNTl%E?!4;uYi$p5jykMz2~Oe7%>c7ZGg#bmCa1lQZp9-rm= +z&bYYuCn+!Dv-u96TU0avdBFPp{wwzU8*~2-@em1#|NG+qIU)a#v4@FHMHvDBNL=kh +zO;=q;-NhXW01)`)ALSYt`QLs28OhiUe%0arVWGlE{D6&oD+DxIieIw1w{CgD+S@_C~2q8+{7P9l+r +zk2|rHB6L;0Aw&n)G0}xOccXG>zT@S7tuS%{+)O@s$Z9L +z8Xm8g=$=~sc!!tK$yU3xRI@J1wVAVm8IbjIW0z^}zmvjj`i+o4!S}S~bRV&~oz=>O +z;dGDHV|I)UY`xYdca5onC^1{j_AyCd3aZ-WvCH^s;+>1C{>UCZhoHK$4&OYIxOX@- +z`IkAgaHpK>To<14oTJa$sZX9AkG52OZS^xj@@HMN2I`~4!V+74{E_anaq>sr?$o>CRfCD8BqO&2mM&Cj +zHA^|w?aA$U{r8u+&N!k&$z6f%VJ24`iyC*O8v9-xf9D+cqRERhKff&yNLxN+)C~K5 +zOCU{-dr=(%47gU4r`jP68z}iXy0*p#Y`&e8{r8`BRD)oRQ}9cTNg7z7U4>wHo^<=O +zx{S_2A&oe2?jK=$cbKtio2IbfdWcy5G=WUy;b>@(xhEKcB=sCupu#hl`)?TUT2J21 +z^#n>Nu-5%6~^PzvC +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 +zh)k- +z-m#Pyx=HfK?>u)FFPy^&e-w?z8 +zKK9Dm&zB6yz={Tb<{+k5u!#qhz`RaEiwsE$w$!B+`&hE?_8By1b=`m2zP?#%w~flT9<2g27JHQpz` +zE?+OLn+jh6M}0C%XK44CQSP4QoK?ch0H7M5FnDJT;)c}0Tj2xgcZuvu;~MdFRoCi*kN2u2F&YxnxBnUS?p5RxFno3(;) +z4C8XirqC5?Ri-hJR3rc6Qm4k$Bk6R&pZpOB#Bw^|=5%x&%RRm9X$vt-yMl5ho${wKiQ?}^yM0RmN_$<05G!w<&9B&#D06;Fz@M(iOy|lVe}m^nP6=0S6QZb +zI5B8u>G(0)0nF+Gm1g}US|nWHDH=#C@NbeZqmH4Va^ZCp2yW^=h*;hA1;~NrZ8$xZ*J^kHO1FE5v5ViB)+5wAx +z2Nxn#H*SPH%1^W5{wn38CXb17{7?r1Fy+xz23SH+6f6m}+61<>iLp3BKs;4m*SuhG +z+*>zELF-VZO~1xMsY(rUb?#k=Jfw-%_Rs=~;=6uN>@9d&euo^y130jJvia*PJf7GE +z&wf|tv43}0`fIm)qY;1DS`VHsI2e|X)hz#+Ntlfsgns(jQMdcc{i2|~MC*(PT*d+# +zki1`+R=m1~IV%ppKWo&~N0BnX`Vu_qv22u5PZJ>Q*nqOri}m@rJUWn +zzP8l*Z4c1llAkj|R38uIaDy2(F!{3aHc99#Z&IbLvkq?Zo4W)AT#k@FuDsF=0XXPj +ze2-)-RpRf>QNG}`%s`~LsNdcwH-c2!fl!RtB{c>q)lv3&Lw@ogd2m1}0B$z~>nxiPTTF$7DT$mV08_BSm5T2XNd!t^%fkDQ{^D +zKi|P2+U`s^ifu?WI5F(_uuY=OlIEe?PgDQ?I?ZVPJn5jm;Vef~ck>>-!KHtssoibC +zt-frn9*t{xK4Y2ey5fAfX?1lmy^8NXOfj9|b$5VT;uH@{e^oGdo$E|}MKv7z1NNmz +z>0meEyhL;_y<3YyQ0*53yBx8Z+a<3Uh}di*1gv= +zpik=e?w~r3war_`(=RBBER2wo{Wt7eK=N5WLQxJEIE4A-lWv^ad^ItLR?X2 +z8I2py+beOR7Nj%4W8t}s?IoQ{z3LQ|gZ!W=MfLqocR#qCMii8ODGs`wx0XBOEJ`KUSiH{^&iqXDK*N+idsg| +zPDUfFSZQYI#z5|2c~Or$oO>~+OR3vDY|6KshyXP|G%PJWG;bCK6AJ>T=FZ^GMZ@N= +zs2xP$Ebk#X?nTo? +z;j+OSYv_9m(9c1^OuFf8eGm~mP`~%BQ<~0={al&1toiac1bPY#;8`gsq+?xwASi54 +z6k6s%Z84zOjtVpj`lgIVaWl&XTyf}Wi2ypl+Zl;QGjRnbx3I$vgubi>3wzQ1P#;SV +zuN#lLTv)I#kOAxTik0eoWgxAF)1dl%0Cab<*g9rM6>QBU7H6YSYrTOAgS;Xq(&>`- +zV-TKh(0Psk`h4~)0~oLH+!Yn3^UZF%f^x2;suNen%;VWVKXvz(JA*z}bD{HT@F;1(r>p`vks>!;m?NF*bM#dCNK`Jrqe;PfK|Xzpf`W +z?RIbq6ygeoPuyUfA1<*t|lU3*4P49D&m9l7~c#R>_fhgib;sbBMWw +zkHWwo083!@Sk+EEUwb@nU=->`I0W$h5rqY|Eimh*xIE&CSD_)qJR%qHn!7rEZ!c$H +z3{52%gh;v`(RsmDFLmM`5UWIo-B_2gPXibCaQy;EwhVl|cWA#n${OQ7N97ceI2lR)gA&z&v|IC&A{Z*4tMoJfBxSmUV&mb8YKsm+>W;eDxMA(I3+P~@ +zy>xJ;ts?fm2$GH~V)DE}34-jZpL}y2r-KXE&02W|e7)@oIkv3P&eBH3ZqO^MfkM1% +zmZUBDM3U4HXjv)2>%xG@B4va1oun0YlHPYbA;<`Jj36M!dv +zp>&TXNsKcTBxVnu?)sRxoh2YH+$)?r-Ql +zmi0%{n#HuzF06DTUbeA@*oYAh@i@uVr0-fdKL3PkbJe>V+C-^Gp=`sq{}8RnR%9z< +zkuI-*KmWI#m=f5-1gC!ztclY94Q0keXJTXF{NG#}|I<6KP+8|b6-qmI1)s^zd*ja!Jln@^dA#M&n*Frk}^HO~Q<)aLgSHq5b7zFF==Hjxg +zs`~B)EUl(SNFIgt_gN0%@ +z((^kP?AQDLeEb?}pQd_-@q+?=(xO<~MI38-ciQ~W3U}^^Mt6_4@Wx6)8sT?l +z4zojt4)MwRHYMV9k2sVr%{Y?B$%l&3lgcXTM(2blccAB?vE_S?pSGz3=lMjtnFjBN +zMsE>ja({0P#-CA2y2NrzrfD^^WvK!bFiNptIKmF9JK&B;zAM34?xg!&*92U6{b-$5`W*!sYR@y#6|IVN5r*w4Pb}d%(dmSOeo(ttLwD!5!AZA6x3Jxq +z-7XwlpWoJfhNe#upcq_XYkxi}R}m9$hPKGlb=#hp&~r!`{T +zqbTGxk2_AV0Hbq5NJ6^b_juXK^tVj-1e7MA>qM7?;zK5Y4<^6)6#BQKUVi4R4YHr2_CNhd-5hmo>G0;*bv+_U-U_66-7~2ky0K&h&>7gn8vAL)$CVdW5@5>! +zklf&D5xI#}wK);z)Vd=`@y$)-ixL6I!j6IU04a_XSq%{tO!SxyIMXQd*~l6VR%k(Y +zCHM$egV0G;ZP8@oObr8q4Up*-os3qDD-0O30s(g00$`uezW#JEpbD?)JiZp6pD&vd +zIn^gnzdj+vN!?mCh?4AeZW@+*uZPf65ad(~GGX*m`o@ryVS_+06AKW~Dye_i>-DQkf3eLQ9rNTV +zGHVK(j*1ery(gLv4RVwP^QGQ>GA~tww0w**THJ$dls`TdnkDvk#;PC~MD0)~PP#)p +zAkdLA#e3QQ96*7M6<4vOG!tWYz%r9@A}SF7>y;+HEhDdGT+O;O%Jc5d~cxS&PEKjGFRHJYzjBq0{&;6VB?@m)W6`|iL#hE>?} +znvi+nSsV6-&0(bd`=@{$9NDmr{gj;Oiu)bFpnp_RW@`q~Js12F_QbfAFq{oMOd=1% +zv#%hOXUybJz{mh=-cXv{M`;RAfH=A+)E6@IO+DIzBCb&zgR{Dkv$BHjL8sb0)wuZF +z=)!OfjAt*x6V-m|YUtN3=@B6T7RC)f)5IQv@bde_MIEH{5*9LOf3&lKgOS~yI+LR_ +zPA&ycxN`&afY4p^NLjXl+L|?99Zw}WzxejDJ;O7n;_blpRnyf;+GrD|F8CJU=DO23 +zkz`I?&+eFE+G9f^NmQ)B2d)2J^d@Nk@{|l35`UF=QB)%Ii2b9#kcv_XH3j7T2#Yr5 +zTXM>z`zi~{EVOrjXraJ8Fkt61m_^uBjgo8lZDbQ%o?RvCMw(@KFitW|YeY@40QX6b +ze(Oi-?Cub@g$6olT-q+Y>MVRtq>p4^AY`rhl$9D1uX73ekcM_%+$B1bG&r4db2z-X +z+krU$Ys9cw`wM{3%}DxFcBvG86{Obj5#7|PNfv5EcDW}xzQEAI--^+`=0+&|LDl;? +zEOK70k-HAuL4SXGf*yMhm8c+VBNrK$#&R6_m?8P0`#J{d9_th4J$cwUgAXBM{UD4z +z80~7fDDy(9kP^|L?N@sBtg|C2r4D>aV58weeyU@WvCA^OHaOG6@Cm>IFU=92MKM(?SdOA=J$N8`CwP8_p*S7;7%Z +ziX=ij37#f`n)_jai}#~f;_BNcOB)Mxkt09WnUrU>%s&ogkx%< +z4ZI?2b00elqK;@G$m$57NNw>2Wt0#1Oe0|13n`hvG`Y{Rne+( +zV~2Px%p!S#yM$rJRw=(N0vL0!WeR$kWlxG%W7dRZM6rcaL?WF|4d1V2c(-?~e3Y~* +zH%TEGr_6KMzI_x1DGe}>Cb;I~!A!9wO%op)LXxSf&8l@>!vshxV?(dEps1=068%ao +zL%nG8%LUnC7lKy);KSR+oK3kEl oOmMl_1ASDTDHpw +zKvR_jVNC!>#Dfe-o2>ib5jY5>8-I+<3k@IRoZ-NQaiF&3IS*DF`bej1V%Caf(vIq- +z%uvMG`0j_|_c~`m!+mXLDCza}Rq!lI?yS~Cv!d%@B?0bW;Lq8mxMrp#I +zr6nsT+C{hPWkyFYmfCCFHGF1vzw_ATQCuve5domR4Fm3PifgeCQ-`tlD4^wP?T-{15!s{GE-G&We_ +z{>0LY&YXe?R=KsEAETL~_p(0TjD7N%AI$g|bMdALn%5wOay%A@iG( +zhz}b;o$C$QcGRc4Cf&GKy4j&|mZL34&l$$n@X+Sco|OkL{#Bkqa`#i0xrvgkSRM%H +zbwabU-;EN0c!_ub5pG#>9yW73b)f36)`A&BD#>oz%E5TWr-v?iPiohOx5sGNurZ5) +zaG1|gp4e5rQlppfnpu{%35CGM5*Gx#B@>{zAWeR+Qk@HaRFhbV|BJDAijpPV!Y#|T +zZQFM3@-Ex9ZQHhO+pgMW+qUhld*4ovKI3$Z81a&CnfWW$n)6$^7O$7khBtP;Wc_PD +zC;v#GfluflQBF!u;bj|3<|k%1yQqUZSa}}J1#j8J2GNx)>+D^)QWng4yS}Ii@N(J^ +zcIh>BZ+L(v^J)UOnzAK{j4%3q%*Hewl3~Me0|-OSO53N9`Sn$lgxEhCjuk|$@`|jp +z+Dgh3vbz+W%oN=UhS=pg>NSgJ&ea59sx>aWP9}0zmSPDPUtAt)(#scZaTyet>vN;X +zj{yxjih>vUy9hz7eNa(z;))FIQUqOAMhgWjl6dQfLp53(F-*&0(Ie!w!fux$mO;WV +z!@@$z+560wiRM+TXI@i8Th?~(5b`^cFj;)!!qstoM)mALh=m?^{-E9%JZ$k32MON1uVjmH8=imiux>j6nsBq$ +zvLcE4+{xhWR3oR1Sv9|| +zb(4iQzWFh)jd?154g^<})_7_CVrNJDz2A?Q +z{k!zKxK=-!wp|~-v7+*&o(@=V1p&YW*VOqb^Ye}p-Q=qge8@!NHOqR3|J*A9V{+B7 +z8(#tWTVJ}k7TTij8fw$qz6j6$RNi}?*dGd4E+TC-2{Nv)ubaJmuA#k7 +z132jGahKcXZiwwlkc9bESmHsBc=56CKr>5B&Zv(wKfKH0TCe^36>2JI$NvjDS~PsV +zRR#a@hGc_qFwh8Cu*4)cGIm75(g`rXukRg3*~Vqt8GL>|1eNsn|{sq +z?xcJ9BcdRFiQU6paP04#-@1ubWcslv==#*w&fYvXFI51U$I4LE-t@=6t+4|U?ndz{ +znpym{TM}jBg97ynr8Ihi1$5nfsjr(jHMX^+tD}8^J`p#zGN$}PqxXcpZ8UP8LwJbn +z?UP3PHqr{#s*xn;o>uxENCO;Iy@C$4SxQPSfM!z-eWChoLqPF++1xZL*7l3=x`~!X +z9nHWTjOr5!qaNxab?*r)rU1FB={mKj{cL;QbBG;ZMO1J|EfIu%gSWGxhO*}Ngtcz` +z_0r-}7tzYk7qfGA7#;CEuM!K-&R#07ju3;5c6xn>B37lSlkLDKjKe9=3mPb=RK3;;~0ZTTCcuJ|_$Ej#P`enT}D@ADlc +zu|(7NjS`Dt9s(>Vy<%OI-HIg#HmvFue0Zo$t~3LP +zuHr6#%k63ZqLA3vHORv)bE$5}RRb3nw>xY|1>EgLpbHy_BttvF&LvNY+<6I&{=wkA +zZZSR{V@+_%npO}armRbR+(==Z{-G)2;;@W_8$+-dY$q@l8z*E@a*#3WXc`Z80UAqK +zzQuB!>l%0kQ%I#EG`;O?@faV7Xk@!@n@W +z))nY+T{2^(I!ZOdTSj~nd&-_Bf`Cc2JXQJNSBvD~Kj*~Ce?VUnmHwcQT2Y17p0eBu~Pi`)wiF^cba +z#rH2dGD*-)U|^`{8wnJ~F<-tlhlxO|nf;!g)kSofOh$ILNu=PX07#RAV&%kX6TcJ= +zn5QUB2zh>dtL3>VCp+MKLESymk{i9884s#hmjdp1vHdDnN9D{Uy#*fL@Oq~pgv*@dI=2{qHKKLegz!2 +z5>&CPaG|8mJbyRBiNwf@Ne?b|WP;XXV*%J-G;DTSXv<%+w6t1cmuh+JSF$8$wqjat +z78z?F`Gu;4BOe%4YM{oCJ%r1kv*{nc7yh`j@>e7)>npV2zG7vf!m9kNCQrf-a?-y7 +z%0A_8Np52k#hMUOO`CA^ +zhMZ5E+OdO{c54K2au!v^d=(bEBi!WdOj1p5zSFL^7K;_bsc6*-cyq7pSBSdKU8bye +z{3r(2)~FXhsBn@dEiJD;O7*6FRi3s7b?yh2k<2n5z?jEys{72Ci*7r(KH!7K%XHx# +z^j2O-2P-?mC;*dv#7&W1qeFSGDi-#p1Aga|ZBzOvZ!3J^oJj2!QdLsyH(5w{qkC|C +zUccgwV6eqnHZ8mkdFsV(+y_grzRQP_%=;wmWM%d +zJ<6elc{IV?mKD|qyI`%^RG^&!P~EPmZ?bMdV+)4Rkzy;RUbe3nryFwQp>YG+BdViS@-CB(}!HTd&>Hv&Dc({U(0 +zMvSNcn&Q$&=y-qz(9B`0-)3TCy_;gxB4Y795)aBUEEChd4$l2c_D!#XvVBM02p_|R +z7mO@f0DWY?Di6gESM#(Dn$w@cgFJ^3!7gnI5!RT%Y8B9%5{(%i;CWEx$DX)u +zM)0zQcV`D((mbO#n^&K(sqy4Y3ht0HG$6FvLPiraX#HB?33t0ilU*KGR4ayeRrIVo +z#k+i=dpYZC>!1zI5rKbA{6_t*A;yW-U;sl>83XM2K`t)^@LZUjZy3WYBdLua$TZn2 +z+&+bFd>||UhJGyab&UE;5^0P7JuK@q7tAzl=uQL3>&iT#!3ItOAtQ!(&3F*~BT{hMX|IC%>IRaN_r +zch;WZ(KxeXW +zOIcy+$Yl!!CwGP$a)n9K1wfZscO`AO_o(YD_3wwb$+~Su^w&!Eh=x)JF12cIO7+sq +z(?d+xU`bL*%vaXGfiC~^8lgyC-`d%IskFXadcH+)FL+BGSAC=2)v1lfOu|I9tuocj +zJ=H3;WB2kJE3FBS^2pdOmv8U>A-eWiHgWtwvRp4+&!wvSj3^mlvJ=N0DM!;zQyUxf +z%8scwj!GX#9k^Wjy&lzi~%Fj)2|dLLR#T84v-#&k)=K6BeAO +z^s%m^C7+ohPFneWrZ1eq8jfSugBgcRpZ4aMUvh@G63vG-3zow>U@UMK>U5>GiXuMn +z2L95Bkc4iH#`AWkr6Qg24Q@s5c7FRsp73E9`Bs$3^0x8fuMrRT_LfO#=0zs=)5nr) +z_?g=S3fi=TE19stwl(+R?!uJUUP!|Xh(v-Pe}HouMeqh<6H=AinX0k;l8p@pz3vPQ +zrWY^?x|%wqn7L4+Ye^W87&7F*Q~QE!>cRr<#a|Dz(zm0-G_b3A{S{w{(F<7VmA75r +zMQfP&8o9;MFqTpX$9``=t0w27P2em1=uD!5D8*R@k`4ZzYV2whQ8p_|yR!{dx+aTp +zkgX3)x%`GT<;Pi_2PkY$wSj(1F&Z}<-%4Pkc(y}b7Hxe0@Vu^O>qd7S|gyKm#2%nv+ +zW=i)dIlbhilh5_j%NlQFplE5S{#Qsjk;UzrfU?+$NfdSo`Z-W$Lx0dV1arir~6W7-Q7<5|#pQXDA*~A@YV*V(Se?X0K4EdU6mj +z-hr)8?;XNc+(u!2#!<>)04A^rwn+7#>uCb(f|!}OjYrKtx0HD#Ne*MQ#o>VRCPQi^ +zwAE=oYrGVYn1Jdq8+j*q;5%?;4cO%}g4v)!PW%5lLo~o|h%Y8JredQ(12sa?-N=jQ +zhpM#_geQ%@i=49JR~r&F-ISqo94`);kzt2oyQ{DFNZk!a)Zy9-+MWCuy)CF{;vY^Y +zt#VOQ#|XF6wnRsbgpr0!4?&*ckOe>C1}8(n*ZZmp>HXM~zR0YV&XGHDq84!z0oz_Fmyyp^@fw`1eolkJ;=A^InSJ9 +zk&irG2keV=e}VmHi-_J*)!x?c6z%X!d;Yh7nU$@+(f@1_*@~Zl3t&JHdHIT1^Do&0 +z{=4~aIoQ9LaSCKTs7P*JrB@eEWLt7yveV!?%jH@N!jOX0&@QObRcLSqO7XY-Lnq2W +zuDuqUP^c0IXzBv1p-X9fV7r{GzaVXb)Ph+RS$QeaL%*trTxCil1Dh0okeVdk4CZn9 +ztIS@M(573ijb(u{$Og~)v@UlZD&Eu6l>nq7(E%{ZSi}VL0X8*2$l2de2XI%0n_6u| +zC&`g%~EoCsL9o}D^1J@DJaYZj9HQx +zLCV%p>!vUhw}8$Nf>s4mlE!d01b6WL!=dOgpt~9h=zxMt4Be*5gAo69+}}LpV^i5BaFX7@dq=frh(@@Xb$E?F$;y;^U|Gdb1iVj +zJ$}#5zv$xj7D8J`J4(zHJXN(Yk{X3J#hxWPVvHuc1`S{zV~@UB^TS~L^^;OysYI>} +z|G>$EU1AQjC#bxtB +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 +zlMvxRZDM2m&$Y?9w&@bu7iY!l +zz;?fl?6f!1kiaByXC!}@WA-5_3#JOn +zoK096J5k*US-md~$-3LUGTH5onH32{A555Nu@O80*}=YWzTpx3`6)R(&@gGHEO_P) +zad)Y)edISKs1;kOBmn1Yg49cgs}L?YYOBsIv9@p!c3#7twj$Mw06cTpmzp!-WN`~p?G;U;4%QBd^W4!370gg^p;4L>E6Uk#66^eP7Fs=0lFH>G(2(D| +zmpy@c?8r6G`!&68Q$*KPCX7rXD`&&hRr**i-@?C2Uvt@FppXq-ib6WE*4hnsl9AuJiFYnv +z?Dl8vuihtGUWtzz!(()uQEh(-XR7J<@54NP7PsSzX-E*vbI#jks0NkNQWEny9U&%x;lyd+^t9T=uUun0fvC8pS?jL|F0*6peZ2 +znb-te3ZCgoe25gTMoEcpj|>C`DO2d5*#CL%g{pIuw*FOXj{L$V|LxrCVEq5%MNqSv +z@2^-B`FkgK|Ck^SW+DXn@@hSgZnl6?F7GNNe=xFOntFi=*D{_oun5aE*T5RinOj-DS(uU_I9)dA}_p`cCpOChf#?4P)u-s>1N2Sm*xFkJoKLO&+ePwHTZ?E;SV +z4YW8D8EL8jnJCBEs{&;lx7Ks_3~t3F)juNlV9EOGr}&K5Hi!bH;Yb#)mNgvPZS}Uvps= +zK5A8z9q3rh9FHTRwkOPYPPp{oa8I7PD7XYA$PB8UkOCC~(9IpYGj7RS2>x6+nB4UW +z1}@p)lL{u=1{D$lVN>QPojDT@)$;un)WQ*nFrB&(R3XK!gn3910 +zAcKctSOoI%krM%#);73!C6ozdJT`mLI-kCM=O#hfo|#8cD4G=s?SkLE;Ib(9^!m|qzC_kw +zGDDbma!h7Z;*TJ*io2u>Pd_Xqb=@&(H@?0G#vSF3pWYj7t@J8bIjh<<(7n~FCo?Wf +zTtGUqxo!etl@o4lUl1DWpRjT2)e_8HqchxAp*Gni8CyN-g5G3f4^0@9E|;gD_qs6+ +zWqI#oEA)KW>(V3I>#QV$@Uiq~hg8BtJdJm?Ls|`{^VqO`;rQc_786=FA(`Aka`~3a +zt#%NTlwr(ms*FNRt$yGKjiJhkv1uz +z^qC?*7OTojNBH=1Rggm-c+RMmT^P}fOTB|KK3;ssHbjd&Tky4v#3PofVB6P-zx16&_f*vWGylfjKN@hm3phEK&pDgzN$xA3hwl*!jl +zp|PGtL3O4iIGsZ7meN2itSqv2`rM8%X9c}1JaZx3z3zzfwjQ@{>b5Waq&tnoj5m>| +zrCr02zoJj<=8~}r&i_4ri+dW>O{R{X{HUj-Hcp(p-$drs%J<=Mk~oR?OChF77s8W& +z{?Txsm6QyYVm1a>A$^u30 +z2oJI@XfiKys8Erk#p{=9yVR9rwBXwCKPi&$yth>=m8gEQ^TB*hSriLCPGa|dxCniX +zm5q!vz5BT6L9%&-U#?Nwz=&qDA@z|eZHAaNaJaKdxXaT#lkI;y7PG6#jdI-TU!xnb +zjrsVLFyym!_Dqqdw=(znd@}spxVho;@%TFaYUax~Fu#FU3)jCr@|`%i1ohE5IJvra +z{82agl +z_5KG`9(Ap(&{lJb|1W0OQsHR^5Shd&`1!Io^gKEXld|Ul>(%wtS2!2%73d;Qgp60` +zTwB22(|IZXj9YBy+jO$p&MXg?E{lqHvPkb`@xCiVYmdj-Us18CYFbIn#KY4&JTcG3 +zOUYrAXp|mjE2||5(4+H_&Wv|-1753x#|?g`6F;#TtVGY1W9IITt0{?h^xPjF&%4Qd +zR`!YKip*;pp?O^1^v@0fZ1YP_CT_5+p^t0xk)H6DATfGgVh(oBKxSzb)AfIwrTB-! +z3Tf=`7f#V1>SoJ*W_B)IofgKPZmi>f$NP6}|8otmu1^s;{$+hvS^m2;&%xNv!PxP) +zZ1_JbI!7ATwg+v9-#NPd-2`!2Pv==p8$e(Ur+BlVaHn_?+h`*3U>YMO$XjCxvPJ9{ +zJ-^yK7>V#A3iau$W{Pw8aA~+)cS2JR=6Us;hlWWt;($s`tnuXpT?;r4YHq8V%?!1N}e}f6;0*+hjboQ+} +z5aQ1R-K|9z-3lTh0Tj!6eRmzD;PD5g2nXg-LO7}4fTQBJx0;$i>@AcR_M=tJ^^?fTs!5a!ED4 +zE~9AKi-~R8sU1Wn5X(y@@nKVD0I7Xx7q!_u>(2j)L{~0dHA~vhufUE+PKz@aV4a+t +zj!=joejMhii#ysvL`Ax+gjfTQj8h2|eN2V3(P-UW9__iP<_G@{0t9*vCWiW4xD|@% +z;r3k$Tk44)Chxywdk~E)L8v=3b%;>&3;22RN;%mmnRax==bqmf*6w8QE<4_$g*vQk##6!~T?-h&eYpjB2bDW?ILe +zZa+x`cmG1IbAQbJ8h`F7RUwMM2?z7_Cca1*nscm|W7sh*8Q5Fs=}En4*li=stXr!+ +zyr4{ZvZbHJY&s3UOkpJ;CbG^9e^5f +z-p{*Ya_T<%-mUY=?_A;kMx3hEzqEOtvl|{KVb?Or0+1A}l)^>4?sS}MxrFnsxeO>? +zHaWa5^eQ!clDta!47krudgYBnHE;!gkzDqy<-33lqGNGOEK_ljM~FXsrtTZR_1bUz +zhheH;VqNejWbLHm1wq&m1zi2S?v~UbqEIiC=Yt>WH=hi@`guyoT5>@SYTdwmRxg8A +z)=)o!$|*=Q+qbllTUVA?1Op+fF%Y5-f$4>_lb^yOeaRUzM}-o~N^8nwFfTq&)1RV{ +z4RUkB$P5x>k<`RXeT!$Z7CrL9xS3w+5_kSboyAdpDpem;AiO$|$m=>Xf<3g~x(-l< +z9b3>q9Cvpp?w2Bw(sVI=Pe>Owll*S(i)Yz$P(~S31H}^t;=LWf4&_?n(C{?G&m1QE +ziL!;v$(eiTxD-R1C6c$xNsjPF3THRDBqsq$ro+*J-^aEkd?w=P@9xLWkx?E}XihxC`% +zLF7rG8`UbfKF#}5<+uDtjkz+Hp!{*gyI8=tst5@Bt^=6k$E%L2sUwuJK#|V)49ARv +z9GlwDHL~61UTEBnq}mH>Y(&$)M}qO~2&$gAuBsJ|WMlpWky_EN%88YyhOMr7*G_dF +zako@qvy31B#G{Pe&;EJ}M0o)l?d5eB;Kyg+09)}XQYU?<&fLYXhhG4nViXBdqY)>E +zAWDQp2EXyqF*$%kIEI9T#U@~jog-A#3jC$HWm7w?CeWKONDFRx{14+to0lIUp{B4u +zkm;l>F#E<$P&%#&C7F-!5_TbGsWTRl%&cJUKF|DErOL;Q*R@IE;6_)B;MOqsMNv3*$hJHOd&Ga0Sfy1uZ1sx9M{-JwyuV2S;Oz$G?1R%oNpGQl01&G3mBsMkA8PxBToDg>o05|utbv2`XH +zI;*;KYX4Q@DaRk$gFTt>#CMB#k{SFUpc2VL_q4b9x}`bNzoQMCddW6ipT%gj(;{!jpG1>k?BfSCl +ze(W#k{rpmCf8h>Fa@oH^zzviQ>3SJ^sS-j!Tjgz!Dsz9Vin9G=tur9wtS{3=wzVV! +zoo#l?0@ao@IpA}}D!XCMa7L_DF#L=w}&KcRMSu_%5M;UyT)`PpVoJ`%F0UA|Bg;S3AvasJ?LL6s6sKCeMAUo)OaMw-*Va} +z5OUqkbR6QAHQ37fNL|0y5yxGl65GE2QVe0~fb?ME_llNx4FrbL<9XIwpY^&q^P`E^ +z!%blB!Heihb?PR~$mwc}{PK@Gkk(Z4P~pq*S)o_W`OU+n(`(C_Oy*YmT$)Iw6C-uC +zw81H9OM3ZfuJ41Y!^vUXQEm1f$7gXl9;4>UZ*=|cn~+46+Cq)vS1`nd1sdQA56kI? +zrTbD6-EDSmaW+Hw*G=>~bQ&|&M^!lziBJ0`w>li=UwWw8t2SGu^>%L)Zl^Q4>r7fs +zc{yYhSc7b6=Tr;vR>@bjYa`f|q+?7u*zAQwC?zbvX7-EP?n&C$F3`)Q!LNkuEX`qN +z4i3QVH|Y_m6|Aj2k~%pEG`wmo{%f*dKwoM_Mep0_Y8O +zjm_@}(Qpa@ZYhAP+OzEJ*@l({R+W%+uWl);edRPlZXmS9EqjVB8-Ac5TVuR_L0s~_ +zf0xGcb?vRb_!n(YUeViL%vCFNBG%vn}s +z5EOaqIrHCFTiODf{eTv@(KHYBAD4}uzZZi84jxF%pv>ClE5Gb8cD!gmJ`v-{}cV@TXxESnrSe> +z8jO4Y!eD2t)^#WN5B}2unU@WxEexK89#8K<(pmKv8GMAEjT%b0?RjHbD!Q +zor)J{4AqX)dZd{O&Xe9hb%ZfJbc2o9QNZO3c>s=cmL#Y_!+0Ac3yvg*h`@ltH;+(n +zri>+G9=_cW;|a(F)}BcL1@fB}!0%Rv=ms5uDq7A14(HdQz7u9N0c7Q2@T@#}$h=CoP +zW{I@Ezdks4i~)p9d6AG~@&}NkoZzKk&nmDhXQd|!mC6v)ynnUX#jc(MNr4ynuqjwh +zqdTL)F>Jv`@OB|nXt*JmVD>nLQVCe|s$OMP@(s`t{dHe!f~ImtpR@Fv`=)@7Eo`E) +z2}l*S+`4zMxrMqKx4N+-sRVodPYF-?_weR6_sPfs|l-&%3LU2pT3#&g*VhAJYAITR`>q2;p8T4)sg;j(4m2| +z=)w9n^Q?)8aQnJ{~oxQqCyyIzbz~l_4aw#cO>8WRr%!>45 +zd~-5aPb9JWcH|c!J1G_Z%!qLohEkVIv8`&F{pm5VNmouDUR;q%_*98iIh(cZawCOC +zI(A9J&bfJd?5k%%XI%?!u;_wF3w(cX)@p4@6i>8zw7fNeB3%0GFzl7HJ&5zumFbi( +zZ7bI&rFB5z(7)0c5{@*@r0+~?Rs(veg#@cjptyLzSg&;Zt6=Bvnj#SRQcYWl?N_XA +zunAN$k=n~HAt{cHS&1yLmEd?whHT9pN?)?L<3tw44401@RgvD@L-PzyBp2S!+RZGn +zuQnnkSYW&x8~VU6wR+(M47PzQ4B*_mdR(oHc9cnYLi-x4y|1VaY`n;folG)V@|VD< +zVF|cxJ-BGiVAm}?W!c&%=M1UPR_1vckEHcApH{WU(hM*czJQ9kw3d#ls$NjC8@4Qu +z8;I`k^}rdA9m28XS3)6sMXH2TD;y=CDWdM-irP(2wiqp8V$?4+S6tkK*oU0_g-Go5vd_G$Sm0ci2?iUKXaAt-CaYEl(njL>Sz<4K+9m7m +z?p$j1iXM}o-81Cgsqxh+E-v7>XDat%zdluOz)82D-PUktnj7t?I8}?CFfdBQ!Q#1o*eqZs@tc9{PPDw3Y +zP6yq|;rVe>m#J3NP03|T{7jlZQ1p=Pj0hm;7*#b)X4J|lWN<5rnU36q?OvJsq%gW8 +z3ok^`-P1OjJR)p)ev!Witg{K4V6W4R0FIFrEPE2C#uP6mq1G|S^i0d0f$>-{yTpE0 +z)EaH%n|Zl6eS7LT_d)#bHvK?(@%?|r;)!IsABU3w0F;jZcS)S1p}vjH|J<}(@mi{G +zay0BbP~$i-Z}FOY5PW3l3#$qJqcOc-7ciOLj)N}Q@ME{>TyX(kP2b${sgz96_E@tq +zOSt5{fakF)Cs9t8EUr*Eo9@>0#VIjQ0&n~U!bas0lh#5hXag=;C}s+GS)>v!iDYI1 +z?k&(zv0d{<`ZKq#&?`T{H*Dd~eO-0=f_-zYyVtfjS3lfab3+!)FuY(A-C6qJYhAW` +zR=wTjlq+?@B4l%V*Wb*YeP3*CU3IT7Rf>l$&^KyzZ@NF{+^DJeJipJ;JMp?%E6+sK +zot5KUK|7w0Zz}xWPfE2v-yWVHT03t&Ki;;VeR{F>pC__=dps0>eCxgpzh=ItpC7E> +z>@g-27-T{P<#kA3QG0sB2P7D#OBN_-BMD?6-y+x!tOAn$1)8Ub^^;Hya!`uO-AvEP +zr;&(>^_$$*kW0OU_`f@{&op+ud{1XD{F)aEA%0-@&uAtlc16;B!^nJheD1O&$A}VM +z8zx&_ucH#{D1hK +zyE_C0K^i2L2Bo{ZOS+}Iq`TugzPH3z@AbapgRvQB@Z)*rnrqfRd+j-4M2SRN#KA+* +z(NAbDkhMUv*5p-}eZ*X{XUVw{!orqW8N$P^M4}TBAy^1M9)A&!M)Kvo+Tb0?_=Jqi +z8pseJfSTpgZx`E_e_5`x!wC!^~$gF<(Mo<57xW`bCMO)O%jz +zKhakl!k9p+6YKEwr`Nz1w7`wonEIf*MsZ{!f09OIQ;3j@bm4EFHB3QlRlys8^4g7) +z0Vi+l^mvoCafd2l8_eYPL*b4o0iz?zdQ|Qe-(rpJa-p;-u){Y*jEP!es|J^W@LZGN +zcrdg80EQA)OX3w4XMd&!_r5gq@}!B+_8VXBX2Lh0K*u+}aDIXXOkG!q&22VphrU`- +ze7Yx(>J{=<*MUWq2a-O3m{sPj<;}|$(2+RC_{e$YBK-Wr%9JjVz=Bo@@NHyvx{oM! +zR?u%B_xCK)()ya>#u>5VaAmr`Oh4d*s^deUUh0kYu{@)v?b;9n)jFW->%H> +zeZEclD(n+tO9F2~N|K^Smx;WaFhw{89{06}lK(+(JH&_{@1O&67`g2TGrd84j9+!+ +zqxjTGGW5N9wl9j9t4FWTi6R1(K1pEt0iV~($f^R +zzYi0}H93w}MjYtqpa=hw8VR4mpDp$3Mqvlu-`ygMkvG~v2wnfcU^0WdT$8v4FV9LZ +z6yc^sU1c<3#c|RB9iX+2pV@4NnjDBx_-1hAg4G`fGX?bh^Cj}YPEI*UM7gODcLSM# +z>MH^mb$pUX?7qI9g`X}{(oG>fF~iK(AUQ?gqml;8QM!pYANT9&vRP906^W9+TjbIC +z1lLH8e~(iK9R(Xg`&~(BNWzx;&S>&dqAYh|1COZ8vLN7qy@*#fHp{l96 +zb~8$gZQ!x~5csokA_)xdH@UZRIVYTsp%!nNnDyCf6tGX^v31D>L2kCQcoei=@I4V< +zE>X~qcb1}!RaN@%>Sp(XH&{;)o|DqK7`ycOM>*(2aB5-L?m%ZuZ$Fpvf;=8qJ3=zE +z;si5v>97dr^p^C&>FiEYOz8rp=g{2EnA{D_o*6U@?nT~hRN*~t$*_%U`;kwCDQ3yt +z^1v>zg$d*s3Be&XQE +z1nv?VkLfQ5-B`obr%rH73&zTdCI(d1Ey;bRnw(-C-yS(Ni-Y8KA5I}GW!wvxJ-9% +zjDCJGuK4oYq=`w9ZI*jIUmt +zJ(&y+Hne5@E_bdmKg0QbK@ww6)d~2}>Q-0x0+OKRhPJ99r|ap-xIc575+$)xvi`IP +z@@f3rKGP@OmDf9JvMOu%Rw;&~{A!Yf1%<6!+CDPS()f!ZLzQ2M5jmi7Qi*zQYmL#; +z?V^mYkH%jrv~{JGL>1W4I{T(dil$0F`&c~vq9)>|FeXl1OPM%>`4OxE_>5I-sdbjqm&adYG`U@0eqhBQN{dh6&tNy +z9UdeMF9^Zd!rqES72VUigc!^s?}1M{n>M$_p9^0hQ-@zg_RDg!m5oe?0E}3{f)?wk +zKX!)i2~9!DP-f23o@(}C(N6N~7&3Wb%)Mgw$ku~e<-ms+B)ieM3QWVIv2J}WYln*= +z;@VkR=!T6IxyP;l6MXgFK6t_5+ +zsvKF(NI5*#Ol<2EF6+BR9o8lZ_enG*q`_fi*2giIV-Ivp3f`B}n!y$l%2R{M$@E&G +zUw?Hn@XBwH6_XOuBqnn58N$2;!yHfkUgXmDxclo_%6b_)p{A2SQmL#v0p$AgOts!E +z7~1!S&b@)N(^X>1b~Ds#*zaMwSe4#AH`lhF{J>rP?yNbREA`#`nk65CQ5JYyJKV6U +zF4OlEaW`GJ`?!yXH|x06rQp&v_=WLW~~TT +z6H+-~lex8jiYRnz(B;mE7f(Ndr$oC+p<3o2|XV!~I_ +z8b07=B%22|M?F~2Go1@q_RQb8U@E9bt=7A3@lSgy@8RpWvF?~0hEer0GorcqWesb~ +z%+@$^BmB|L{q(4pZ>~k!(qX}}sk+y5`sL2Fqw(CPBO9Be`1h1q4iKD@dh!$}OBU*P +z%k|myBsBZPz5$9pys|a68$nxo_9)sIf|Li7zQxq(^(U_Lx}A%Yxo^@T*@>3~wg@&r +z(;UTd+jQ(?YobNG%bTHnUv8s3T2h2Eq0jD~SE8kaC#5IX1^n`5T=@j_ +zc*EVyKRm>eP0mzumDWBqAU#rNLSEIGGO>T?#WgroS%OUW%>q>*DJVDOX+8o~BaOz& +z#}NaDbX<&_lPs>RoYsb&8!{@$a*$69WiHayjDoK%>?>cB&%!0Av1NrV=O*Q<%Vktp +z-=aS6={HR868Ep1_8YAXyNsi#K10Wh>3yz&Z{(A=6l%4EQMitibl+H!=KF9O^$C2#PKs#jZO4cK@#U7ywM92~P +zU1;4B9!%%t4wSGhT{&&H!N&L3Gho%He9CqV^;WyXsh^7@#>JjmvTc%)d<^ba>7QTQ +zpBVbSnDyX7O5UBL!Jk1$#F$CE@KIq6Ug{e@{+0M~Hgu*_Qy2Lg?Q{3TMH54#t`dP& +zlk3?<>FO&u7N@dV)^`w+4*qAbvUIWfhwS8Z92|OM7RCl7iUfCWh)4T8W!NpFqw^!P +zhF?iFm@}SA-ol5juAS>YzRCfk2j7np_%s}Jws?JirC?) +zpVnwX4HG9~m0!16ReG(A4woD9)6tX5REv%;36INc(LY8Cq1qfaM+>jS%1y)DB=U`&ElaAV~-_#6um9}r)5ZP-X|dNko`?DiD=(rfY*)ppeH3-m<}<#(r3olt4A}*N}_mtf$hEizaru +zf+p=y>=J?A$sSv1iHl&M-Kkja9VAiXZeUliLNl5&T{Ba9h%AOH|&S62SYW};A&C5@(HrA +ziH10YDwWl6mY%o_ZUsTdY?L3RO^8&TIb-tVshiEXI1fKS092AqQ +zXHREO%UO5|2*ps3^r4>b0g3NGpjw*>RLxDMmmVUzNyJrjw{6+E#$e@YP?TD5i<@QJ +zaBw8uQ$yLYnhB{T(y%%uLM8coI<{?7aCE&%0yEQ!Am(eoqLzxu^Dw5(DUA6rYFBP2^I=wRCky*^B_p*d-OtB|uNR|__EP`?hM(<>}jIWfoGNpSnwNY|rHL#Nn? +zWtdZBUX}&FHo^2)_UvPI_i51>87NQk{N!aWDudKevQQZvR%1;4p!l9c2^hhS^y?M~ +zA!3?X!-@q|JdHpY)`J&-x#Ht};u(Lo6T&o~oDIsHwZ0FzJjV0t7+*M7Fo5O@v=X3k +zhoBH5U`_8uJfZfB`+&B~wu*p-(Lo}2#2L$gmO8Q<*Y;ljbPQzkcS2OQI=Y*W7#gS7 +zh%d`GD65^X&^&79CnU?r`UIM}4@UPEuGI_qw?u97KI;lwE?wdlC->V30h~hzDw}3K +z>-k4%c8l|w+8B;9$uNETnO4qDSVF5;f{Z^bwUx!0#TF`@Q8^Rx*5Zq?JZd6m+K4c* +z##hcET3A8q0l&=261zX&Tbi*s=&HI{VYs_M@aF9#N<~8W0^TIQ&EI$jcgJbWFO>XI +zb5lj7eXMS1I7(F!u|J298-eHZL +zOL^;%#WKMTg@6lEDal@k;8a=C$kM2A>LlUn5K-Q$@|b5mi)5CU_BB=drBtg%rwM}Ls0sSMjbW)ITLm&%_q8Hbe;F%QZ0 +zVTyKrVUmT|TG*j6xCe#3@}=f*1o}SOMdcsC$MdVG$dS%pCYEzW^_k#G +zGuYq+gNr1ofIgi3;%pmhgvP{Tz&E%n;Hp4v0!|ITV!Hec8jDj%#GgT9ub6);1Fqbh +z4@|1}YV)WUgy_c0`EbO_6yD0HXPhw?t=#FvgE>BcwfXH_Em*pe +z>e6Plz~F_FsFM#haPgmNNwxaI3YWZ{5s7cZr +z)@|*D=X!H6)~;YY%8i}>NOse1x#dK#B+vXdk5~`}Q;vwm6MIb2Y0JN`)vQ%LYO5%N +ztJSj3hFHQC3$fDMa#sK+`B4=ZCGcI>RudSsHX$u6wY4;*_e29r5=PrOxg?2nI}9m$ +ziznq5t{Tj5Sr4A?YDG;MO=Hy42{bT|iOQ!FhCj8jDZt{LZZAD3JNJdInqS|))2N5L +ztnn@Nt5 +z+Oc|I^3gX?Xmwk*6|7l|?y1U>1;=#px2-8QW{ +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 +z;SGlMHBfZ}HOZ-}{ee +zNIW_NJ*vcYp14EccZWkgCB6}aU{)P4_A}t>soNTR~VD=DG*1$CvdNcg{k*p^(>J&@M1Mur_<2mfCgpI`sz+N;hB01Ta~}h{1&_V2K3qVd +zP1!bvhYn_>iLpfZQb6sb4GLevf+(OAMcmG)Fy^riNk}wTZUlMSTww?CM10t$$bO_~ +zsFeBZL{%BFpumpQ#!CdgDnTdsQWfq_l2eNG7{yKkXGw5_dIglvdX1O2xC{B0uDCr; +zTDNhkjdNlv9nVHrmMnk=s`HluD~cl6v&wqkR6AR6?%+GdW7w +zo1J%_3ZhAL;{b5xZCDhq)pK(=`&2-H?sLiJRq$DQ6I*xPP%o288bs@L&#t{*aZ|&(rm@s83dY{>vD&Uv4`s0QbocU!PyKE +z%wii7LC?|$rWC0pU}|&FpY^NnF8Wkz60<0-D=`h2=H^+^^rvdHS2L?dsWr--BEM$0 +zz%CL)?W4e=4XS(7orfCF{H_iB^|6?YS~;wVJrf$q`?P10RMb#LtXsxkWbJM;qqY0B +z8l3{rQfXTvV#AD?E2k?s>g2JvF~}%%6CI5@1Z>yO4!6z=W+blj>RgO(O)?K`5`011 +zMIb?O_7~cSZSyT?PZ~!}uqMk?v!OHN^_@{RCv%;$)5R@E4B^7qk +z$XfNr`fs^tWN7+pr)=$W(DMw8=kTm6z!0xFDw{D`4P?03+I8VL?mBrxK_RO4u=LpD +zT@3|KgH@wrF2)KCDmLU2AaOfAKX9z_%aZ7AacpCU%0HFSr +zbX@ct4NQ&f|Fk_m6=~HUblpx;ZLyzWM{c;LlQYVURfvhwoTFs1PH?slSb +zq=RD}m3zW>y8{s{0Y)siadN>?7Tc+%_oY_kNVa}NjmQYLpQ +z*xI6O2Z<`>_D9|ky9Y7zW-i)?q~lanRL{u@FyauqT)f3qtUmQcm81{HSYnz`fj%jl +z>hn$xwl!`VkM-OAd`G8N5(Py)&{w<&oCWG|{jO!7P{SiL|EC+{+hk)ZjrQZ;uL((uEYdEUV$jYL!P5=wb-p@c@g +zJRUd6;-`W(!e-ZD4Rc_sL1%_g*xVT(eDj7CCueC?AkjYzhC{tn&w|fC?_7**bGj(E +zd>;pFN3uKywe6A8;r>bQ*dTZ3@}1V8MquAwGaejH*6GEr?kYKv}#hNs0H;iVdbfR(RH_ +z%_jW}j&Tle4Vl3pNVm&U7&B>^cR4Z>Gk4b=>sT8-eLQSnU>RhVXUXLqRm2_!K&4=l +zJPw6=*d1JVD$-U|xr&fuMrFnjY4~%h{91fsH+?f2$xCWl)l%o8C8dI`voRRng&-RU +zoD44YPDV9B^16Y9$UbZUH0!75!Jr>oh)umbB*zXBji!Kau;Vh67o_tzvrSFYK{4me +z0d8;`gQXXgwIT12IM*Ukbt73wK(MK^)%}SAo2{&vtIo!XaC#JFI~2i}D%GZatO`6f +za^8q7wq9rmLVmz&gK?SBC*^0eA{}v%>^H`N(`E8!**Q_b7|W(_V|W7y9#(LCAN{m+nRJD2CnWAt@~LW(-427!Mr9A;YcmYMQA}+n-uRV={|E&7GpQG9 +z2OWe{DPwDGLGVs(YzUh$z{4X!V#ACI0#U^GHt$Gzh@YO~Q5u+< +zQn2fI4yLT-OrLXO=i8TL)pwkteN32xgT@gZni!v1nYxrnl=+)GSL?pu!R%J=_X+7k +z6!|x2S!-EAF<(kuo``htji^Hl$kmmGfLv8_mtZmYJyx}>!fAKpBm=LP)ux|g+ +zM;^$hOsm14$7(mU^VKMV0?gg=BO?2la%52s=Q_p{${1UpzH^Nh!aBYRAq;?RuBwka +z%pKg`j-wI%!tC)hA+}Y8jrlxJ3Bi)$l)5wti{2Fhw@$YP-ix%~$pdu}!Y=Dv=;Y<> +z+x<8fb-nav#&;sDa##NNE9jRA!)&{jW=*Y~wlh#oD?0}3qaOz`I@@|odq<{0Q7UlO +z7gxuExt8$3%`^3Rc&q7n=l +zT;>^;Xd)V`cZzu5&)t2YBN1Bc3ix9Ar}n +zYb-LA@mj3uxwF2*n?W81wgMAuO>j@CTtArS2*^GwV5086K*gMYq7;25uOa9Q2PUD$ +zdz7ip3~ce(Qj_i17CJwfWa*yr?yzEWY4f5YQum#uvG{?F?Xf|&&=IBx^QCQL{8>s1 +zbDZ9u>vVAoLPOg4WHx2592;;f+G@QX_1Gl~QEV +z_WDKc&B+no@nwP!PraaIyGi@TYY-!Ep2;{JT|E%>xhdX5ui6Sy%HX;)oTC +z7x&mVmGJ?07syAbVThn%P{^^=l&w_U-(~V +z0lT4s?o~Os!v+%bVQ;roTV{JT6<9kjD`?SCbG&6UteKD?tvpeF6R7&(38{0ByBqJ* +zYH`0*Ol03sXd@Vu^uj=t48dmR0yf+?z?bdR0L7Jv6ONS=PpEiRh>q+LSa?dMP!A#G +zBtbw4!GIMl@aJoF@t+UB|A7Kv|NaRXuq*KOqnWicu)ux%TnPdI_O-zHZn?jK{We<1 +z+A&xhqa5|EKCoi81x_SD{T&Rb8o-B_U(tRC)6p@rHgo)4l|Wo#<-f&s0OATgrc31m +zBFF=;hq(Jdwm)!z??OA6*;xN)i-5Sae~d+62^sKGOFRTVj5LY-MX{ZER-p2P$yp^55-S5D*pe%3n4F_?-qbJph360QG|NTU19Q +zSI0kK75^Ugs}i1C6P#ZGl~4dw0>T5>L7?6JF07HY^B<7^u>Lrpqy^H8f!Xi?KsxdF +zJBBRx7o@9!k?q$F`>i+z+Jf~z3tP)>IUNWG02~03AKDnC);GvT22PH@kA?!`{)5C{ +zm7wqR7~`uyTqAtHV>70I;(jyQ+I}yxp1p~a6|mvhKWgN+c=lBz5+>)<9QP_@25L;1r`ezOV&2#`Nyb#gSb`~$P*XPCd@d!UGa +zVgjdZ8QR$YgYW$q^AG3v>fj$zK_i>K{K0TCF60Rg~^<^fL`()W7*a}oM_1{N-Q_J$4y +zHdeNJ|0bXqp(!A}1ePpZz?en&fbIqIPwAw@g+yc&MgC)+zYtb(i;G}!f#$hK^7SFb +zw?Rt|r?E70H3P=SU*3Ji+ZKthH8m4ZO%IoV +zkN-35|6%iw26}8^fD}T&5Ks0%Q&xmOqX9nJ(R0u-G&A@&+YV)SdXoaQFe%`gs2&iF +zlKzy)(b3*a-^tPF-;#Y!iYv+j+88OY=pcAVM)otZe_>)PzzFzGRa)JE004OF^nDdJ +zO!+;9gPH3eaq#Nj#lb%+^53lLmGJk-ZhBUh%)e|?+8gM`RKTxM87aTERQlE}hJJ$l +zKUmZR845Zt(4x42S@$7{9B}V{Ph-D_@!!P%8bWOTnBw)%sr_qJGW*Ic=muCfaOr;^ +z8`tE1Mq=&YXzyh3?5UT1;LyRnd3@N~{ +z^3YV34S&pF=wxO4`;*mQQ`VL8WPLoampp0C_YoJ{_{S7RR%VWWbfx_@jwO4esx6?$ +z-NSsZDCn0z;rMgD?5{CA9K!jn@jmnW2?Nl|ejmyCYZRz-buqKR7-b1G--mlq=>KDi +ze|^*PaA4uLt5^cgG5nEt{58Jt*A`L5j?+~MoSN5Z@ckkh1Aol$uL|q0-j~STO{Ncw +zBkKPdQ1I;{dgFgg@y)n`Ut7haAn`>dU{2Z+{@%g;Qh!GAu$%I)@qlw=>U#n!1$f}? +z&BJKLTKF@bZ+g!C8VRmSAWjuI01(^%y#vBj{g~w27BRoZp`FBr{2mP0Py`qiAJz-l +zvp?Z@cuM2fXdXTg{&wpT7Jp3h&Ew&}qxf-E^)UZMFaL;w<(Kmxr{29P63}h~D8Aq9 +z<)0w`5BaaloL-X#7(h_|EB{Gs{aF0}A^$mVqwo#^cX|ZqdE@v{l>t4 +zO!4pY-@}RO-}*%g;ZHdJ-}2u8$xj&mZT@>Wf%w}+u#*3n;{P%KnNs|i;@{@Khf`y} +zU4#hLj~V`T{(Cr}=UWOs_8(LH>-_g{e!#aBApAe0_+Rqh!!E1e@(79ljOSnHzlTlH +zz9qSo|1rtG&wmfkC4S2>?fest|KI%A?fzq$U(A2MeIN7Rhh4s1iLBS36c3KR0Q2fU +tJem4hwLN^E_ZP0u%XUA|hfnu@mZ%{hfz>fU9QezL2LR-~1{VB){{tZ({`CL= + +diff --git a/tasks/_vendor/path.py b/tasks/_vendor/path.py +deleted file mode 100644 +index 2c7a71c..0000000 +--- a/tasks/_vendor/path.py ++++ /dev/null +@@ -1,1725 +0,0 @@ +-# +-# SOURCE: https://pypi.python.org/pypi/path.py +-# VERSION: 8.2.1 +-# ----------------------------------------------------------------------------- +-# Copyright (c) 2010 Mikhail Gusarov +-# +-# Permission is hereby granted, free of charge, to any person obtaining a copy +-# of this software and associated documentation files (the "Software"), to deal +-# in the Software without restriction, including without limitation the rights +-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +-# copies of the Software, and to permit persons to whom the Software is +-# furnished to do so, subject to the following conditions: +-# +-# The above copyright notice and this permission notice shall be included in +-# all copies or substantial portions of the Software. +-# +-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-# SOFTWARE. +-# +- +-""" +-path.py - An object representing a path to a file or directory. +- +-https://github.com/jaraco/path.py +- +-Example:: +- +- from path import Path +- d = Path('/home/guido/bin') +- for f in d.files('*.py'): +- f.chmod(0o755) +-""" +- +-from __future__ import unicode_literals +- +-import sys +-import warnings +-import os +-import fnmatch +-import glob +-import shutil +-import codecs +-import hashlib +-import errno +-import tempfile +-import functools +-import operator +-import re +-import contextlib +-import io +-from distutils import dir_util +-import importlib +- +-try: +- import win32security +-except ImportError: +- pass +- +-try: +- import pwd +-except ImportError: +- pass +- +-try: +- import grp +-except ImportError: +- pass +- +-############################################################################## +-# Python 2/3 support +-PY3 = sys.version_info >= (3,) +-PY2 = not PY3 +- +-string_types = str, +-text_type = str +-getcwdu = os.getcwd +- +-def surrogate_escape(error): +- """ +- Simulate the Python 3 ``surrogateescape`` handler, but for Python 2 only. +- """ +- chars = error.object[error.start:error.end] +- assert len(chars) == 1 +- val = ord(chars) +- val += 0xdc00 +- return __builtin__.unichr(val), error.end +- +-if PY2: +- import __builtin__ +- string_types = __builtin__.basestring, +- text_type = __builtin__.unicode +- getcwdu = os.getcwdu +- codecs.register_error('surrogateescape', surrogate_escape) +- +-@contextlib.contextmanager +-def io_error_compat(): +- try: +- yield +- except IOError as io_err: +- # On Python 2, io.open raises IOError; transform to OSError for +- # future compatibility. +- os_err = OSError(*io_err.args) +- os_err.filename = getattr(io_err, 'filename', None) +- raise os_err +- +-############################################################################## +- +-__all__ = ['Path', 'CaseInsensitivePattern'] +- +- +-LINESEPS = ['\r\n', '\r', '\n'] +-U_LINESEPS = LINESEPS + ['\u0085', '\u2028', '\u2029'] +-NEWLINE = re.compile('|'.join(LINESEPS)) +-U_NEWLINE = re.compile('|'.join(U_LINESEPS)) +-NL_END = re.compile(r'(?:{0})$'.format(NEWLINE.pattern)) +-U_NL_END = re.compile(r'(?:{0})$'.format(U_NEWLINE.pattern)) +- +- +-try: +- import pkg_resources +- __version__ = pkg_resources.require('path.py')[0].version +-except Exception: +- __version__ = '8.2.1' # XXX-MODIFIED-WAS: 'unknown' +- +- +-class TreeWalkWarning(Warning): +- pass +- +- +-# from jaraco.functools +-def compose(*funcs): +- compose_two = lambda f1, f2: lambda *args, **kwargs: f1(f2(*args, **kwargs)) +- return functools.reduce(compose_two, funcs) +- +- +-def simple_cache(func): +- """ +- Save results for the :meth:'path.using_module' classmethod. +- When Python 3.2 is available, use functools.lru_cache instead. +- """ +- saved_results = {} +- +- def wrapper(cls, module): +- if module in saved_results: +- return saved_results[module] +- saved_results[module] = func(cls, module) +- return saved_results[module] +- return wrapper +- +- +-class ClassProperty(property): +- def __get__(self, cls, owner): +- return self.fget.__get__(None, owner)() +- +- +-class multimethod(object): +- """ +- Acts like a classmethod when invoked from the class and like an +- instancemethod when invoked from the instance. +- """ +- def __init__(self, func): +- self.func = func +- +- def __get__(self, instance, owner): +- return ( +- functools.partial(self.func, owner) if instance is None +- else functools.partial(self.func, owner, instance) +- ) +- +- +-class Path(text_type): +- """ +- Represents a filesystem path. +- +- For documentation on individual methods, consult their +- counterparts in :mod:`os.path`. +- +- Some methods are additionally included from :mod:`shutil`. +- The functions are linked directly into the class namespace +- such that they will be bound to the Path instance. For example, +- ``Path(src).copy(target)`` is equivalent to +- ``shutil.copy(src, target)``. Therefore, when referencing +- the docs for these methods, assume `src` references `self`, +- the Path instance. +- """ +- +- module = os.path +- """ The path module to use for path operations. +- +- .. seealso:: :mod:`os.path` +- """ +- +- def __init__(self, other=''): +- if other is None: +- raise TypeError("Invalid initial value for path: None") +- +- @classmethod +- @simple_cache +- def using_module(cls, module): +- subclass_name = cls.__name__ + '_' + module.__name__ +- if PY2: +- subclass_name = str(subclass_name) +- bases = (cls,) +- ns = {'module': module} +- return type(subclass_name, bases, ns) +- +- @ClassProperty +- @classmethod +- def _next_class(cls): +- """ +- What class should be used to construct new instances from this class +- """ +- return cls +- +- @classmethod +- def _always_unicode(cls, path): +- """ +- Ensure the path as retrieved from a Python API, such as :func:`os.listdir`, +- is a proper Unicode string. +- """ +- if PY3 or isinstance(path, text_type): +- return path +- return path.decode(sys.getfilesystemencoding(), 'surrogateescape') +- +- # --- Special Python methods. +- +- def __repr__(self): +- return '%s(%s)' % (type(self).__name__, super(Path, self).__repr__()) +- +- # Adding a Path and a string yields a Path. +- def __add__(self, more): +- try: +- return self._next_class(super(Path, self).__add__(more)) +- except TypeError: # Python bug +- return NotImplemented +- +- def __radd__(self, other): +- if not isinstance(other, string_types): +- return NotImplemented +- return self._next_class(other.__add__(self)) +- +- # The / operator joins Paths. +- def __div__(self, rel): +- """ fp.__div__(rel) == fp / rel == fp.joinpath(rel) +- +- Join two path components, adding a separator character if +- needed. +- +- .. seealso:: :func:`os.path.join` +- """ +- return self._next_class(self.module.join(self, rel)) +- +- # Make the / operator work even when true division is enabled. +- __truediv__ = __div__ +- +- # The / operator joins Paths the other way around +- def __rdiv__(self, rel): +- """ fp.__rdiv__(rel) == rel / fp +- +- Join two path components, adding a separator character if +- needed. +- +- .. seealso:: :func:`os.path.join` +- """ +- return self._next_class(self.module.join(rel, self)) +- +- # Make the / operator work even when true division is enabled. +- __rtruediv__ = __rdiv__ +- +- def __enter__(self): +- self._old_dir = self.getcwd() +- os.chdir(self) +- return self +- +- def __exit__(self, *_): +- os.chdir(self._old_dir) +- +- @classmethod +- def getcwd(cls): +- """ Return the current working directory as a path object. +- +- .. seealso:: :func:`os.getcwdu` +- """ +- return cls(getcwdu()) +- +- # +- # --- Operations on Path strings. +- +- def abspath(self): +- """ .. seealso:: :func:`os.path.abspath` """ +- return self._next_class(self.module.abspath(self)) +- +- def normcase(self): +- """ .. seealso:: :func:`os.path.normcase` """ +- return self._next_class(self.module.normcase(self)) +- +- def normpath(self): +- """ .. seealso:: :func:`os.path.normpath` """ +- return self._next_class(self.module.normpath(self)) +- +- def realpath(self): +- """ .. seealso:: :func:`os.path.realpath` """ +- return self._next_class(self.module.realpath(self)) +- +- def expanduser(self): +- """ .. seealso:: :func:`os.path.expanduser` """ +- return self._next_class(self.module.expanduser(self)) +- +- def expandvars(self): +- """ .. seealso:: :func:`os.path.expandvars` """ +- return self._next_class(self.module.expandvars(self)) +- +- def dirname(self): +- """ .. seealso:: :attr:`parent`, :func:`os.path.dirname` """ +- return self._next_class(self.module.dirname(self)) +- +- def basename(self): +- """ .. seealso:: :attr:`name`, :func:`os.path.basename` """ +- return self._next_class(self.module.basename(self)) +- +- def expand(self): +- """ Clean up a filename by calling :meth:`expandvars()`, +- :meth:`expanduser()`, and :meth:`normpath()` on it. +- +- This is commonly everything needed to clean up a filename +- read from a configuration file, for example. +- """ +- return self.expandvars().expanduser().normpath() +- +- @property +- def namebase(self): +- """ The same as :meth:`name`, but with one file extension stripped off. +- +- For example, +- ``Path('/home/guido/python.tar.gz').name == 'python.tar.gz'``, +- but +- ``Path('/home/guido/python.tar.gz').namebase == 'python.tar'``. +- """ +- base, ext = self.module.splitext(self.name) +- return base +- +- @property +- def ext(self): +- """ The file extension, for example ``'.py'``. """ +- f, ext = self.module.splitext(self) +- return ext +- +- @property +- def drive(self): +- """ The drive specifier, for example ``'C:'``. +- +- This is always empty on systems that don't use drive specifiers. +- """ +- drive, r = self.module.splitdrive(self) +- return self._next_class(drive) +- +- parent = property( +- dirname, None, None, +- """ This path's parent directory, as a new Path object. +- +- For example, +- ``Path('/usr/local/lib/libpython.so').parent == +- Path('/usr/local/lib')`` +- +- .. seealso:: :meth:`dirname`, :func:`os.path.dirname` +- """) +- +- name = property( +- basename, None, None, +- """ The name of this file or directory without the full path. +- +- For example, +- ``Path('/usr/local/lib/libpython.so').name == 'libpython.so'`` +- +- .. seealso:: :meth:`basename`, :func:`os.path.basename` +- """) +- +- def splitpath(self): +- """ p.splitpath() -> Return ``(p.parent, p.name)``. +- +- .. seealso:: :attr:`parent`, :attr:`name`, :func:`os.path.split` +- """ +- parent, child = self.module.split(self) +- return self._next_class(parent), child +- +- def splitdrive(self): +- """ p.splitdrive() -> Return ``(p.drive, )``. +- +- Split the drive specifier from this path. If there is +- no drive specifier, :samp:`{p.drive}` is empty, so the return value +- is simply ``(Path(''), p)``. This is always the case on Unix. +- +- .. seealso:: :func:`os.path.splitdrive` +- """ +- drive, rel = self.module.splitdrive(self) +- return self._next_class(drive), rel +- +- def splitext(self): +- """ p.splitext() -> Return ``(p.stripext(), p.ext)``. +- +- Split the filename extension from this path and return +- the two parts. Either part may be empty. +- +- The extension is everything from ``'.'`` to the end of the +- last path segment. This has the property that if +- ``(a, b) == p.splitext()``, then ``a + b == p``. +- +- .. seealso:: :func:`os.path.splitext` +- """ +- filename, ext = self.module.splitext(self) +- return self._next_class(filename), ext +- +- def stripext(self): +- """ p.stripext() -> Remove one file extension from the path. +- +- For example, ``Path('/home/guido/python.tar.gz').stripext()`` +- returns ``Path('/home/guido/python.tar')``. +- """ +- return self.splitext()[0] +- +- def splitunc(self): +- """ .. seealso:: :func:`os.path.splitunc` """ +- unc, rest = self.module.splitunc(self) +- return self._next_class(unc), rest +- +- @property +- def uncshare(self): +- """ +- The UNC mount point for this path. +- This is empty for paths on local drives. +- """ +- unc, r = self.module.splitunc(self) +- return self._next_class(unc) +- +- @multimethod +- def joinpath(cls, first, *others): +- """ +- Join first to zero or more :class:`Path` components, adding a separator +- character (:samp:`{first}.module.sep`) if needed. Returns a new instance of +- :samp:`{first}._next_class`. +- +- .. seealso:: :func:`os.path.join` +- """ +- if not isinstance(first, cls): +- first = cls(first) +- return first._next_class(first.module.join(first, *others)) +- +- def splitall(self): +- r""" Return a list of the path components in this path. +- +- The first item in the list will be a Path. Its value will be +- either :data:`os.curdir`, :data:`os.pardir`, empty, or the root +- directory of this path (for example, ``'/'`` or ``'C:\\'``). The +- other items in the list will be strings. +- +- ``path.Path.joinpath(*result)`` will yield the original path. +- """ +- parts = [] +- loc = self +- while loc != os.curdir and loc != os.pardir: +- prev = loc +- loc, child = prev.splitpath() +- if loc == prev: +- break +- parts.append(child) +- parts.append(loc) +- parts.reverse() +- return parts +- +- def relpath(self, start='.'): +- """ Return this path as a relative path, +- based from `start`, which defaults to the current working directory. +- """ +- cwd = self._next_class(start) +- return cwd.relpathto(self) +- +- def relpathto(self, dest): +- """ Return a relative path from `self` to `dest`. +- +- If there is no relative path from `self` to `dest`, for example if +- they reside on different drives in Windows, then this returns +- ``dest.abspath()``. +- """ +- origin = self.abspath() +- dest = self._next_class(dest).abspath() +- +- orig_list = origin.normcase().splitall() +- # Don't normcase dest! We want to preserve the case. +- dest_list = dest.splitall() +- +- if orig_list[0] != self.module.normcase(dest_list[0]): +- # Can't get here from there. +- return dest +- +- # Find the location where the two paths start to differ. +- i = 0 +- for start_seg, dest_seg in zip(orig_list, dest_list): +- if start_seg != self.module.normcase(dest_seg): +- break +- i += 1 +- +- # Now i is the point where the two paths diverge. +- # Need a certain number of "os.pardir"s to work up +- # from the origin to the point of divergence. +- segments = [os.pardir] * (len(orig_list) - i) +- # Need to add the diverging part of dest_list. +- segments += dest_list[i:] +- if len(segments) == 0: +- # If they happen to be identical, use os.curdir. +- relpath = os.curdir +- else: +- relpath = self.module.join(*segments) +- return self._next_class(relpath) +- +- # --- Listing, searching, walking, and matching +- +- def listdir(self, pattern=None): +- """ D.listdir() -> List of items in this directory. +- +- Use :meth:`files` or :meth:`dirs` instead if you want a listing +- of just files or just subdirectories. +- +- The elements of the list are Path objects. +- +- With the optional `pattern` argument, this only lists +- items whose names match the given pattern. +- +- .. seealso:: :meth:`files`, :meth:`dirs` +- """ +- if pattern is None: +- pattern = '*' +- return [ +- self / child +- for child in map(self._always_unicode, os.listdir(self)) +- if self._next_class(child).fnmatch(pattern) +- ] +- +- def dirs(self, pattern=None): +- """ D.dirs() -> List of this directory's subdirectories. +- +- The elements of the list are Path objects. +- This does not walk recursively into subdirectories +- (but see :meth:`walkdirs`). +- +- With the optional `pattern` argument, this only lists +- directories whose names match the given pattern. For +- example, ``d.dirs('build-*')``. +- """ +- return [p for p in self.listdir(pattern) if p.isdir()] +- +- def files(self, pattern=None): +- """ D.files() -> List of the files in this directory. +- +- The elements of the list are Path objects. +- This does not walk into subdirectories (see :meth:`walkfiles`). +- +- With the optional `pattern` argument, this only lists files +- whose names match the given pattern. For example, +- ``d.files('*.pyc')``. +- """ +- +- return [p for p in self.listdir(pattern) if p.isfile()] +- +- def walk(self, pattern=None, errors='strict'): +- """ D.walk() -> iterator over files and subdirs, recursively. +- +- The iterator yields Path objects naming each child item of +- this directory and its descendants. This requires that +- ``D.isdir()``. +- +- This performs a depth-first traversal of the directory tree. +- Each directory is returned just before all its children. +- +- The `errors=` keyword argument controls behavior when an +- error occurs. The default is ``'strict'``, which causes an +- exception. Other allowed values are ``'warn'`` (which +- reports the error via :func:`warnings.warn()`), and ``'ignore'``. +- `errors` may also be an arbitrary callable taking a msg parameter. +- """ +- class Handlers: +- def strict(msg): +- raise +- +- def warn(msg): +- warnings.warn(msg, TreeWalkWarning) +- +- def ignore(msg): +- pass +- +- if not callable(errors) and errors not in vars(Handlers): +- raise ValueError("invalid errors parameter") +- errors = vars(Handlers).get(errors, errors) +- +- try: +- childList = self.listdir() +- except Exception: +- exc = sys.exc_info()[1] +- tmpl = "Unable to list directory '%(self)s': %(exc)s" +- msg = tmpl % locals() +- errors(msg) +- return +- +- for child in childList: +- if pattern is None or child.fnmatch(pattern): +- yield child +- try: +- isdir = child.isdir() +- except Exception: +- exc = sys.exc_info()[1] +- tmpl = "Unable to access '%(child)s': %(exc)s" +- msg = tmpl % locals() +- errors(msg) +- isdir = False +- +- if isdir: +- for item in child.walk(pattern, errors): +- yield item +- +- def walkdirs(self, pattern=None, errors='strict'): +- """ D.walkdirs() -> iterator over subdirs, recursively. +- +- With the optional `pattern` argument, this yields only +- directories whose names match the given pattern. For +- example, ``mydir.walkdirs('*test')`` yields only directories +- with names ending in ``'test'``. +- +- The `errors=` keyword argument controls behavior when an +- error occurs. The default is ``'strict'``, which causes an +- exception. The other allowed values are ``'warn'`` (which +- reports the error via :func:`warnings.warn()`), and ``'ignore'``. +- """ +- if errors not in ('strict', 'warn', 'ignore'): +- raise ValueError("invalid errors parameter") +- +- try: +- dirs = self.dirs() +- except Exception: +- if errors == 'ignore': +- return +- elif errors == 'warn': +- warnings.warn( +- "Unable to list directory '%s': %s" +- % (self, sys.exc_info()[1]), +- TreeWalkWarning) +- return +- else: +- raise +- +- for child in dirs: +- if pattern is None or child.fnmatch(pattern): +- yield child +- for subsubdir in child.walkdirs(pattern, errors): +- yield subsubdir +- +- def walkfiles(self, pattern=None, errors='strict'): +- """ D.walkfiles() -> iterator over files in D, recursively. +- +- The optional argument `pattern` limits the results to files +- with names that match the pattern. For example, +- ``mydir.walkfiles('*.tmp')`` yields only files with the ``.tmp`` +- extension. +- """ +- if errors not in ('strict', 'warn', 'ignore'): +- raise ValueError("invalid errors parameter") +- +- try: +- childList = self.listdir() +- except Exception: +- if errors == 'ignore': +- return +- elif errors == 'warn': +- warnings.warn( +- "Unable to list directory '%s': %s" +- % (self, sys.exc_info()[1]), +- TreeWalkWarning) +- return +- else: +- raise +- +- for child in childList: +- try: +- isfile = child.isfile() +- isdir = not isfile and child.isdir() +- except: +- if errors == 'ignore': +- continue +- elif errors == 'warn': +- warnings.warn( +- "Unable to access '%s': %s" +- % (self, sys.exc_info()[1]), +- TreeWalkWarning) +- continue +- else: +- raise +- +- if isfile: +- if pattern is None or child.fnmatch(pattern): +- yield child +- elif isdir: +- for f in child.walkfiles(pattern, errors): +- yield f +- +- def fnmatch(self, pattern, normcase=None): +- """ Return ``True`` if `self.name` matches the given `pattern`. +- +- `pattern` - A filename pattern with wildcards, +- for example ``'*.py'``. If the pattern contains a `normcase` +- attribute, it is applied to the name and path prior to comparison. +- +- `normcase` - (optional) A function used to normalize the pattern and +- filename before matching. Defaults to :meth:`self.module`, which defaults +- to :meth:`os.path.normcase`. +- +- .. seealso:: :func:`fnmatch.fnmatch` +- """ +- default_normcase = getattr(pattern, 'normcase', self.module.normcase) +- normcase = normcase or default_normcase +- name = normcase(self.name) +- pattern = normcase(pattern) +- return fnmatch.fnmatchcase(name, pattern) +- +- def glob(self, pattern): +- """ Return a list of Path objects that match the pattern. +- +- `pattern` - a path relative to this directory, with wildcards. +- +- For example, ``Path('/users').glob('*/bin/*')`` returns a list +- of all the files users have in their :file:`bin` directories. +- +- .. seealso:: :func:`glob.glob` +- """ +- cls = self._next_class +- return [cls(s) for s in glob.glob(self / pattern)] +- +- # +- # --- Reading or writing an entire file at once. +- +- def open(self, *args, **kwargs): +- """ Open this file and return a corresponding :class:`file` object. +- +- Keyword arguments work as in :func:`io.open`. If the file cannot be +- opened, an :class:`~exceptions.OSError` is raised. +- """ +- with io_error_compat(): +- return io.open(self, *args, **kwargs) +- +- def bytes(self): +- """ Open this file, read all bytes, return them as a string. """ +- with self.open('rb') as f: +- return f.read() +- +- def chunks(self, size, *args, **kwargs): +- """ Returns a generator yielding chunks of the file, so it can +- be read piece by piece with a simple for loop. +- +- Any argument you pass after `size` will be passed to :meth:`open`. +- +- :example: +- +- >>> hash = hashlib.md5() +- >>> for chunk in Path("path.py").chunks(8192, mode='rb'): +- ... hash.update(chunk) +- +- This will read the file by chunks of 8192 bytes. +- """ +- with self.open(*args, **kwargs) as f: +- for chunk in iter(lambda: f.read(size) or None, None): +- yield chunk +- +- def write_bytes(self, bytes, append=False): +- """ Open this file and write the given bytes to it. +- +- Default behavior is to overwrite any existing file. +- Call ``p.write_bytes(bytes, append=True)`` to append instead. +- """ +- if append: +- mode = 'ab' +- else: +- mode = 'wb' +- with self.open(mode) as f: +- f.write(bytes) +- +- def text(self, encoding=None, errors='strict'): +- r""" Open this file, read it in, return the content as a string. +- +- All newline sequences are converted to ``'\n'``. Keyword arguments +- will be passed to :meth:`open`. +- +- .. seealso:: :meth:`lines` +- """ +- with self.open(mode='r', encoding=encoding, errors=errors) as f: +- return U_NEWLINE.sub('\n', f.read()) +- +- def write_text(self, text, encoding=None, errors='strict', +- linesep=os.linesep, append=False): +- r""" Write the given text to this file. +- +- The default behavior is to overwrite any existing file; +- to append instead, use the `append=True` keyword argument. +- +- There are two differences between :meth:`write_text` and +- :meth:`write_bytes`: newline handling and Unicode handling. +- See below. +- +- Parameters: +- +- `text` - str/unicode - The text to be written. +- +- `encoding` - str - The Unicode encoding that will be used. +- This is ignored if `text` isn't a Unicode string. +- +- `errors` - str - How to handle Unicode encoding errors. +- Default is ``'strict'``. See ``help(unicode.encode)`` for the +- options. This is ignored if `text` isn't a Unicode +- string. +- +- `linesep` - keyword argument - str/unicode - The sequence of +- characters to be used to mark end-of-line. The default is +- :data:`os.linesep`. You can also specify ``None`` to +- leave all newlines as they are in `text`. +- +- `append` - keyword argument - bool - Specifies what to do if +- the file already exists (``True``: append to the end of it; +- ``False``: overwrite it.) The default is ``False``. +- +- +- --- Newline handling. +- +- ``write_text()`` converts all standard end-of-line sequences +- (``'\n'``, ``'\r'``, and ``'\r\n'``) to your platform's default +- end-of-line sequence (see :data:`os.linesep`; on Windows, for example, +- the end-of-line marker is ``'\r\n'``). +- +- If you don't like your platform's default, you can override it +- using the `linesep=` keyword argument. If you specifically want +- ``write_text()`` to preserve the newlines as-is, use ``linesep=None``. +- +- This applies to Unicode text the same as to 8-bit text, except +- there are three additional standard Unicode end-of-line sequences: +- ``u'\x85'``, ``u'\r\x85'``, and ``u'\u2028'``. +- +- (This is slightly different from when you open a file for +- writing with ``fopen(filename, "w")`` in C or ``open(filename, 'w')`` +- in Python.) +- +- +- --- Unicode +- +- If `text` isn't Unicode, then apart from newline handling, the +- bytes are written verbatim to the file. The `encoding` and +- `errors` arguments are not used and must be omitted. +- +- If `text` is Unicode, it is first converted to :func:`bytes` using the +- specified `encoding` (or the default encoding if `encoding` +- isn't specified). The `errors` argument applies only to this +- conversion. +- +- """ +- if isinstance(text, text_type): +- if linesep is not None: +- text = U_NEWLINE.sub(linesep, text) +- text = text.encode(encoding or sys.getdefaultencoding(), errors) +- else: +- assert encoding is None +- text = NEWLINE.sub(linesep, text) +- self.write_bytes(text, append=append) +- +- def lines(self, encoding=None, errors='strict', retain=True): +- r""" Open this file, read all lines, return them in a list. +- +- Optional arguments: +- `encoding` - The Unicode encoding (or character set) of +- the file. The default is ``None``, meaning the content +- of the file is read as 8-bit characters and returned +- as a list of (non-Unicode) str objects. +- `errors` - How to handle Unicode errors; see help(str.decode) +- for the options. Default is ``'strict'``. +- `retain` - If ``True``, retain newline characters; but all newline +- character combinations (``'\r'``, ``'\n'``, ``'\r\n'``) are +- translated to ``'\n'``. If ``False``, newline characters are +- stripped off. Default is ``True``. +- +- This uses ``'U'`` mode. +- +- .. seealso:: :meth:`text` +- """ +- if encoding is None and retain: +- with self.open('U') as f: +- return f.readlines() +- else: +- return self.text(encoding, errors).splitlines(retain) +- +- def write_lines(self, lines, encoding=None, errors='strict', +- linesep=os.linesep, append=False): +- r""" Write the given lines of text to this file. +- +- By default this overwrites any existing file at this path. +- +- This puts a platform-specific newline sequence on every line. +- See `linesep` below. +- +- `lines` - A list of strings. +- +- `encoding` - A Unicode encoding to use. This applies only if +- `lines` contains any Unicode strings. +- +- `errors` - How to handle errors in Unicode encoding. This +- also applies only to Unicode strings. +- +- linesep - The desired line-ending. This line-ending is +- applied to every line. If a line already has any +- standard line ending (``'\r'``, ``'\n'``, ``'\r\n'``, +- ``u'\x85'``, ``u'\r\x85'``, ``u'\u2028'``), that will +- be stripped off and this will be used instead. The +- default is os.linesep, which is platform-dependent +- (``'\r\n'`` on Windows, ``'\n'`` on Unix, etc.). +- Specify ``None`` to write the lines as-is, like +- :meth:`file.writelines`. +- +- Use the keyword argument ``append=True`` to append lines to the +- file. The default is to overwrite the file. +- +- .. warning :: +- +- When you use this with Unicode data, if the encoding of the +- existing data in the file is different from the encoding +- you specify with the `encoding=` parameter, the result is +- mixed-encoding data, which can really confuse someone trying +- to read the file later. +- """ +- with self.open('ab' if append else 'wb') as f: +- for l in lines: +- isUnicode = isinstance(l, text_type) +- if linesep is not None: +- pattern = U_NL_END if isUnicode else NL_END +- l = pattern.sub('', l) + linesep +- if isUnicode: +- l = l.encode(encoding or sys.getdefaultencoding(), errors) +- f.write(l) +- +- def read_md5(self): +- """ Calculate the md5 hash for this file. +- +- This reads through the entire file. +- +- .. seealso:: :meth:`read_hash` +- """ +- return self.read_hash('md5') +- +- def _hash(self, hash_name): +- """ Returns a hash object for the file at the current path. +- +- `hash_name` should be a hash algo name (such as ``'md5'`` or ``'sha1'``) +- that's available in the :mod:`hashlib` module. +- """ +- m = hashlib.new(hash_name) +- for chunk in self.chunks(8192, mode="rb"): +- m.update(chunk) +- return m +- +- def read_hash(self, hash_name): +- """ Calculate given hash for this file. +- +- List of supported hashes can be obtained from :mod:`hashlib` package. +- This reads the entire file. +- +- .. seealso:: :meth:`hashlib.hash.digest` +- """ +- return self._hash(hash_name).digest() +- +- def read_hexhash(self, hash_name): +- """ Calculate given hash for this file, returning hexdigest. +- +- List of supported hashes can be obtained from :mod:`hashlib` package. +- This reads the entire file. +- +- .. seealso:: :meth:`hashlib.hash.hexdigest` +- """ +- return self._hash(hash_name).hexdigest() +- +- # --- Methods for querying the filesystem. +- # N.B. On some platforms, the os.path functions may be implemented in C +- # (e.g. isdir on Windows, Python 3.2.2), and compiled functions don't get +- # bound. Playing it safe and wrapping them all in method calls. +- +- def isabs(self): +- """ .. seealso:: :func:`os.path.isabs` """ +- return self.module.isabs(self) +- +- def exists(self): +- """ .. seealso:: :func:`os.path.exists` """ +- return self.module.exists(self) +- +- def isdir(self): +- """ .. seealso:: :func:`os.path.isdir` """ +- return self.module.isdir(self) +- +- def isfile(self): +- """ .. seealso:: :func:`os.path.isfile` """ +- return self.module.isfile(self) +- +- def islink(self): +- """ .. seealso:: :func:`os.path.islink` """ +- return self.module.islink(self) +- +- def ismount(self): +- """ .. seealso:: :func:`os.path.ismount` """ +- return self.module.ismount(self) +- +- def samefile(self, other): +- """ .. seealso:: :func:`os.path.samefile` """ +- if not hasattr(self.module, 'samefile'): +- other = Path(other).realpath().normpath().normcase() +- return self.realpath().normpath().normcase() == other +- return self.module.samefile(self, other) +- +- def getatime(self): +- """ .. seealso:: :attr:`atime`, :func:`os.path.getatime` """ +- return self.module.getatime(self) +- +- atime = property( +- getatime, None, None, +- """ Last access time of the file. +- +- .. seealso:: :meth:`getatime`, :func:`os.path.getatime` +- """) +- +- def getmtime(self): +- """ .. seealso:: :attr:`mtime`, :func:`os.path.getmtime` """ +- return self.module.getmtime(self) +- +- mtime = property( +- getmtime, None, None, +- """ Last-modified time of the file. +- +- .. seealso:: :meth:`getmtime`, :func:`os.path.getmtime` +- """) +- +- def getctime(self): +- """ .. seealso:: :attr:`ctime`, :func:`os.path.getctime` """ +- return self.module.getctime(self) +- +- ctime = property( +- getctime, None, None, +- """ Creation time of the file. +- +- .. seealso:: :meth:`getctime`, :func:`os.path.getctime` +- """) +- +- def getsize(self): +- """ .. seealso:: :attr:`size`, :func:`os.path.getsize` """ +- return self.module.getsize(self) +- +- size = property( +- getsize, None, None, +- """ Size of the file, in bytes. +- +- .. seealso:: :meth:`getsize`, :func:`os.path.getsize` +- """) +- +- if hasattr(os, 'access'): +- def access(self, mode): +- """ Return ``True`` if current user has access to this path. +- +- mode - One of the constants :data:`os.F_OK`, :data:`os.R_OK`, +- :data:`os.W_OK`, :data:`os.X_OK` +- +- .. seealso:: :func:`os.access` +- """ +- return os.access(self, mode) +- +- def stat(self): +- """ Perform a ``stat()`` system call on this path. +- +- .. seealso:: :meth:`lstat`, :func:`os.stat` +- """ +- return os.stat(self) +- +- def lstat(self): +- """ Like :meth:`stat`, but do not follow symbolic links. +- +- .. seealso:: :meth:`stat`, :func:`os.lstat` +- """ +- return os.lstat(self) +- +- def __get_owner_windows(self): +- """ +- Return the name of the owner of this file or directory. Follow +- symbolic links. +- +- Return a name of the form ``r'DOMAIN\\User Name'``; may be a group. +- +- .. seealso:: :attr:`owner` +- """ +- desc = win32security.GetFileSecurity( +- self, win32security.OWNER_SECURITY_INFORMATION) +- sid = desc.GetSecurityDescriptorOwner() +- account, domain, typecode = win32security.LookupAccountSid(None, sid) +- return domain + '\\' + account +- +- def __get_owner_unix(self): +- """ +- Return the name of the owner of this file or directory. Follow +- symbolic links. +- +- .. seealso:: :attr:`owner` +- """ +- st = self.stat() +- return pwd.getpwuid(st.st_uid).pw_name +- +- def __get_owner_not_implemented(self): +- raise NotImplementedError("Ownership not available on this platform.") +- +- if 'win32security' in globals(): +- get_owner = __get_owner_windows +- elif 'pwd' in globals(): +- get_owner = __get_owner_unix +- else: +- get_owner = __get_owner_not_implemented +- +- owner = property( +- get_owner, None, None, +- """ Name of the owner of this file or directory. +- +- .. seealso:: :meth:`get_owner`""") +- +- if hasattr(os, 'statvfs'): +- def statvfs(self): +- """ Perform a ``statvfs()`` system call on this path. +- +- .. seealso:: :func:`os.statvfs` +- """ +- return os.statvfs(self) +- +- if hasattr(os, 'pathconf'): +- def pathconf(self, name): +- """ .. seealso:: :func:`os.pathconf` """ +- return os.pathconf(self, name) +- +- # +- # --- Modifying operations on files and directories +- +- def utime(self, times): +- """ Set the access and modified times of this file. +- +- .. seealso:: :func:`os.utime` +- """ +- os.utime(self, times) +- return self +- +- def chmod(self, mode): +- """ +- Set the mode. May be the new mode (os.chmod behavior) or a `symbolic +- mode `_. +- +- .. seealso:: :func:`os.chmod` +- """ +- if isinstance(mode, string_types): +- mask = _multi_permission_mask(mode) +- mode = mask(self.stat().st_mode) +- os.chmod(self, mode) +- return self +- +- def chown(self, uid=-1, gid=-1): +- """ +- Change the owner and group by names rather than the uid or gid numbers. +- +- .. seealso:: :func:`os.chown` +- """ +- if hasattr(os, 'chown'): +- if 'pwd' in globals() and isinstance(uid, string_types): +- uid = pwd.getpwnam(uid).pw_uid +- if 'grp' in globals() and isinstance(gid, string_types): +- gid = grp.getgrnam(gid).gr_gid +- os.chown(self, uid, gid) +- else: +- raise NotImplementedError("Ownership not available on this platform.") +- return self +- +- def rename(self, new): +- """ .. seealso:: :func:`os.rename` """ +- os.rename(self, new) +- return self._next_class(new) +- +- def renames(self, new): +- """ .. seealso:: :func:`os.renames` """ +- os.renames(self, new) +- return self._next_class(new) +- +- # +- # --- Create/delete operations on directories +- +- def mkdir(self, mode=0o777): +- """ .. seealso:: :func:`os.mkdir` """ +- os.mkdir(self, mode) +- return self +- +- def mkdir_p(self, mode=0o777): +- """ Like :meth:`mkdir`, but does not raise an exception if the +- directory already exists. """ +- try: +- self.mkdir(mode) +- except OSError: +- _, e, _ = sys.exc_info() +- if e.errno != errno.EEXIST: +- raise +- return self +- +- def makedirs(self, mode=0o777): +- """ .. seealso:: :func:`os.makedirs` """ +- os.makedirs(self, mode) +- return self +- +- def makedirs_p(self, mode=0o777): +- """ Like :meth:`makedirs`, but does not raise an exception if the +- directory already exists. """ +- try: +- self.makedirs(mode) +- except OSError: +- _, e, _ = sys.exc_info() +- if e.errno != errno.EEXIST: +- raise +- return self +- +- def rmdir(self): +- """ .. seealso:: :func:`os.rmdir` """ +- os.rmdir(self) +- return self +- +- def rmdir_p(self): +- """ Like :meth:`rmdir`, but does not raise an exception if the +- directory is not empty or does not exist. """ +- try: +- self.rmdir() +- except OSError: +- _, e, _ = sys.exc_info() +- if e.errno != errno.ENOTEMPTY and e.errno != errno.EEXIST: +- raise +- return self +- +- def removedirs(self): +- """ .. seealso:: :func:`os.removedirs` """ +- os.removedirs(self) +- return self +- +- def removedirs_p(self): +- """ Like :meth:`removedirs`, but does not raise an exception if the +- directory is not empty or does not exist. """ +- try: +- self.removedirs() +- except OSError: +- _, e, _ = sys.exc_info() +- if e.errno != errno.ENOTEMPTY and e.errno != errno.EEXIST: +- raise +- return self +- +- # --- Modifying operations on files +- +- def touch(self): +- """ Set the access/modified times of this file to the current time. +- Create the file if it does not exist. +- """ +- fd = os.open(self, os.O_WRONLY | os.O_CREAT, 0o666) +- os.close(fd) +- os.utime(self, None) +- return self +- +- def remove(self): +- """ .. seealso:: :func:`os.remove` """ +- os.remove(self) +- return self +- +- def remove_p(self): +- """ Like :meth:`remove`, but does not raise an exception if the +- file does not exist. """ +- try: +- self.unlink() +- except OSError: +- _, e, _ = sys.exc_info() +- if e.errno != errno.ENOENT: +- raise +- return self +- +- def unlink(self): +- """ .. seealso:: :func:`os.unlink` """ +- os.unlink(self) +- return self +- +- def unlink_p(self): +- """ Like :meth:`unlink`, but does not raise an exception if the +- file does not exist. """ +- self.remove_p() +- return self +- +- # --- Links +- +- if hasattr(os, 'link'): +- def link(self, newpath): +- """ Create a hard link at `newpath`, pointing to this file. +- +- .. seealso:: :func:`os.link` +- """ +- os.link(self, newpath) +- return self._next_class(newpath) +- +- if hasattr(os, 'symlink'): +- def symlink(self, newlink): +- """ Create a symbolic link at `newlink`, pointing here. +- +- .. seealso:: :func:`os.symlink` +- """ +- os.symlink(self, newlink) +- return self._next_class(newlink) +- +- if hasattr(os, 'readlink'): +- def readlink(self): +- """ Return the path to which this symbolic link points. +- +- The result may be an absolute or a relative path. +- +- .. seealso:: :meth:`readlinkabs`, :func:`os.readlink` +- """ +- return self._next_class(os.readlink(self)) +- +- def readlinkabs(self): +- """ Return the path to which this symbolic link points. +- +- The result is always an absolute path. +- +- .. seealso:: :meth:`readlink`, :func:`os.readlink` +- """ +- p = self.readlink() +- if p.isabs(): +- return p +- else: +- return (self.parent / p).abspath() +- +- # High-level functions from shutil +- # These functions will be bound to the instance such that +- # Path(name).copy(target) will invoke shutil.copy(name, target) +- +- copyfile = shutil.copyfile +- copymode = shutil.copymode +- copystat = shutil.copystat +- copy = shutil.copy +- copy2 = shutil.copy2 +- copytree = shutil.copytree +- if hasattr(shutil, 'move'): +- move = shutil.move +- rmtree = shutil.rmtree +- +- def rmtree_p(self): +- """ Like :meth:`rmtree`, but does not raise an exception if the +- directory does not exist. """ +- try: +- self.rmtree() +- except OSError: +- _, e, _ = sys.exc_info() +- if e.errno != errno.ENOENT: +- raise +- return self +- +- def chdir(self): +- """ .. seealso:: :func:`os.chdir` """ +- os.chdir(self) +- +- cd = chdir +- +- def merge_tree(self, dst, symlinks=False, *args, **kwargs): +- """ +- Copy entire contents of self to dst, overwriting existing +- contents in dst with those in self. +- +- If the additional keyword `update` is True, each +- `src` will only be copied if `dst` does not exist, +- or `src` is newer than `dst`. +- +- Note that the technique employed stages the files in a temporary +- directory first, so this function is not suitable for merging +- trees with large files, especially if the temporary directory +- is not capable of storing a copy of the entire source tree. +- """ +- update = kwargs.pop('update', False) +- with tempdir() as _temp_dir: +- # first copy the tree to a stage directory to support +- # the parameters and behavior of copytree. +- stage = _temp_dir / str(hash(self)) +- self.copytree(stage, symlinks, *args, **kwargs) +- # now copy everything from the stage directory using +- # the semantics of dir_util.copy_tree +- dir_util.copy_tree(stage, dst, preserve_symlinks=symlinks, +- update=update) +- +- # +- # --- Special stuff from os +- +- if hasattr(os, 'chroot'): +- def chroot(self): +- """ .. seealso:: :func:`os.chroot` """ +- os.chroot(self) +- +- if hasattr(os, 'startfile'): +- def startfile(self): +- """ .. seealso:: :func:`os.startfile` """ +- os.startfile(self) +- return self +- +- # in-place re-writing, courtesy of Martijn Pieters +- # http://www.zopatista.com/python/2013/11/26/inplace-file-rewriting/ +- @contextlib.contextmanager +- def in_place(self, mode='r', buffering=-1, encoding=None, errors=None, +- newline=None, backup_extension=None): +- """ +- A context in which a file may be re-written in-place with new content. +- +- Yields a tuple of :samp:`({readable}, {writable})` file objects, where `writable` +- replaces `readable`. +- +- If an exception occurs, the old file is restored, removing the +- written data. +- +- Mode *must not* use ``'w'``, ``'a'``, or ``'+'``; only read-only-modes are +- allowed. A :exc:`ValueError` is raised on invalid modes. +- +- For example, to add line numbers to a file:: +- +- p = Path(filename) +- assert p.isfile() +- with p.in_place() as (reader, writer): +- for number, line in enumerate(reader, 1): +- writer.write('{0:3}: '.format(number))) +- writer.write(line) +- +- Thereafter, the file at `filename` will have line numbers in it. +- """ +- import io +- +- if set(mode).intersection('wa+'): +- raise ValueError('Only read-only file modes can be used') +- +- # move existing file to backup, create new file with same permissions +- # borrowed extensively from the fileinput module +- backup_fn = self + (backup_extension or os.extsep + 'bak') +- try: +- os.unlink(backup_fn) +- except os.error: +- pass +- os.rename(self, backup_fn) +- readable = io.open(backup_fn, mode, buffering=buffering, +- encoding=encoding, errors=errors, newline=newline) +- try: +- perm = os.fstat(readable.fileno()).st_mode +- except OSError: +- writable = open(self, 'w' + mode.replace('r', ''), +- buffering=buffering, encoding=encoding, errors=errors, +- newline=newline) +- else: +- os_mode = os.O_CREAT | os.O_WRONLY | os.O_TRUNC +- if hasattr(os, 'O_BINARY'): +- os_mode |= os.O_BINARY +- fd = os.open(self, os_mode, perm) +- writable = io.open(fd, "w" + mode.replace('r', ''), +- buffering=buffering, encoding=encoding, errors=errors, +- newline=newline) +- try: +- if hasattr(os, 'chmod'): +- os.chmod(self, perm) +- except OSError: +- pass +- try: +- yield readable, writable +- except Exception: +- # move backup back +- readable.close() +- writable.close() +- try: +- os.unlink(self) +- except os.error: +- pass +- os.rename(backup_fn, self) +- raise +- else: +- readable.close() +- writable.close() +- finally: +- try: +- os.unlink(backup_fn) +- except os.error: +- pass +- +- @ClassProperty +- @classmethod +- def special(cls): +- """ +- Return a SpecialResolver object suitable referencing a suitable +- directory for the relevant platform for the given +- type of content. +- +- For example, to get a user config directory, invoke: +- +- dir = Path.special().user.config +- +- Uses the `appdirs +- `_ to resolve +- the paths in a platform-friendly way. +- +- To create a config directory for 'My App', consider: +- +- dir = Path.special("My App").user.config.makedirs_p() +- +- If the ``appdirs`` module is not installed, invocation +- of special will raise an ImportError. +- """ +- return functools.partial(SpecialResolver, cls) +- +- +-class SpecialResolver(object): +- class ResolverScope: +- def __init__(self, paths, scope): +- self.paths = paths +- self.scope = scope +- +- def __getattr__(self, class_): +- return self.paths.get_dir(self.scope, class_) +- +- def __init__(self, path_class, *args, **kwargs): +- appdirs = importlib.import_module('appdirs') +- +- # let appname default to None until +- # https://github.com/ActiveState/appdirs/issues/55 is solved. +- not args and kwargs.setdefault('appname', None) +- +- vars(self).update( +- path_class=path_class, +- wrapper=appdirs.AppDirs(*args, **kwargs), +- ) +- +- def __getattr__(self, scope): +- return self.ResolverScope(self, scope) +- +- def get_dir(self, scope, class_): +- """ +- Return the callable function from appdirs, but with the +- result wrapped in self.path_class +- """ +- prop_name = '{scope}_{class_}_dir'.format(**locals()) +- value = getattr(self.wrapper, prop_name) +- MultiPath = Multi.for_class(self.path_class) +- return MultiPath.detect(value) +- +- +-class Multi: +- """ +- A mix-in for a Path which may contain multiple Path separated by pathsep. +- """ +- @classmethod +- def for_class(cls, path_cls): +- name = 'Multi' + path_cls.__name__ +- if PY2: +- name = str(name) +- return type(name, (cls, path_cls), {}) +- +- @classmethod +- def detect(cls, input): +- if os.pathsep not in input: +- cls = cls._next_class +- return cls(input) +- +- def __iter__(self): +- return iter(map(self._next_class, self.split(os.pathsep))) +- +- @ClassProperty +- @classmethod +- def _next_class(cls): +- """ +- Multi-subclasses should use the parent class +- """ +- return next( +- class_ +- for class_ in cls.__mro__ +- if not issubclass(class_, Multi) +- ) +- +- +-class tempdir(Path): +- """ +- A temporary directory via :func:`tempfile.mkdtemp`, and constructed with the +- same parameters that you can use as a context manager. +- +- Example: +- +- with tempdir() as d: +- # do stuff with the Path object "d" +- +- # here the directory is deleted automatically +- +- .. seealso:: :func:`tempfile.mkdtemp` +- """ +- +- @ClassProperty +- @classmethod +- def _next_class(cls): +- return Path +- +- def __new__(cls, *args, **kwargs): +- dirname = tempfile.mkdtemp(*args, **kwargs) +- return super(tempdir, cls).__new__(cls, dirname) +- +- def __init__(self, *args, **kwargs): +- pass +- +- def __enter__(self): +- return self +- +- def __exit__(self, exc_type, exc_value, traceback): +- if not exc_value: +- self.rmtree() +- +- +-def _multi_permission_mask(mode): +- """ +- Support multiple, comma-separated Unix chmod symbolic modes. +- +- >>> _multi_permission_mask('a=r,u+w')(0) == 0o644 +- True +- """ +- compose = lambda f, g: lambda *args, **kwargs: g(f(*args, **kwargs)) +- return functools.reduce(compose, map(_permission_mask, mode.split(','))) +- +- +-def _permission_mask(mode): +- """ +- Convert a Unix chmod symbolic mode like ``'ugo+rwx'`` to a function +- suitable for applying to a mask to affect that change. +- +- >>> mask = _permission_mask('ugo+rwx') +- >>> mask(0o554) == 0o777 +- True +- +- >>> _permission_mask('go-x')(0o777) == 0o766 +- True +- +- >>> _permission_mask('o-x')(0o445) == 0o444 +- True +- +- >>> _permission_mask('a+x')(0) == 0o111 +- True +- +- >>> _permission_mask('a=rw')(0o057) == 0o666 +- True +- +- >>> _permission_mask('u=x')(0o666) == 0o166 +- True +- +- >>> _permission_mask('g=')(0o157) == 0o107 +- True +- """ +- # parse the symbolic mode +- parsed = re.match('(?P[ugoa]+)(?P[-+=])(?P[rwx]*)$', mode) +- if not parsed: +- raise ValueError("Unrecognized symbolic mode", mode) +- +- # generate a mask representing the specified permission +- spec_map = dict(r=4, w=2, x=1) +- specs = (spec_map[perm] for perm in parsed.group('what')) +- spec = functools.reduce(operator.or_, specs, 0) +- +- # now apply spec to each subject in who +- shift_map = dict(u=6, g=3, o=0) +- who = parsed.group('who').replace('a', 'ugo') +- masks = (spec << shift_map[subj] for subj in who) +- mask = functools.reduce(operator.or_, masks) +- +- op = parsed.group('op') +- +- # if op is -, invert the mask +- if op == '-': +- mask ^= 0o777 +- +- # if op is =, retain extant values for unreferenced subjects +- if op == '=': +- masks = (0o7 << shift_map[subj] for subj in who) +- retain = functools.reduce(operator.or_, masks) ^ 0o777 +- +- op_map = { +- '+': operator.or_, +- '-': operator.and_, +- '=': lambda mask, target: target & retain ^ mask, +- } +- return functools.partial(op_map[op], mask) +- +- +-class CaseInsensitivePattern(text_type): +- """ +- A string with a ``'normcase'`` property, suitable for passing to +- :meth:`listdir`, :meth:`dirs`, :meth:`files`, :meth:`walk`, +- :meth:`walkdirs`, or :meth:`walkfiles` to match case-insensitive. +- +- For example, to get all files ending in .py, .Py, .pY, or .PY in the +- current directory:: +- +- from path import Path, CaseInsensitivePattern as ci +- Path('.').files(ci('*.py')) +- """ +- +- @property +- def normcase(self): +- return __import__('ntpath').normcase +- +-######################## +-# Backward-compatibility +-class path(Path): +- def __new__(cls, *args, **kwargs): +- msg = "path is deprecated. Use Path instead." +- warnings.warn(msg, DeprecationWarning) +- return Path.__new__(cls, *args, **kwargs) +- +- +-__all__ += ['path'] +-######################## +diff --git a/tasks/_vendor/pathlib.py b/tasks/_vendor/pathlib.py +deleted file mode 100644 +index 9ab0e70..0000000 +--- a/tasks/_vendor/pathlib.py ++++ /dev/null +@@ -1,1280 +0,0 @@ +-import fnmatch +-import functools +-import io +-import ntpath +-import os +-import posixpath +-import re +-import sys +-import time +-from collections import Sequence +-from contextlib import contextmanager +-from errno import EINVAL, ENOENT +-from operator import attrgetter +-from stat import S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO +-try: +- from urllib import quote as urlquote, quote as urlquote_from_bytes +-except ImportError: +- from urllib.parse import quote as urlquote, quote_from_bytes as urlquote_from_bytes +- +- +-try: +- intern = intern +-except NameError: +- intern = sys.intern +-try: +- basestring = basestring +-except NameError: +- basestring = str +- +-supports_symlinks = True +-try: +- import nt +-except ImportError: +- nt = None +-else: +- if sys.getwindowsversion()[:2] >= (6, 0) and sys.version_info >= (3, 2): +- from nt import _getfinalpathname +- else: +- supports_symlinks = False +- _getfinalpathname = None +- +- +-__all__ = [ +- "PurePath", "PurePosixPath", "PureWindowsPath", +- "Path", "PosixPath", "WindowsPath", +- ] +- +-# +-# Internals +-# +- +-_py2 = sys.version_info < (3,) +-_py2_fs_encoding = 'ascii' +- +-def _py2_fsencode(parts): +- # py2 => minimal unicode support +- return [part.encode(_py2_fs_encoding) if isinstance(part, unicode) +- else part for part in parts] +- +-def _is_wildcard_pattern(pat): +- # Whether this pattern needs actual matching using fnmatch, or can +- # be looked up directly as a file. +- return "*" in pat or "?" in pat or "[" in pat +- +- +-class _Flavour(object): +- """A flavour implements a particular (platform-specific) set of path +- semantics.""" +- +- def __init__(self): +- self.join = self.sep.join +- +- def parse_parts(self, parts): +- if _py2: +- parts = _py2_fsencode(parts) +- parsed = [] +- sep = self.sep +- altsep = self.altsep +- drv = root = '' +- it = reversed(parts) +- for part in it: +- if not part: +- continue +- if altsep: +- part = part.replace(altsep, sep) +- drv, root, rel = self.splitroot(part) +- if sep in rel: +- for x in reversed(rel.split(sep)): +- if x and x != '.': +- parsed.append(intern(x)) +- else: +- if rel and rel != '.': +- parsed.append(intern(rel)) +- if drv or root: +- if not drv: +- # If no drive is present, try to find one in the previous +- # parts. This makes the result of parsing e.g. +- # ("C:", "/", "a") reasonably intuitive. +- for part in it: +- drv = self.splitroot(part)[0] +- if drv: +- break +- break +- if drv or root: +- parsed.append(drv + root) +- parsed.reverse() +- return drv, root, parsed +- +- def join_parsed_parts(self, drv, root, parts, drv2, root2, parts2): +- """ +- Join the two paths represented by the respective +- (drive, root, parts) tuples. Return a new (drive, root, parts) tuple. +- """ +- if root2: +- if not drv2 and drv: +- return drv, root2, [drv + root2] + parts2[1:] +- elif drv2: +- if drv2 == drv or self.casefold(drv2) == self.casefold(drv): +- # Same drive => second path is relative to the first +- return drv, root, parts + parts2[1:] +- else: +- # Second path is non-anchored (common case) +- return drv, root, parts + parts2 +- return drv2, root2, parts2 +- +- +-class _WindowsFlavour(_Flavour): +- # Reference for Windows paths can be found at +- # http://msdn.microsoft.com/en-us/library/aa365247%28v=vs.85%29.aspx +- +- sep = '\\' +- altsep = '/' +- has_drv = True +- pathmod = ntpath +- +- is_supported = (nt is not None) +- +- drive_letters = ( +- set(chr(x) for x in range(ord('a'), ord('z') + 1)) | +- set(chr(x) for x in range(ord('A'), ord('Z') + 1)) +- ) +- ext_namespace_prefix = '\\\\?\\' +- +- reserved_names = ( +- set(['CON', 'PRN', 'AUX', 'NUL']) | +- set(['COM%d' % i for i in range(1, 10)]) | +- set(['LPT%d' % i for i in range(1, 10)]) +- ) +- +- # Interesting findings about extended paths: +- # - '\\?\c:\a', '//?/c:\a' and '//?/c:/a' are all supported +- # but '\\?\c:/a' is not +- # - extended paths are always absolute; "relative" extended paths will +- # fail. +- +- def splitroot(self, part, sep=sep): +- first = part[0:1] +- second = part[1:2] +- if (second == sep and first == sep): +- # XXX extended paths should also disable the collapsing of "." +- # components (according to MSDN docs). +- prefix, part = self._split_extended_path(part) +- first = part[0:1] +- second = part[1:2] +- else: +- prefix = '' +- third = part[2:3] +- if (second == sep and first == sep and third != sep): +- # is a UNC path: +- # vvvvvvvvvvvvvvvvvvvvv root +- # \\machine\mountpoint\directory\etc\... +- # directory ^^^^^^^^^^^^^^ +- index = part.find(sep, 2) +- if index != -1: +- index2 = part.find(sep, index + 1) +- # a UNC path can't have two slashes in a row +- # (after the initial two) +- if index2 != index + 1: +- if index2 == -1: +- index2 = len(part) +- if prefix: +- return prefix + part[1:index2], sep, part[index2+1:] +- else: +- return part[:index2], sep, part[index2+1:] +- drv = root = '' +- if second == ':' and first in self.drive_letters: +- drv = part[:2] +- part = part[2:] +- first = third +- if first == sep: +- root = first +- part = part.lstrip(sep) +- return prefix + drv, root, part +- +- def casefold(self, s): +- return s.lower() +- +- def casefold_parts(self, parts): +- return [p.lower() for p in parts] +- +- def resolve(self, path): +- s = str(path) +- if not s: +- return os.getcwd() +- if _getfinalpathname is not None: +- return self._ext_to_normal(_getfinalpathname(s)) +- # Means fallback on absolute +- return None +- +- def _split_extended_path(self, s, ext_prefix=ext_namespace_prefix): +- prefix = '' +- if s.startswith(ext_prefix): +- prefix = s[:4] +- s = s[4:] +- if s.startswith('UNC\\'): +- prefix += s[:3] +- s = '\\' + s[3:] +- return prefix, s +- +- def _ext_to_normal(self, s): +- # Turn back an extended path into a normal DOS-like path +- return self._split_extended_path(s)[1] +- +- def is_reserved(self, parts): +- # NOTE: the rules for reserved names seem somewhat complicated +- # (e.g. r"..\NUL" is reserved but not r"foo\NUL"). +- # We err on the side of caution and return True for paths which are +- # not considered reserved by Windows. +- if not parts: +- return False +- if parts[0].startswith('\\\\'): +- # UNC paths are never reserved +- return False +- return parts[-1].partition('.')[0].upper() in self.reserved_names +- +- def make_uri(self, path): +- # Under Windows, file URIs use the UTF-8 encoding. +- drive = path.drive +- if len(drive) == 2 and drive[1] == ':': +- # It's a path on a local drive => 'file:///c:/a/b' +- rest = path.as_posix()[2:].lstrip('/') +- return 'file:///%s/%s' % ( +- drive, urlquote_from_bytes(rest.encode('utf-8'))) +- else: +- # It's a path on a network drive => 'file://host/share/a/b' +- return 'file:' + urlquote_from_bytes(path.as_posix().encode('utf-8')) +- +- +-class _PosixFlavour(_Flavour): +- sep = '/' +- altsep = '' +- has_drv = False +- pathmod = posixpath +- +- is_supported = (os.name != 'nt') +- +- def splitroot(self, part, sep=sep): +- if part and part[0] == sep: +- stripped_part = part.lstrip(sep) +- # According to POSIX path resolution: +- # http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap04.html#tag_04_11 +- # "A pathname that begins with two successive slashes may be +- # interpreted in an implementation-defined manner, although more +- # than two leading slashes shall be treated as a single slash". +- if len(part) - len(stripped_part) == 2: +- return '', sep * 2, stripped_part +- else: +- return '', sep, stripped_part +- else: +- return '', '', part +- +- def casefold(self, s): +- return s +- +- def casefold_parts(self, parts): +- return parts +- +- def resolve(self, path): +- sep = self.sep +- accessor = path._accessor +- seen = {} +- def _resolve(path, rest): +- if rest.startswith(sep): +- path = '' +- +- for name in rest.split(sep): +- if not name or name == '.': +- # current dir +- continue +- if name == '..': +- # parent dir +- path, _, _ = path.rpartition(sep) +- continue +- newpath = path + sep + name +- if newpath in seen: +- # Already seen this path +- path = seen[newpath] +- if path is not None: +- # use cached value +- continue +- # The symlink is not resolved, so we must have a symlink loop. +- raise RuntimeError("Symlink loop from %r" % newpath) +- # Resolve the symbolic link +- try: +- target = accessor.readlink(newpath) +- except OSError as e: +- if e.errno != EINVAL: +- raise +- # Not a symlink +- path = newpath +- else: +- seen[newpath] = None # not resolved symlink +- path = _resolve(path, target) +- seen[newpath] = path # resolved symlink +- +- return path +- # NOTE: according to POSIX, getcwd() cannot contain path components +- # which are symlinks. +- base = '' if path.is_absolute() else os.getcwd() +- return _resolve(base, str(path)) or sep +- +- def is_reserved(self, parts): +- return False +- +- def make_uri(self, path): +- # We represent the path using the local filesystem encoding, +- # for portability to other applications. +- bpath = bytes(path) +- return 'file://' + urlquote_from_bytes(bpath) +- +- +-_windows_flavour = _WindowsFlavour() +-_posix_flavour = _PosixFlavour() +- +- +-class _Accessor: +- """An accessor implements a particular (system-specific or not) way of +- accessing paths on the filesystem.""" +- +- +-class _NormalAccessor(_Accessor): +- +- def _wrap_strfunc(strfunc): +- @functools.wraps(strfunc) +- def wrapped(pathobj, *args): +- return strfunc(str(pathobj), *args) +- return staticmethod(wrapped) +- +- def _wrap_binary_strfunc(strfunc): +- @functools.wraps(strfunc) +- def wrapped(pathobjA, pathobjB, *args): +- return strfunc(str(pathobjA), str(pathobjB), *args) +- return staticmethod(wrapped) +- +- stat = _wrap_strfunc(os.stat) +- +- lstat = _wrap_strfunc(os.lstat) +- +- open = _wrap_strfunc(os.open) +- +- listdir = _wrap_strfunc(os.listdir) +- +- chmod = _wrap_strfunc(os.chmod) +- +- if hasattr(os, "lchmod"): +- lchmod = _wrap_strfunc(os.lchmod) +- else: +- def lchmod(self, pathobj, mode): +- raise NotImplementedError("lchmod() not available on this system") +- +- mkdir = _wrap_strfunc(os.mkdir) +- +- unlink = _wrap_strfunc(os.unlink) +- +- rmdir = _wrap_strfunc(os.rmdir) +- +- rename = _wrap_binary_strfunc(os.rename) +- +- if sys.version_info >= (3, 3): +- replace = _wrap_binary_strfunc(os.replace) +- +- if nt: +- if supports_symlinks: +- symlink = _wrap_binary_strfunc(os.symlink) +- else: +- def symlink(a, b, target_is_directory): +- raise NotImplementedError("symlink() not available on this system") +- else: +- # Under POSIX, os.symlink() takes two args +- @staticmethod +- def symlink(a, b, target_is_directory): +- return os.symlink(str(a), str(b)) +- +- utime = _wrap_strfunc(os.utime) +- +- # Helper for resolve() +- def readlink(self, path): +- return os.readlink(path) +- +- +-_normal_accessor = _NormalAccessor() +- +- +-# +-# Globbing helpers +-# +- +-@contextmanager +-def _cached(func): +- try: +- func.__cached__ +- yield func +- except AttributeError: +- cache = {} +- def wrapper(*args): +- try: +- return cache[args] +- except KeyError: +- value = cache[args] = func(*args) +- return value +- wrapper.__cached__ = True +- try: +- yield wrapper +- finally: +- cache.clear() +- +-def _make_selector(pattern_parts): +- pat = pattern_parts[0] +- child_parts = pattern_parts[1:] +- if pat == '**': +- cls = _RecursiveWildcardSelector +- elif '**' in pat: +- raise ValueError("Invalid pattern: '**' can only be an entire path component") +- elif _is_wildcard_pattern(pat): +- cls = _WildcardSelector +- else: +- cls = _PreciseSelector +- return cls(pat, child_parts) +- +-if hasattr(functools, "lru_cache"): +- _make_selector = functools.lru_cache()(_make_selector) +- +- +-class _Selector: +- """A selector matches a specific glob pattern part against the children +- of a given path.""" +- +- def __init__(self, child_parts): +- self.child_parts = child_parts +- if child_parts: +- self.successor = _make_selector(child_parts) +- else: +- self.successor = _TerminatingSelector() +- +- def select_from(self, parent_path): +- """Iterate over all child paths of `parent_path` matched by this +- selector. This can contain parent_path itself.""" +- path_cls = type(parent_path) +- is_dir = path_cls.is_dir +- exists = path_cls.exists +- listdir = parent_path._accessor.listdir +- return self._select_from(parent_path, is_dir, exists, listdir) +- +- +-class _TerminatingSelector: +- +- def _select_from(self, parent_path, is_dir, exists, listdir): +- yield parent_path +- +- +-class _PreciseSelector(_Selector): +- +- def __init__(self, name, child_parts): +- self.name = name +- _Selector.__init__(self, child_parts) +- +- def _select_from(self, parent_path, is_dir, exists, listdir): +- if not is_dir(parent_path): +- return +- path = parent_path._make_child_relpath(self.name) +- if exists(path): +- for p in self.successor._select_from(path, is_dir, exists, listdir): +- yield p +- +- +-class _WildcardSelector(_Selector): +- +- def __init__(self, pat, child_parts): +- self.pat = re.compile(fnmatch.translate(pat)) +- _Selector.__init__(self, child_parts) +- +- def _select_from(self, parent_path, is_dir, exists, listdir): +- if not is_dir(parent_path): +- return +- cf = parent_path._flavour.casefold +- for name in listdir(parent_path): +- casefolded = cf(name) +- if self.pat.match(casefolded): +- path = parent_path._make_child_relpath(name) +- for p in self.successor._select_from(path, is_dir, exists, listdir): +- yield p +- +- +-class _RecursiveWildcardSelector(_Selector): +- +- def __init__(self, pat, child_parts): +- _Selector.__init__(self, child_parts) +- +- def _iterate_directories(self, parent_path, is_dir, listdir): +- yield parent_path +- for name in listdir(parent_path): +- path = parent_path._make_child_relpath(name) +- if is_dir(path): +- for p in self._iterate_directories(path, is_dir, listdir): +- yield p +- +- def _select_from(self, parent_path, is_dir, exists, listdir): +- if not is_dir(parent_path): +- return +- with _cached(listdir) as listdir: +- yielded = set() +- try: +- successor_select = self.successor._select_from +- for starting_point in self._iterate_directories(parent_path, is_dir, listdir): +- for p in successor_select(starting_point, is_dir, exists, listdir): +- if p not in yielded: +- yield p +- yielded.add(p) +- finally: +- yielded.clear() +- +- +-# +-# Public API +-# +- +-class _PathParents(Sequence): +- """This object provides sequence-like access to the logical ancestors +- of a path. Don't try to construct it yourself.""" +- __slots__ = ('_pathcls', '_drv', '_root', '_parts') +- +- def __init__(self, path): +- # We don't store the instance to avoid reference cycles +- self._pathcls = type(path) +- self._drv = path._drv +- self._root = path._root +- self._parts = path._parts +- +- def __len__(self): +- if self._drv or self._root: +- return len(self._parts) - 1 +- else: +- return len(self._parts) +- +- def __getitem__(self, idx): +- if idx < 0 or idx >= len(self): +- raise IndexError(idx) +- return self._pathcls._from_parsed_parts(self._drv, self._root, +- self._parts[:-idx - 1]) +- +- def __repr__(self): +- return "<{0}.parents>".format(self._pathcls.__name__) +- +- +-class PurePath(object): +- """PurePath represents a filesystem path and offers operations which +- don't imply any actual filesystem I/O. Depending on your system, +- instantiating a PurePath will return either a PurePosixPath or a +- PureWindowsPath object. You can also instantiate either of these classes +- directly, regardless of your system. +- """ +- __slots__ = ( +- '_drv', '_root', '_parts', +- '_str', '_hash', '_pparts', '_cached_cparts', +- ) +- +- def __new__(cls, *args): +- """Construct a PurePath from one or several strings and or existing +- PurePath objects. The strings and path objects are combined so as +- to yield a canonicalized path, which is incorporated into the +- new PurePath object. +- """ +- if cls is PurePath: +- cls = PureWindowsPath if os.name == 'nt' else PurePosixPath +- return cls._from_parts(args) +- +- def __reduce__(self): +- # Using the parts tuple helps share interned path parts +- # when pickling related paths. +- return (self.__class__, tuple(self._parts)) +- +- @classmethod +- def _parse_args(cls, args): +- # This is useful when you don't want to create an instance, just +- # canonicalize some constructor arguments. +- parts = [] +- for a in args: +- if isinstance(a, PurePath): +- parts += a._parts +- elif isinstance(a, basestring): +- parts.append(a) +- else: +- raise TypeError( +- "argument should be a path or str object, not %r" +- % type(a)) +- return cls._flavour.parse_parts(parts) +- +- @classmethod +- def _from_parts(cls, args, init=True): +- # We need to call _parse_args on the instance, so as to get the +- # right flavour. +- self = object.__new__(cls) +- drv, root, parts = self._parse_args(args) +- self._drv = drv +- self._root = root +- self._parts = parts +- if init: +- self._init() +- return self +- +- @classmethod +- def _from_parsed_parts(cls, drv, root, parts, init=True): +- self = object.__new__(cls) +- self._drv = drv +- self._root = root +- self._parts = parts +- if init: +- self._init() +- return self +- +- @classmethod +- def _format_parsed_parts(cls, drv, root, parts): +- if drv or root: +- return drv + root + cls._flavour.join(parts[1:]) +- else: +- return cls._flavour.join(parts) +- +- def _init(self): +- # Overriden in concrete Path +- pass +- +- def _make_child(self, args): +- drv, root, parts = self._parse_args(args) +- drv, root, parts = self._flavour.join_parsed_parts( +- self._drv, self._root, self._parts, drv, root, parts) +- return self._from_parsed_parts(drv, root, parts) +- +- def __str__(self): +- """Return the string representation of the path, suitable for +- passing to system calls.""" +- try: +- return self._str +- except AttributeError: +- self._str = self._format_parsed_parts(self._drv, self._root, +- self._parts) or '.' +- return self._str +- +- def as_posix(self): +- """Return the string representation of the path with forward (/) +- slashes.""" +- f = self._flavour +- return str(self).replace(f.sep, '/') +- +- def __bytes__(self): +- """Return the bytes representation of the path. This is only +- recommended to use under Unix.""" +- if sys.version_info < (3, 2): +- raise NotImplementedError("needs Python 3.2 or later") +- return os.fsencode(str(self)) +- +- def __repr__(self): +- return "{0}({1!r})".format(self.__class__.__name__, self.as_posix()) +- +- def as_uri(self): +- """Return the path as a 'file' URI.""" +- if not self.is_absolute(): +- raise ValueError("relative path can't be expressed as a file URI") +- return self._flavour.make_uri(self) +- +- @property +- def _cparts(self): +- # Cached casefolded parts, for hashing and comparison +- try: +- return self._cached_cparts +- except AttributeError: +- self._cached_cparts = self._flavour.casefold_parts(self._parts) +- return self._cached_cparts +- +- def __eq__(self, other): +- if not isinstance(other, PurePath): +- return NotImplemented +- return self._cparts == other._cparts and self._flavour is other._flavour +- +- def __ne__(self, other): +- return not self == other +- +- def __hash__(self): +- try: +- return self._hash +- except AttributeError: +- self._hash = hash(tuple(self._cparts)) +- return self._hash +- +- def __lt__(self, other): +- if not isinstance(other, PurePath) or self._flavour is not other._flavour: +- return NotImplemented +- return self._cparts < other._cparts +- +- def __le__(self, other): +- if not isinstance(other, PurePath) or self._flavour is not other._flavour: +- return NotImplemented +- return self._cparts <= other._cparts +- +- def __gt__(self, other): +- if not isinstance(other, PurePath) or self._flavour is not other._flavour: +- return NotImplemented +- return self._cparts > other._cparts +- +- def __ge__(self, other): +- if not isinstance(other, PurePath) or self._flavour is not other._flavour: +- return NotImplemented +- return self._cparts >= other._cparts +- +- drive = property(attrgetter('_drv'), +- doc="""The drive prefix (letter or UNC path), if any.""") +- +- root = property(attrgetter('_root'), +- doc="""The root of the path, if any.""") +- +- @property +- def anchor(self): +- """The concatenation of the drive and root, or ''.""" +- anchor = self._drv + self._root +- return anchor +- +- @property +- def name(self): +- """The final path component, if any.""" +- parts = self._parts +- if len(parts) == (1 if (self._drv or self._root) else 0): +- return '' +- return parts[-1] +- +- @property +- def suffix(self): +- """The final component's last suffix, if any.""" +- name = self.name +- i = name.rfind('.') +- if 0 < i < len(name) - 1: +- return name[i:] +- else: +- return '' +- +- @property +- def suffixes(self): +- """A list of the final component's suffixes, if any.""" +- name = self.name +- if name.endswith('.'): +- return [] +- name = name.lstrip('.') +- return ['.' + suffix for suffix in name.split('.')[1:]] +- +- @property +- def stem(self): +- """The final path component, minus its last suffix.""" +- name = self.name +- i = name.rfind('.') +- if 0 < i < len(name) - 1: +- return name[:i] +- else: +- return name +- +- def with_name(self, name): +- """Return a new path with the file name changed.""" +- if not self.name: +- raise ValueError("%r has an empty name" % (self,)) +- return self._from_parsed_parts(self._drv, self._root, +- self._parts[:-1] + [name]) +- +- def with_suffix(self, suffix): +- """Return a new path with the file suffix changed (or added, if none).""" +- # XXX if suffix is None, should the current suffix be removed? +- drv, root, parts = self._flavour.parse_parts((suffix,)) +- if drv or root or len(parts) != 1: +- raise ValueError("Invalid suffix %r" % (suffix)) +- suffix = parts[0] +- if not suffix.startswith('.'): +- raise ValueError("Invalid suffix %r" % (suffix)) +- name = self.name +- if not name: +- raise ValueError("%r has an empty name" % (self,)) +- old_suffix = self.suffix +- if not old_suffix: +- name = name + suffix +- else: +- name = name[:-len(old_suffix)] + suffix +- return self._from_parsed_parts(self._drv, self._root, +- self._parts[:-1] + [name]) +- +- def relative_to(self, *other): +- """Return the relative path to another path identified by the passed +- arguments. If the operation is not possible (because this is not +- a subpath of the other path), raise ValueError. +- """ +- # For the purpose of this method, drive and root are considered +- # separate parts, i.e.: +- # Path('c:/').relative_to('c:') gives Path('/') +- # Path('c:/').relative_to('/') raise ValueError +- if not other: +- raise TypeError("need at least one argument") +- parts = self._parts +- drv = self._drv +- root = self._root +- if root: +- abs_parts = [drv, root] + parts[1:] +- else: +- abs_parts = parts +- to_drv, to_root, to_parts = self._parse_args(other) +- if to_root: +- to_abs_parts = [to_drv, to_root] + to_parts[1:] +- else: +- to_abs_parts = to_parts +- n = len(to_abs_parts) +- cf = self._flavour.casefold_parts +- if (root or drv) if n == 0 else cf(abs_parts[:n]) != cf(to_abs_parts): +- formatted = self._format_parsed_parts(to_drv, to_root, to_parts) +- raise ValueError("{!r} does not start with {!r}" +- .format(str(self), str(formatted))) +- return self._from_parsed_parts('', root if n == 1 else '', +- abs_parts[n:]) +- +- @property +- def parts(self): +- """An object providing sequence-like access to the +- components in the filesystem path.""" +- # We cache the tuple to avoid building a new one each time .parts +- # is accessed. XXX is this necessary? +- try: +- return self._pparts +- except AttributeError: +- self._pparts = tuple(self._parts) +- return self._pparts +- +- def joinpath(self, *args): +- """Combine this path with one or several arguments, and return a +- new path representing either a subpath (if all arguments are relative +- paths) or a totally different path (if one of the arguments is +- anchored). +- """ +- return self._make_child(args) +- +- def __truediv__(self, key): +- return self._make_child((key,)) +- +- def __rtruediv__(self, key): +- return self._from_parts([key] + self._parts) +- +- if sys.version_info < (3,): +- __div__ = __truediv__ +- __rdiv__ = __rtruediv__ +- +- @property +- def parent(self): +- """The logical parent of the path.""" +- drv = self._drv +- root = self._root +- parts = self._parts +- if len(parts) == 1 and (drv or root): +- return self +- return self._from_parsed_parts(drv, root, parts[:-1]) +- +- @property +- def parents(self): +- """A sequence of this path's logical parents.""" +- return _PathParents(self) +- +- def is_absolute(self): +- """True if the path is absolute (has both a root and, if applicable, +- a drive).""" +- if not self._root: +- return False +- return not self._flavour.has_drv or bool(self._drv) +- +- def is_reserved(self): +- """Return True if the path contains one of the special names reserved +- by the system, if any.""" +- return self._flavour.is_reserved(self._parts) +- +- def match(self, path_pattern): +- """ +- Return True if this path matches the given pattern. +- """ +- cf = self._flavour.casefold +- path_pattern = cf(path_pattern) +- drv, root, pat_parts = self._flavour.parse_parts((path_pattern,)) +- if not pat_parts: +- raise ValueError("empty pattern") +- if drv and drv != cf(self._drv): +- return False +- if root and root != cf(self._root): +- return False +- parts = self._cparts +- if drv or root: +- if len(pat_parts) != len(parts): +- return False +- pat_parts = pat_parts[1:] +- elif len(pat_parts) > len(parts): +- return False +- for part, pat in zip(reversed(parts), reversed(pat_parts)): +- if not fnmatch.fnmatchcase(part, pat): +- return False +- return True +- +- +-class PurePosixPath(PurePath): +- _flavour = _posix_flavour +- __slots__ = () +- +- +-class PureWindowsPath(PurePath): +- _flavour = _windows_flavour +- __slots__ = () +- +- +-# Filesystem-accessing classes +- +- +-class Path(PurePath): +- __slots__ = ( +- '_accessor', +- ) +- +- def __new__(cls, *args, **kwargs): +- if cls is Path: +- cls = WindowsPath if os.name == 'nt' else PosixPath +- self = cls._from_parts(args, init=False) +- if not self._flavour.is_supported: +- raise NotImplementedError("cannot instantiate %r on your system" +- % (cls.__name__,)) +- self._init() +- return self +- +- def _init(self, +- # Private non-constructor arguments +- template=None, +- ): +- if template is not None: +- self._accessor = template._accessor +- else: +- self._accessor = _normal_accessor +- +- def _make_child_relpath(self, part): +- # This is an optimization used for dir walking. `part` must be +- # a single part relative to this path. +- parts = self._parts + [part] +- return self._from_parsed_parts(self._drv, self._root, parts) +- +- def _opener(self, name, flags, mode=0o666): +- # A stub for the opener argument to built-in open() +- return self._accessor.open(self, flags, mode) +- +- def _raw_open(self, flags, mode=0o777): +- """ +- Open the file pointed by this path and return a file descriptor, +- as os.open() does. +- """ +- return self._accessor.open(self, flags, mode) +- +- # Public API +- +- @classmethod +- def cwd(cls): +- """Return a new path pointing to the current working directory +- (as returned by os.getcwd()). +- """ +- return cls(os.getcwd()) +- +- def iterdir(self): +- """Iterate over the files in this directory. Does not yield any +- result for the special paths '.' and '..'. +- """ +- for name in self._accessor.listdir(self): +- if name in ('.', '..'): +- # Yielding a path object for these makes little sense +- continue +- yield self._make_child_relpath(name) +- +- def glob(self, pattern): +- """Iterate over this subtree and yield all existing files (of any +- kind, including directories) matching the given pattern. +- """ +- pattern = self._flavour.casefold(pattern) +- drv, root, pattern_parts = self._flavour.parse_parts((pattern,)) +- if drv or root: +- raise NotImplementedError("Non-relative patterns are unsupported") +- selector = _make_selector(tuple(pattern_parts)) +- for p in selector.select_from(self): +- yield p +- +- def rglob(self, pattern): +- """Recursively yield all existing files (of any kind, including +- directories) matching the given pattern, anywhere in this subtree. +- """ +- pattern = self._flavour.casefold(pattern) +- drv, root, pattern_parts = self._flavour.parse_parts((pattern,)) +- if drv or root: +- raise NotImplementedError("Non-relative patterns are unsupported") +- selector = _make_selector(("**",) + tuple(pattern_parts)) +- for p in selector.select_from(self): +- yield p +- +- def absolute(self): +- """Return an absolute version of this path. This function works +- even if the path doesn't point to anything. +- +- No normalization is done, i.e. all '.' and '..' will be kept along. +- Use resolve() to get the canonical path to a file. +- """ +- # XXX untested yet! +- if self.is_absolute(): +- return self +- # FIXME this must defer to the specific flavour (and, under Windows, +- # use nt._getfullpathname()) +- obj = self._from_parts([os.getcwd()] + self._parts, init=False) +- obj._init(template=self) +- return obj +- +- def resolve(self): +- """ +- Make the path absolute, resolving all symlinks on the way and also +- normalizing it (for example turning slashes into backslashes under +- Windows). +- """ +- s = self._flavour.resolve(self) +- if s is None: +- # No symlink resolution => for consistency, raise an error if +- # the path doesn't exist or is forbidden +- self.stat() +- s = str(self.absolute()) +- # Now we have no symlinks in the path, it's safe to normalize it. +- normed = self._flavour.pathmod.normpath(s) +- obj = self._from_parts((normed,), init=False) +- obj._init(template=self) +- return obj +- +- def stat(self): +- """ +- Return the result of the stat() system call on this path, like +- os.stat() does. +- """ +- return self._accessor.stat(self) +- +- def owner(self): +- """ +- Return the login name of the file owner. +- """ +- import pwd +- return pwd.getpwuid(self.stat().st_uid).pw_name +- +- def group(self): +- """ +- Return the group name of the file gid. +- """ +- import grp +- return grp.getgrgid(self.stat().st_gid).gr_name +- +- def open(self, mode='r', buffering=-1, encoding=None, +- errors=None, newline=None): +- """ +- Open the file pointed by this path and return a file object, as +- the built-in open() function does. +- """ +- if sys.version_info >= (3, 3): +- return io.open(str(self), mode, buffering, encoding, errors, newline, +- opener=self._opener) +- else: +- return io.open(str(self), mode, buffering, encoding, errors, newline) +- +- def touch(self, mode=0o666, exist_ok=True): +- """ +- Create this file with the given access mode, if it doesn't exist. +- """ +- if exist_ok: +- # First try to bump modification time +- # Implementation note: GNU touch uses the UTIME_NOW option of +- # the utimensat() / futimens() functions. +- t = time.time() +- try: +- self._accessor.utime(self, (t, t)) +- except OSError: +- # Avoid exception chaining +- pass +- else: +- return +- flags = os.O_CREAT | os.O_WRONLY +- if not exist_ok: +- flags |= os.O_EXCL +- fd = self._raw_open(flags, mode) +- os.close(fd) +- +- def mkdir(self, mode=0o777, parents=False): +- if not parents: +- self._accessor.mkdir(self, mode) +- else: +- try: +- self._accessor.mkdir(self, mode) +- except OSError as e: +- if e.errno != ENOENT: +- raise +- self.parent.mkdir(parents=True) +- self._accessor.mkdir(self, mode) +- +- def chmod(self, mode): +- """ +- Change the permissions of the path, like os.chmod(). +- """ +- self._accessor.chmod(self, mode) +- +- def lchmod(self, mode): +- """ +- Like chmod(), except if the path points to a symlink, the symlink's +- permissions are changed, rather than its target's. +- """ +- self._accessor.lchmod(self, mode) +- +- def unlink(self): +- """ +- Remove this file or link. +- If the path is a directory, use rmdir() instead. +- """ +- self._accessor.unlink(self) +- +- def rmdir(self): +- """ +- Remove this directory. The directory must be empty. +- """ +- self._accessor.rmdir(self) +- +- def lstat(self): +- """ +- Like stat(), except if the path points to a symlink, the symlink's +- status information is returned, rather than its target's. +- """ +- return self._accessor.lstat(self) +- +- def rename(self, target): +- """ +- Rename this path to the given path. +- """ +- self._accessor.rename(self, target) +- +- def replace(self, target): +- """ +- Rename this path to the given path, clobbering the existing +- destination if it exists. +- """ +- if sys.version_info < (3, 3): +- raise NotImplementedError("replace() is only available " +- "with Python 3.3 and later") +- self._accessor.replace(self, target) +- +- def symlink_to(self, target, target_is_directory=False): +- """ +- Make this path a symlink pointing to the given path. +- Note the order of arguments (self, target) is the reverse of os.symlink's. +- """ +- self._accessor.symlink(target, self, target_is_directory) +- +- # Convenience functions for querying the stat results +- +- def exists(self): +- """ +- Whether this path exists. +- """ +- try: +- self.stat() +- except OSError as e: +- if e.errno != ENOENT: +- raise +- return False +- return True +- +- def is_dir(self): +- """ +- Whether this path is a directory. +- """ +- try: +- return S_ISDIR(self.stat().st_mode) +- except OSError as e: +- if e.errno != ENOENT: +- raise +- # Path doesn't exist or is a broken symlink +- # (see https://bitbucket.org/pitrou/pathlib/issue/12/) +- return False +- +- def is_file(self): +- """ +- Whether this path is a regular file (also True for symlinks pointing +- to regular files). +- """ +- try: +- return S_ISREG(self.stat().st_mode) +- except OSError as e: +- if e.errno != ENOENT: +- raise +- # Path doesn't exist or is a broken symlink +- # (see https://bitbucket.org/pitrou/pathlib/issue/12/) +- return False +- +- def is_symlink(self): +- """ +- Whether this path is a symbolic link. +- """ +- try: +- return S_ISLNK(self.lstat().st_mode) +- except OSError as e: +- if e.errno != ENOENT: +- raise +- # Path doesn't exist +- return False +- +- def is_block_device(self): +- """ +- Whether this path is a block device. +- """ +- try: +- return S_ISBLK(self.stat().st_mode) +- except OSError as e: +- if e.errno != ENOENT: +- raise +- # Path doesn't exist or is a broken symlink +- # (see https://bitbucket.org/pitrou/pathlib/issue/12/) +- return False +- +- def is_char_device(self): +- """ +- Whether this path is a character device. +- """ +- try: +- return S_ISCHR(self.stat().st_mode) +- except OSError as e: +- if e.errno != ENOENT: +- raise +- # Path doesn't exist or is a broken symlink +- # (see https://bitbucket.org/pitrou/pathlib/issue/12/) +- return False +- +- def is_fifo(self): +- """ +- Whether this path is a FIFO. +- """ +- try: +- return S_ISFIFO(self.stat().st_mode) +- except OSError as e: +- if e.errno != ENOENT: +- raise +- # Path doesn't exist or is a broken symlink +- # (see https://bitbucket.org/pitrou/pathlib/issue/12/) +- return False +- +- def is_socket(self): +- """ +- Whether this path is a socket. +- """ +- try: +- return S_ISSOCK(self.stat().st_mode) +- except OSError as e: +- if e.errno != ENOENT: +- raise +- # Path doesn't exist or is a broken symlink +- # (see https://bitbucket.org/pitrou/pathlib/issue/12/) +- return False +- +- +-class PosixPath(Path, PurePosixPath): +- __slots__ = () +- +-class WindowsPath(Path, PureWindowsPath): +- __slots__ = () +- +diff --git a/tasks/_vendor/six.py b/tasks/_vendor/six.py +deleted file mode 100644 +index 190c023..0000000 +--- a/tasks/_vendor/six.py ++++ /dev/null +@@ -1,868 +0,0 @@ +-"""Utilities for writing code that runs on Python 2 and 3""" +- +-# Copyright (c) 2010-2015 Benjamin Peterson +-# +-# Permission is hereby granted, free of charge, to any person obtaining a copy +-# of this software and associated documentation files (the "Software"), to deal +-# in the Software without restriction, including without limitation the rights +-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +-# copies of the Software, and to permit persons to whom the Software is +-# furnished to do so, subject to the following conditions: +-# +-# The above copyright notice and this permission notice shall be included in all +-# copies or substantial portions of the Software. +-# +-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-# SOFTWARE. +- +-from __future__ import absolute_import +- +-import functools +-import itertools +-import operator +-import sys +-import types +- +-__author__ = "Benjamin Peterson " +-__version__ = "1.10.0" +- +- +-# Useful for very coarse version differentiation. +-PY2 = sys.version_info[0] == 2 +-PY3 = sys.version_info[0] == 3 +-PY34 = sys.version_info[0:2] >= (3, 4) +- +-if PY3: +- string_types = str, +- integer_types = int, +- class_types = type, +- text_type = str +- binary_type = bytes +- +- MAXSIZE = sys.maxsize +-else: +- string_types = basestring, +- integer_types = (int, long) +- class_types = (type, types.ClassType) +- text_type = unicode +- binary_type = str +- +- if sys.platform.startswith("java"): +- # Jython always uses 32 bits. +- MAXSIZE = int((1 << 31) - 1) +- else: +- # It's possible to have sizeof(long) != sizeof(Py_ssize_t). +- class X(object): +- +- def __len__(self): +- return 1 << 31 +- try: +- len(X()) +- except OverflowError: +- # 32-bit +- MAXSIZE = int((1 << 31) - 1) +- else: +- # 64-bit +- MAXSIZE = int((1 << 63) - 1) +- del X +- +- +-def _add_doc(func, doc): +- """Add documentation to a function.""" +- func.__doc__ = doc +- +- +-def _import_module(name): +- """Import module, returning the module after the last dot.""" +- __import__(name) +- return sys.modules[name] +- +- +-class _LazyDescr(object): +- +- def __init__(self, name): +- self.name = name +- +- def __get__(self, obj, tp): +- result = self._resolve() +- setattr(obj, self.name, result) # Invokes __set__. +- try: +- # This is a bit ugly, but it avoids running this again by +- # removing this descriptor. +- delattr(obj.__class__, self.name) +- except AttributeError: +- pass +- return result +- +- +-class MovedModule(_LazyDescr): +- +- def __init__(self, name, old, new=None): +- super(MovedModule, self).__init__(name) +- if PY3: +- if new is None: +- new = name +- self.mod = new +- else: +- self.mod = old +- +- def _resolve(self): +- return _import_module(self.mod) +- +- def __getattr__(self, attr): +- _module = self._resolve() +- value = getattr(_module, attr) +- setattr(self, attr, value) +- return value +- +- +-class _LazyModule(types.ModuleType): +- +- def __init__(self, name): +- super(_LazyModule, self).__init__(name) +- self.__doc__ = self.__class__.__doc__ +- +- def __dir__(self): +- attrs = ["__doc__", "__name__"] +- attrs += [attr.name for attr in self._moved_attributes] +- return attrs +- +- # Subclasses should override this +- _moved_attributes = [] +- +- +-class MovedAttribute(_LazyDescr): +- +- def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): +- super(MovedAttribute, self).__init__(name) +- if PY3: +- if new_mod is None: +- new_mod = name +- self.mod = new_mod +- if new_attr is None: +- if old_attr is None: +- new_attr = name +- else: +- new_attr = old_attr +- self.attr = new_attr +- else: +- self.mod = old_mod +- if old_attr is None: +- old_attr = name +- self.attr = old_attr +- +- def _resolve(self): +- module = _import_module(self.mod) +- return getattr(module, self.attr) +- +- +-class _SixMetaPathImporter(object): +- +- """ +- A meta path importer to import six.moves and its submodules. +- +- This class implements a PEP302 finder and loader. It should be compatible +- with Python 2.5 and all existing versions of Python3 +- """ +- +- def __init__(self, six_module_name): +- self.name = six_module_name +- self.known_modules = {} +- +- def _add_module(self, mod, *fullnames): +- for fullname in fullnames: +- self.known_modules[self.name + "." + fullname] = mod +- +- def _get_module(self, fullname): +- return self.known_modules[self.name + "." + fullname] +- +- def find_module(self, fullname, path=None): +- if fullname in self.known_modules: +- return self +- return None +- +- def __get_module(self, fullname): +- try: +- return self.known_modules[fullname] +- except KeyError: +- raise ImportError("This loader does not know module " + fullname) +- +- def load_module(self, fullname): +- try: +- # in case of a reload +- return sys.modules[fullname] +- except KeyError: +- pass +- mod = self.__get_module(fullname) +- if isinstance(mod, MovedModule): +- mod = mod._resolve() +- else: +- mod.__loader__ = self +- sys.modules[fullname] = mod +- return mod +- +- def is_package(self, fullname): +- """ +- Return true, if the named module is a package. +- +- We need this method to get correct spec objects with +- Python 3.4 (see PEP451) +- """ +- return hasattr(self.__get_module(fullname), "__path__") +- +- def get_code(self, fullname): +- """Return None +- +- Required, if is_package is implemented""" +- self.__get_module(fullname) # eventually raises ImportError +- return None +- get_source = get_code # same as get_code +- +-_importer = _SixMetaPathImporter(__name__) +- +- +-class _MovedItems(_LazyModule): +- +- """Lazy loading of moved objects""" +- __path__ = [] # mark as package +- +- +-_moved_attributes = [ +- MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), +- MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), +- MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"), +- MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), +- MovedAttribute("intern", "__builtin__", "sys"), +- MovedAttribute("map", "itertools", "builtins", "imap", "map"), +- MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"), +- MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"), +- MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), +- MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"), +- MovedAttribute("reduce", "__builtin__", "functools"), +- MovedAttribute("shlex_quote", "pipes", "shlex", "quote"), +- MovedAttribute("StringIO", "StringIO", "io"), +- MovedAttribute("UserDict", "UserDict", "collections"), +- MovedAttribute("UserList", "UserList", "collections"), +- MovedAttribute("UserString", "UserString", "collections"), +- MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), +- MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), +- MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"), +- MovedModule("builtins", "__builtin__"), +- MovedModule("configparser", "ConfigParser"), +- MovedModule("copyreg", "copy_reg"), +- MovedModule("dbm_gnu", "gdbm", "dbm.gnu"), +- MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread"), +- MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), +- MovedModule("http_cookies", "Cookie", "http.cookies"), +- MovedModule("html_entities", "htmlentitydefs", "html.entities"), +- MovedModule("html_parser", "HTMLParser", "html.parser"), +- MovedModule("http_client", "httplib", "http.client"), +- MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), +- MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"), +- MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), +- MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), +- MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), +- MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), +- MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), +- MovedModule("cPickle", "cPickle", "pickle"), +- MovedModule("queue", "Queue"), +- MovedModule("reprlib", "repr"), +- MovedModule("socketserver", "SocketServer"), +- MovedModule("_thread", "thread", "_thread"), +- MovedModule("tkinter", "Tkinter"), +- MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), +- MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), +- MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), +- MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), +- MovedModule("tkinter_tix", "Tix", "tkinter.tix"), +- MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"), +- MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), +- MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), +- MovedModule("tkinter_colorchooser", "tkColorChooser", +- "tkinter.colorchooser"), +- MovedModule("tkinter_commondialog", "tkCommonDialog", +- "tkinter.commondialog"), +- MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), +- MovedModule("tkinter_font", "tkFont", "tkinter.font"), +- MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), +- MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", +- "tkinter.simpledialog"), +- MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"), +- MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"), +- MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"), +- MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), +- MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"), +- MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"), +-] +-# Add windows specific modules. +-if sys.platform == "win32": +- _moved_attributes += [ +- MovedModule("winreg", "_winreg"), +- ] +- +-for attr in _moved_attributes: +- setattr(_MovedItems, attr.name, attr) +- if isinstance(attr, MovedModule): +- _importer._add_module(attr, "moves." + attr.name) +-del attr +- +-_MovedItems._moved_attributes = _moved_attributes +- +-moves = _MovedItems(__name__ + ".moves") +-_importer._add_module(moves, "moves") +- +- +-class Module_six_moves_urllib_parse(_LazyModule): +- +- """Lazy loading of moved objects in six.moves.urllib_parse""" +- +- +-_urllib_parse_moved_attributes = [ +- MovedAttribute("ParseResult", "urlparse", "urllib.parse"), +- MovedAttribute("SplitResult", "urlparse", "urllib.parse"), +- MovedAttribute("parse_qs", "urlparse", "urllib.parse"), +- MovedAttribute("parse_qsl", "urlparse", "urllib.parse"), +- MovedAttribute("urldefrag", "urlparse", "urllib.parse"), +- MovedAttribute("urljoin", "urlparse", "urllib.parse"), +- MovedAttribute("urlparse", "urlparse", "urllib.parse"), +- MovedAttribute("urlsplit", "urlparse", "urllib.parse"), +- MovedAttribute("urlunparse", "urlparse", "urllib.parse"), +- MovedAttribute("urlunsplit", "urlparse", "urllib.parse"), +- MovedAttribute("quote", "urllib", "urllib.parse"), +- MovedAttribute("quote_plus", "urllib", "urllib.parse"), +- MovedAttribute("unquote", "urllib", "urllib.parse"), +- MovedAttribute("unquote_plus", "urllib", "urllib.parse"), +- MovedAttribute("urlencode", "urllib", "urllib.parse"), +- MovedAttribute("splitquery", "urllib", "urllib.parse"), +- MovedAttribute("splittag", "urllib", "urllib.parse"), +- MovedAttribute("splituser", "urllib", "urllib.parse"), +- MovedAttribute("uses_fragment", "urlparse", "urllib.parse"), +- MovedAttribute("uses_netloc", "urlparse", "urllib.parse"), +- MovedAttribute("uses_params", "urlparse", "urllib.parse"), +- MovedAttribute("uses_query", "urlparse", "urllib.parse"), +- MovedAttribute("uses_relative", "urlparse", "urllib.parse"), +-] +-for attr in _urllib_parse_moved_attributes: +- setattr(Module_six_moves_urllib_parse, attr.name, attr) +-del attr +- +-Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes +- +-_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"), +- "moves.urllib_parse", "moves.urllib.parse") +- +- +-class Module_six_moves_urllib_error(_LazyModule): +- +- """Lazy loading of moved objects in six.moves.urllib_error""" +- +- +-_urllib_error_moved_attributes = [ +- MovedAttribute("URLError", "urllib2", "urllib.error"), +- MovedAttribute("HTTPError", "urllib2", "urllib.error"), +- MovedAttribute("ContentTooShortError", "urllib", "urllib.error"), +-] +-for attr in _urllib_error_moved_attributes: +- setattr(Module_six_moves_urllib_error, attr.name, attr) +-del attr +- +-Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes +- +-_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"), +- "moves.urllib_error", "moves.urllib.error") +- +- +-class Module_six_moves_urllib_request(_LazyModule): +- +- """Lazy loading of moved objects in six.moves.urllib_request""" +- +- +-_urllib_request_moved_attributes = [ +- MovedAttribute("urlopen", "urllib2", "urllib.request"), +- MovedAttribute("install_opener", "urllib2", "urllib.request"), +- MovedAttribute("build_opener", "urllib2", "urllib.request"), +- MovedAttribute("pathname2url", "urllib", "urllib.request"), +- MovedAttribute("url2pathname", "urllib", "urllib.request"), +- MovedAttribute("getproxies", "urllib", "urllib.request"), +- MovedAttribute("Request", "urllib2", "urllib.request"), +- MovedAttribute("OpenerDirector", "urllib2", "urllib.request"), +- MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"), +- MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"), +- MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"), +- MovedAttribute("ProxyHandler", "urllib2", "urllib.request"), +- MovedAttribute("BaseHandler", "urllib2", "urllib.request"), +- MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"), +- MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"), +- MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"), +- MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"), +- MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"), +- MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"), +- MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"), +- MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"), +- MovedAttribute("HTTPHandler", "urllib2", "urllib.request"), +- MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"), +- MovedAttribute("FileHandler", "urllib2", "urllib.request"), +- MovedAttribute("FTPHandler", "urllib2", "urllib.request"), +- MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"), +- MovedAttribute("UnknownHandler", "urllib2", "urllib.request"), +- MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"), +- MovedAttribute("urlretrieve", "urllib", "urllib.request"), +- MovedAttribute("urlcleanup", "urllib", "urllib.request"), +- MovedAttribute("URLopener", "urllib", "urllib.request"), +- MovedAttribute("FancyURLopener", "urllib", "urllib.request"), +- MovedAttribute("proxy_bypass", "urllib", "urllib.request"), +-] +-for attr in _urllib_request_moved_attributes: +- setattr(Module_six_moves_urllib_request, attr.name, attr) +-del attr +- +-Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes +- +-_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"), +- "moves.urllib_request", "moves.urllib.request") +- +- +-class Module_six_moves_urllib_response(_LazyModule): +- +- """Lazy loading of moved objects in six.moves.urllib_response""" +- +- +-_urllib_response_moved_attributes = [ +- MovedAttribute("addbase", "urllib", "urllib.response"), +- MovedAttribute("addclosehook", "urllib", "urllib.response"), +- MovedAttribute("addinfo", "urllib", "urllib.response"), +- MovedAttribute("addinfourl", "urllib", "urllib.response"), +-] +-for attr in _urllib_response_moved_attributes: +- setattr(Module_six_moves_urllib_response, attr.name, attr) +-del attr +- +-Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes +- +-_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"), +- "moves.urllib_response", "moves.urllib.response") +- +- +-class Module_six_moves_urllib_robotparser(_LazyModule): +- +- """Lazy loading of moved objects in six.moves.urllib_robotparser""" +- +- +-_urllib_robotparser_moved_attributes = [ +- MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"), +-] +-for attr in _urllib_robotparser_moved_attributes: +- setattr(Module_six_moves_urllib_robotparser, attr.name, attr) +-del attr +- +-Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes +- +-_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"), +- "moves.urllib_robotparser", "moves.urllib.robotparser") +- +- +-class Module_six_moves_urllib(types.ModuleType): +- +- """Create a six.moves.urllib namespace that resembles the Python 3 namespace""" +- __path__ = [] # mark as package +- parse = _importer._get_module("moves.urllib_parse") +- error = _importer._get_module("moves.urllib_error") +- request = _importer._get_module("moves.urllib_request") +- response = _importer._get_module("moves.urllib_response") +- robotparser = _importer._get_module("moves.urllib_robotparser") +- +- def __dir__(self): +- return ['parse', 'error', 'request', 'response', 'robotparser'] +- +-_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"), +- "moves.urllib") +- +- +-def add_move(move): +- """Add an item to six.moves.""" +- setattr(_MovedItems, move.name, move) +- +- +-def remove_move(name): +- """Remove item from six.moves.""" +- try: +- delattr(_MovedItems, name) +- except AttributeError: +- try: +- del moves.__dict__[name] +- except KeyError: +- raise AttributeError("no such move, %r" % (name,)) +- +- +-if PY3: +- _meth_func = "__func__" +- _meth_self = "__self__" +- +- _func_closure = "__closure__" +- _func_code = "__code__" +- _func_defaults = "__defaults__" +- _func_globals = "__globals__" +-else: +- _meth_func = "im_func" +- _meth_self = "im_self" +- +- _func_closure = "func_closure" +- _func_code = "func_code" +- _func_defaults = "func_defaults" +- _func_globals = "func_globals" +- +- +-try: +- advance_iterator = next +-except NameError: +- def advance_iterator(it): +- return it.next() +-next = advance_iterator +- +- +-try: +- callable = callable +-except NameError: +- def callable(obj): +- return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) +- +- +-if PY3: +- def get_unbound_function(unbound): +- return unbound +- +- create_bound_method = types.MethodType +- +- def create_unbound_method(func, cls): +- return func +- +- Iterator = object +-else: +- def get_unbound_function(unbound): +- return unbound.im_func +- +- def create_bound_method(func, obj): +- return types.MethodType(func, obj, obj.__class__) +- +- def create_unbound_method(func, cls): +- return types.MethodType(func, None, cls) +- +- class Iterator(object): +- +- def next(self): +- return type(self).__next__(self) +- +- callable = callable +-_add_doc(get_unbound_function, +- """Get the function out of a possibly unbound function""") +- +- +-get_method_function = operator.attrgetter(_meth_func) +-get_method_self = operator.attrgetter(_meth_self) +-get_function_closure = operator.attrgetter(_func_closure) +-get_function_code = operator.attrgetter(_func_code) +-get_function_defaults = operator.attrgetter(_func_defaults) +-get_function_globals = operator.attrgetter(_func_globals) +- +- +-if PY3: +- def iterkeys(d, **kw): +- return iter(d.keys(**kw)) +- +- def itervalues(d, **kw): +- return iter(d.values(**kw)) +- +- def iteritems(d, **kw): +- return iter(d.items(**kw)) +- +- def iterlists(d, **kw): +- return iter(d.lists(**kw)) +- +- viewkeys = operator.methodcaller("keys") +- +- viewvalues = operator.methodcaller("values") +- +- viewitems = operator.methodcaller("items") +-else: +- def iterkeys(d, **kw): +- return d.iterkeys(**kw) +- +- def itervalues(d, **kw): +- return d.itervalues(**kw) +- +- def iteritems(d, **kw): +- return d.iteritems(**kw) +- +- def iterlists(d, **kw): +- return d.iterlists(**kw) +- +- viewkeys = operator.methodcaller("viewkeys") +- +- viewvalues = operator.methodcaller("viewvalues") +- +- viewitems = operator.methodcaller("viewitems") +- +-_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.") +-_add_doc(itervalues, "Return an iterator over the values of a dictionary.") +-_add_doc(iteritems, +- "Return an iterator over the (key, value) pairs of a dictionary.") +-_add_doc(iterlists, +- "Return an iterator over the (key, [values]) pairs of a dictionary.") +- +- +-if PY3: +- def b(s): +- return s.encode("latin-1") +- +- def u(s): +- return s +- unichr = chr +- import struct +- int2byte = struct.Struct(">B").pack +- del struct +- byte2int = operator.itemgetter(0) +- indexbytes = operator.getitem +- iterbytes = iter +- import io +- StringIO = io.StringIO +- BytesIO = io.BytesIO +- _assertCountEqual = "assertCountEqual" +- if sys.version_info[1] <= 1: +- _assertRaisesRegex = "assertRaisesRegexp" +- _assertRegex = "assertRegexpMatches" +- else: +- _assertRaisesRegex = "assertRaisesRegex" +- _assertRegex = "assertRegex" +-else: +- def b(s): +- return s +- # Workaround for standalone backslash +- +- def u(s): +- return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape") +- unichr = unichr +- int2byte = chr +- +- def byte2int(bs): +- return ord(bs[0]) +- +- def indexbytes(buf, i): +- return ord(buf[i]) +- iterbytes = functools.partial(itertools.imap, ord) +- import StringIO +- StringIO = BytesIO = StringIO.StringIO +- _assertCountEqual = "assertItemsEqual" +- _assertRaisesRegex = "assertRaisesRegexp" +- _assertRegex = "assertRegexpMatches" +-_add_doc(b, """Byte literal""") +-_add_doc(u, """Text literal""") +- +- +-def assertCountEqual(self, *args, **kwargs): +- return getattr(self, _assertCountEqual)(*args, **kwargs) +- +- +-def assertRaisesRegex(self, *args, **kwargs): +- return getattr(self, _assertRaisesRegex)(*args, **kwargs) +- +- +-def assertRegex(self, *args, **kwargs): +- return getattr(self, _assertRegex)(*args, **kwargs) +- +- +-if PY3: +- exec_ = getattr(moves.builtins, "exec") +- +- def reraise(tp, value, tb=None): +- if value is None: +- value = tp() +- if value.__traceback__ is not tb: +- raise value.with_traceback(tb) +- raise value +- +-else: +- def exec_(_code_, _globs_=None, _locs_=None): +- """Execute code in a namespace.""" +- if _globs_ is None: +- frame = sys._getframe(1) +- _globs_ = frame.f_globals +- if _locs_ is None: +- _locs_ = frame.f_locals +- del frame +- elif _locs_ is None: +- _locs_ = _globs_ +- exec("""exec _code_ in _globs_, _locs_""") +- +- exec_("""def reraise(tp, value, tb=None): +- raise tp, value, tb +-""") +- +- +-if sys.version_info[:2] == (3, 2): +- exec_("""def raise_from(value, from_value): +- if from_value is None: +- raise value +- raise value from from_value +-""") +-elif sys.version_info[:2] > (3, 2): +- exec_("""def raise_from(value, from_value): +- raise value from from_value +-""") +-else: +- def raise_from(value, from_value): +- raise value +- +- +-print_ = getattr(moves.builtins, "print", None) +-if print_ is None: +- def print_(*args, **kwargs): +- """The new-style print function for Python 2.4 and 2.5.""" +- fp = kwargs.pop("file", sys.stdout) +- if fp is None: +- return +- +- def write(data): +- if not isinstance(data, basestring): +- data = str(data) +- # If the file has an encoding, encode unicode with it. +- if (isinstance(fp, file) and +- isinstance(data, unicode) and +- fp.encoding is not None): +- errors = getattr(fp, "errors", None) +- if errors is None: +- errors = "strict" +- data = data.encode(fp.encoding, errors) +- fp.write(data) +- want_unicode = False +- sep = kwargs.pop("sep", None) +- if sep is not None: +- if isinstance(sep, unicode): +- want_unicode = True +- elif not isinstance(sep, str): +- raise TypeError("sep must be None or a string") +- end = kwargs.pop("end", None) +- if end is not None: +- if isinstance(end, unicode): +- want_unicode = True +- elif not isinstance(end, str): +- raise TypeError("end must be None or a string") +- if kwargs: +- raise TypeError("invalid keyword arguments to print()") +- if not want_unicode: +- for arg in args: +- if isinstance(arg, unicode): +- want_unicode = True +- break +- if want_unicode: +- newline = unicode("\n") +- space = unicode(" ") +- else: +- newline = "\n" +- space = " " +- if sep is None: +- sep = space +- if end is None: +- end = newline +- for i, arg in enumerate(args): +- if i: +- write(sep) +- write(arg) +- write(end) +-if sys.version_info[:2] < (3, 3): +- _print = print_ +- +- def print_(*args, **kwargs): +- fp = kwargs.get("file", sys.stdout) +- flush = kwargs.pop("flush", False) +- _print(*args, **kwargs) +- if flush and fp is not None: +- fp.flush() +- +-_add_doc(reraise, """Reraise an exception.""") +- +-if sys.version_info[0:2] < (3, 4): +- def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS, +- updated=functools.WRAPPER_UPDATES): +- def wrapper(f): +- f = functools.wraps(wrapped, assigned, updated)(f) +- f.__wrapped__ = wrapped +- return f +- return wrapper +-else: +- wraps = functools.wraps +- +- +-def with_metaclass(meta, *bases): +- """Create a base class with a metaclass.""" +- # This requires a bit of explanation: the basic idea is to make a dummy +- # metaclass for one level of class instantiation that replaces itself with +- # the actual metaclass. +- class metaclass(meta): +- +- def __new__(cls, name, this_bases, d): +- return meta(name, bases, d) +- return type.__new__(metaclass, 'temporary_class', (), {}) +- +- +-def add_metaclass(metaclass): +- """Class decorator for creating a class with a metaclass.""" +- def wrapper(cls): +- orig_vars = cls.__dict__.copy() +- slots = orig_vars.get('__slots__') +- if slots is not None: +- if isinstance(slots, str): +- slots = [slots] +- for slots_var in slots: +- orig_vars.pop(slots_var) +- orig_vars.pop('__dict__', None) +- orig_vars.pop('__weakref__', None) +- return metaclass(cls.__name__, cls.__bases__, orig_vars) +- return wrapper +- +- +-def python_2_unicode_compatible(klass): +- """ +- A decorator that defines __unicode__ and __str__ methods under Python 2. +- Under Python 3 it does nothing. +- +- To support Python 2 and 3 with a single code base, define a __str__ method +- returning text and apply this decorator to the class. +- """ +- if PY2: +- if '__str__' not in klass.__dict__: +- raise ValueError("@python_2_unicode_compatible cannot be applied " +- "to %s because it doesn't define __str__()." % +- klass.__name__) +- klass.__unicode__ = klass.__str__ +- klass.__str__ = lambda self: self.__unicode__().encode('utf-8') +- return klass +- +- +-# Complete the moves implementation. +-# This code is at the end of this module to speed up module loading. +-# Turn this module into a package. +-__path__ = [] # required for PEP 302 and PEP 451 +-__package__ = __name__ # see PEP 366 @ReservedAssignment +-if globals().get("__spec__") is not None: +- __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable +-# Remove other six meta path importers, since they cause problems. This can +-# happen if six is removed from sys.modules and then reloaded. (Setuptools does +-# this for some reason.) +-if sys.meta_path: +- for i, importer in enumerate(sys.meta_path): +- # Here's some real nastiness: Another "instance" of the six module might +- # be floating around. Therefore, we can't use isinstance() to check for +- # the six meta path importer, since the other six instance will have +- # inserted an importer with different class. +- if (type(importer).__name__ == "_SixMetaPathImporter" and +- importer.name == __name__): +- del sys.meta_path[i] +- break +- del i, importer +-# Finally, add the importer to the meta path import hook. +-sys.meta_path.append(_importer) +diff --git a/tasks/docs.py b/tasks/docs.py +index 3360279..77c1d83 100644 +--- a/tasks/docs.py ++++ b/tasks/docs.py +@@ -11,7 +11,8 @@ from invoke.util import cd + from path import Path + + # -- TASK-LIBRARY: +-from ._tasklet_cleanup import cleanup_tasks, cleanup_dirs ++# PREPARED: from invoke_cleanup import cleanup_tasks, cleanup_dirs ++from .invoke_cleanup import cleanup_tasks, cleanup_dirs + + + # ----------------------------------------------------------------------------- +@@ -69,6 +70,7 @@ def build(ctx, builder="html", language=None, options=""): + opts=options) + ctx.run(command) + ++ + @task(help={ + "builder": "Builder to use (html, ...)", + "language": "Language to use (en, ...)", +@@ -81,12 +83,38 @@ def rebuild(ctx, builder="html", language=None, options=""): + clean(ctx) + build(ctx, builder=builder, language=None, options=options) + ++ ++@task(aliases=["auto", "watch"], ++ help={ ++ "builder": "Builder to use (html, ...)", ++ "language": "Language to use (en, ...)", ++ "options": "Additional options for sphinx-build", ++}) ++def autobuild(ctx, builder="html", language=None, options=""): ++ """Build docs with sphinx-build""" ++ language = _sphinxdoc_get_language(ctx, language) ++ sourcedir = ctx.config.sphinx.sourcedir ++ destdir = _sphinxdoc_get_destdir(ctx, builder, language=language) ++ destdir = destdir.abspath() ++ with cd(sourcedir): ++ destdir_relative = Path(".").relpathto(destdir) ++ command = "sphinx-autobuild {opts} -b {builder} -D language={language} {sourcedir} {destdir}" \ ++ .format(builder=builder, sourcedir=".", ++ destdir=destdir_relative, ++ language=language, ++ opts=options) ++ ctx.run(command) ++ ++ + @task + def linkcheck(ctx): + """Check if all links are corect.""" + build(ctx, builder="linkcheck") + +-@task(help={"language": "Language to use (en, ...)"}) ++ ++@task(aliases=["open"], ++ help={"language": "Language to use (en, ...)"} ++) + def browse(ctx, language=None): + """Open documentation in web browser.""" + output_dir = _sphinxdoc_get_destdir(ctx, "html", language=language) +@@ -182,6 +210,7 @@ def update_translation(ctx, language="all"): + # ----------------------------------------------------------------------------- + namespace = Collection(clean, rebuild, linkcheck, browse, save, update_translation) + namespace.add_task(build, default=True) ++namespace.add_task(autobuild) + namespace.configure({ + "sphinx": { + # -- FOR TASKS: docs.build, docs.rebuild, docs.clean, ... +diff --git a/tasks/invoke_cleanup.py b/tasks/invoke_cleanup.py +new file mode 100644 +index 0000000..4e631c4 +--- /dev/null ++++ b/tasks/invoke_cleanup.py +@@ -0,0 +1,447 @@ ++# -*- coding: UTF-8 -*- ++""" ++Provides cleanup tasks for invoke build scripts (as generic invoke tasklet). ++Simplifies writing common, composable and extendable cleanup tasks. ++ ++PYTHON PACKAGE DEPENDENCIES: ++ ++* path (python >= 3.5) or path.py >= 11.5.0 (as path-object abstraction) ++* pathlib (for ant-like wildcard patterns; since: python > 3.5) ++* pycmd (required-by: clean_python()) ++ ++ ++cleanup task: Add Additional Directories and Files to be removed ++------------------------------------------------------------------------------- ++ ++Create an invoke configuration file (YAML of JSON) with the additional ++configuration data: ++ ++.. code-block:: yaml ++ ++ # -- FILE: invoke.yaml ++ # USE: cleanup.directories, cleanup.files to override current configuration. ++ cleanup: ++ # directories: Default directory patterns (can be overwritten). ++ # files: Default file patterns (can be ovewritten). ++ extra_directories: ++ - **/tmp/ ++ extra_files: ++ - **/*.log ++ - **/*.bak ++ ++ ++Registration of Cleanup Tasks ++------------------------------ ++ ++Other task modules often have an own cleanup task to recover the clean state. ++The :meth:`cleanup` task, that is provided here, supports the registration ++of additional cleanup tasks. Therefore, when the :meth:`cleanup` task is executed, ++all registered cleanup tasks will be executed. ++ ++EXAMPLE:: ++ ++ # -- FILE: tasks/docs.py ++ from __future__ import absolute_import ++ from invoke import task, Collection ++ from invoke_cleanup import cleanup_tasks, cleanup_dirs ++ ++ @task ++ def clean(ctx): ++ "Cleanup generated documentation artifacts." ++ dry_run = ctx.config.run.dry ++ cleanup_dirs(["build/docs"], dry_run=dry_run) ++ ++ namespace = Collection(clean) ++ ... ++ ++ # -- REGISTER CLEANUP TASK: ++ cleanup_tasks.add_task(clean, "clean_docs") ++ cleanup_tasks.configure(namespace.configuration()) ++""" ++ ++from __future__ import absolute_import, print_function ++import os ++import sys ++from invoke import task, Collection ++from invoke.executor import Executor ++from invoke.exceptions import Exit, Failure, UnexpectedExit ++from invoke.util import cd ++from path import Path ++ ++# -- PYTHON BACKWARD COMPATIBILITY: ++python_version = sys.version_info[:2] ++python35 = (3, 5) # HINT: python3.8 does not raise OSErrors. ++if python_version < python35: # noqa ++ import pathlib2 as pathlib ++else: ++ import pathlib # noqa ++ ++ ++# ----------------------------------------------------------------------------- ++# CONSTANTS: ++# ----------------------------------------------------------------------------- ++VERSION = "0.3.6" ++ ++ ++# ----------------------------------------------------------------------------- ++# CLEANUP UTILITIES: ++# ----------------------------------------------------------------------------- ++def execute_cleanup_tasks(ctx, cleanup_tasks, workdir=".", verbose=False): ++ """Execute several cleanup tasks as part of the cleanup. ++ ++ :param ctx: Context object for the tasks. ++ :param cleanup_tasks: Collection of cleanup tasks (as Collection). ++ """ ++ # pylint: disable=redefined-outer-name ++ executor = Executor(cleanup_tasks, ctx.config) ++ failure_count = 0 ++ with cd(workdir) as cwd: ++ for cleanup_task in cleanup_tasks.tasks: ++ try: ++ print("CLEANUP TASK: %s" % cleanup_task) ++ executor.execute(cleanup_task) ++ except (Exit, Failure, UnexpectedExit) as e: ++ print(e) ++ print("FAILURE in CLEANUP TASK: %s (GRACEFULLY-IGNORED)" % cleanup_task) ++ failure_count += 1 ++ ++ if failure_count: ++ print("CLEANUP TASKS: %d failure(s) occured" % failure_count) ++ ++ ++def make_excluded(excluded, config_dir=None, workdir=None): ++ workdir = workdir or Path.getcwd() ++ config_dir = config_dir or workdir ++ workdir = Path(workdir) ++ config_dir = Path(config_dir) ++ ++ excluded2 = [] ++ for p in excluded: ++ assert p, "REQUIRE: non-empty" ++ p = Path(p) ++ if p.isabs(): ++ excluded2.append(p.normpath()) ++ else: ++ # -- RELATIVE PATH: ++ # Described relative to config_dir. ++ # Recompute it relative to current workdir. ++ p = Path(config_dir)/p ++ p = workdir.relpathto(p) ++ excluded2.append(p.normpath()) ++ excluded2.append(p.abspath()) ++ return set(excluded2) ++ ++ ++def is_directory_excluded(directory, excluded): ++ directory = Path(directory).normpath() ++ directory2 = directory.abspath() ++ if (directory in excluded) or (directory2 in excluded): ++ return True ++ # -- OTHERWISE: ++ return False ++ ++ ++def cleanup_dirs(patterns, workdir=".", excluded=None, ++ dry_run=False, verbose=False, show_skipped=False): ++ """Remove directories (and their contents) recursively. ++ Skips removal if directories does not exist. ++ ++ :param patterns: Directory name patterns, like "**/tmp*" (as list). ++ :param workdir: Current work directory (default=".") ++ :param dry_run: Dry-run mode indicator (as bool). ++ """ ++ excluded = excluded or [] ++ excluded = set([Path(p) for p in excluded]) ++ show_skipped = show_skipped or verbose ++ current_dir = Path(workdir) ++ python_basedir = Path(Path(sys.executable).dirname()).joinpath("..").abspath() ++ warn2_counter = 0 ++ for dir_pattern in patterns: ++ for directory in path_glob(dir_pattern, current_dir): ++ if is_directory_excluded(directory, excluded): ++ print("SKIP-DIR: %s (excluded)" % directory) ++ continue ++ directory2 = directory.abspath() ++ if sys.executable.startswith(directory2): ++ # -- PROTECT VIRTUAL ENVIRONMENT (currently in use): ++ # pylint: disable=line-too-long ++ print("SKIP-SUICIDE: '%s' contains current python executable" % directory) ++ continue ++ elif directory2.startswith(python_basedir): ++ # -- PROTECT VIRTUAL ENVIRONMENT (currently in use): ++ # HINT: Limit noise in DIAGNOSTIC OUTPUT to X messages. ++ if warn2_counter <= 4: # noqa ++ print("SKIP-SUICIDE: '%s'" % directory) ++ warn2_counter += 1 ++ continue ++ ++ if not directory.isdir(): ++ if show_skipped: ++ print("RMTREE: %s (SKIPPED: Not a directory)" % directory) ++ continue ++ ++ if dry_run: ++ print("RMTREE: %s (dry-run)" % directory) ++ else: ++ try: ++ # -- MAYBE: directory.rmtree(ignore_errors=True) ++ print("RMTREE: %s" % directory) ++ directory.rmtree_p() ++ except OSError as e: ++ print("RMTREE-FAILED: %s (for: %s)" % (e, directory)) ++ ++ ++def cleanup_files(patterns, workdir=".", dry_run=False, verbose=False, show_skipped=False): ++ """Remove files or files selected by file patterns. ++ Skips removal if file does not exist. ++ ++ :param patterns: File patterns, like "**/*.pyc" (as list). ++ :param workdir: Current work directory (default=".") ++ :param dry_run: Dry-run mode indicator (as bool). ++ """ ++ show_skipped = show_skipped or verbose ++ current_dir = Path(workdir) ++ python_basedir = Path(Path(sys.executable).dirname()).joinpath("..").abspath() ++ error_message = None ++ error_count = 0 ++ for file_pattern in patterns: ++ for file_ in path_glob(file_pattern, current_dir): ++ if file_.abspath().startswith(python_basedir): ++ # -- PROTECT VIRTUAL ENVIRONMENT (currently in use): ++ continue ++ if not file_.isfile(): ++ if show_skipped: ++ print("REMOVE: %s (SKIPPED: Not a file)" % file_) ++ continue ++ ++ if dry_run: ++ print("REMOVE: %s (dry-run)" % file_) ++ else: ++ print("REMOVE: %s" % file_) ++ try: ++ file_.remove_p() ++ except os.error as e: ++ message = "%s: %s" % (e.__class__.__name__, e) ++ print(message + " basedir: "+ python_basedir) ++ error_count += 1 ++ if not error_message: ++ error_message = message ++ if False and error_message: # noqa ++ class CleanupError(RuntimeError): ++ pass ++ raise CleanupError(error_message) ++ ++ ++def path_glob(pattern, current_dir=None): ++ """Use pathlib for ant-like patterns, like: "**/*.py" ++ ++ :param pattern: File/directory pattern to use (as string). ++ :param current_dir: Current working directory (as Path, pathlib.Path, str) ++ :return Resolved Path (as path.Path). ++ """ ++ if not current_dir: # noqa ++ current_dir = pathlib.Path.cwd() ++ elif not isinstance(current_dir, pathlib.Path): ++ # -- CASE: string, path.Path (string-like) ++ current_dir = pathlib.Path(str(current_dir)) ++ ++ pattern_path = Path(pattern) ++ if pattern_path.isabs(): ++ # -- SPECIAL CASE: Path.glob() only supports relative-path(s) / pattern(s). ++ if pattern_path.isdir(): ++ yield pattern_path ++ return ++ ++ # -- HINT: OSError is no longer raised in pathlib2 or python35.pathlib ++ # try: ++ for p in current_dir.glob(pattern): ++ yield Path(str(p)) ++ # except OSError as e: ++ # # -- CORNER-CASE 1: x.glob(pattern) may fail with: ++ # # OSError: [Errno 13] Permission denied: ++ # # HINT: Directory lacks excutable permissions for traversal. ++ # # -- CORNER-CASE 2: symlinked endless loop ++ # # OSError: [Errno 62] Too many levels of symbolic links: ++ # print("{0}: {1}".format(e.__class__.__name__, e)) ++ ++ ++# ----------------------------------------------------------------------------- ++# GENERIC CLEANUP TASKS: ++# ----------------------------------------------------------------------------- ++@task(help={ ++ "workdir": "Directory to clean(up) (default: $CWD).", ++ "verbose": "Enable verbose mode (default: OFF).", ++}) ++def clean(ctx, workdir=".", verbose=False): ++ """Cleanup temporary dirs/files to regain a clean state.""" ++ dry_run = ctx.config.run.dry ++ config_dir = getattr(ctx.config, "config_dir", workdir) ++ directories = list(ctx.config.cleanup.directories or []) ++ directories.extend(ctx.config.cleanup.extra_directories or []) ++ files = list(ctx.config.cleanup.files or []) ++ files.extend(ctx.config.cleanup.extra_files or []) ++ excluded_directories = list(ctx.config.cleanup.excluded_directories or []) ++ excluded_directories = make_excluded(excluded_directories, ++ config_dir=config_dir, workdir=".") ++ ++ # -- PERFORM CLEANUP: ++ execute_cleanup_tasks(ctx, cleanup_tasks) ++ cleanup_dirs(directories, workdir=workdir, excluded=excluded_directories, ++ dry_run=dry_run, verbose=verbose) ++ cleanup_files(files, workdir=workdir, dry_run=dry_run, verbose=verbose) ++ ++ # -- CONFIGURABLE EXTENSION-POINT: ++ # use_cleanup_python = ctx.config.cleanup.use_cleanup_python or False ++ # if use_cleanup_python: ++ # clean_python(ctx) ++ ++ ++@task(name="all", aliases=("distclean",), ++ help={ ++ "workdir": "Directory to clean(up) (default: $CWD).", ++ "verbose": "Enable verbose mode (default: OFF).", ++}) ++def clean_all(ctx, workdir=".", verbose=False): ++ """Clean up everything, even the precious stuff. ++ NOTE: clean task is executed last. ++ """ ++ dry_run = ctx.config.run.dry ++ config_dir = getattr(ctx.config, "config_dir", workdir) ++ directories = list(ctx.config.cleanup_all.directories or []) ++ directories.extend(ctx.config.cleanup_all.extra_directories or []) ++ files = list(ctx.config.cleanup_all.files or []) ++ files.extend(ctx.config.cleanup_all.extra_files or []) ++ excluded_directories = list(ctx.config.cleanup_all.excluded_directories or []) ++ excluded_directories.extend(ctx.config.cleanup.excluded_directories or []) ++ excluded_directories = make_excluded(excluded_directories, ++ config_dir=config_dir, workdir=".") ++ ++ # -- PERFORM CLEANUP: ++ # HINT: Remove now directories, files first before cleanup-tasks. ++ cleanup_dirs(directories, workdir=workdir, excluded=excluded_directories, ++ dry_run=dry_run, verbose=verbose) ++ cleanup_files(files, workdir=workdir, dry_run=dry_run, verbose=verbose) ++ execute_cleanup_tasks(ctx, cleanup_all_tasks) ++ clean(ctx, workdir=workdir, verbose=verbose) ++ ++ # -- CONFIGURABLE EXTENSION-POINT: ++ # use_cleanup_python1 = ctx.config.cleanup.use_cleanup_python or False ++ # use_cleanup_python2 = ctx.config.cleanup_all.use_cleanup_python or False ++ # if use_cleanup_python2 and not use_cleanup_python1: ++ # clean_python(ctx) ++ ++ ++@task(aliases=["python"]) ++def clean_python(ctx, workdir=".", verbose=False): ++ """Cleanup python related files/dirs: *.pyc, *.pyo, ...""" ++ dry_run = ctx.config.run.dry or False ++ # MAYBE NOT: "**/__pycache__" ++ cleanup_dirs(["build", "dist", "*.egg-info", "**/__pycache__"], ++ workdir=workdir, dry_run=dry_run, verbose=verbose) ++ if not dry_run: ++ ctx.run("py.cleanup") ++ cleanup_files(["**/*.pyc", "**/*.pyo", "**/*$py.class"], ++ workdir=workdir, dry_run=dry_run, verbose=verbose) ++ ++ ++@task(help={ ++ "path": "Path to cleanup.", ++ "interactive": "Enable interactive mode.", ++ "force": "Enable force mode.", ++ "options": "Additional git-clean options", ++}) ++def git_clean(ctx, path=None, interactive=False, force=False, ++ dry_run=False, options=None): ++ """Perform git-clean command to cleanup the worktree of a git repository. ++ ++ BEWARE: This may remove any precious files that are not checked in. ++ WARNING: DANGEROUS COMMAND. ++ """ ++ args = [] ++ force = force or ctx.config.git_clean.force ++ path = path or ctx.config.git_clean.path or "." ++ interactive = interactive or ctx.config.git_clean.interactive ++ dry_run = dry_run or ctx.config.run.dry or ctx.config.git_clean.dry_run ++ ++ if interactive: ++ args.append("--interactive") ++ if force: ++ args.append("--force") ++ if dry_run: ++ args.append("--dry-run") ++ args.append(options or "") ++ args = " ".join(args).strip() ++ ++ ctx.run("git clean {options} {path}".format(options=args, path=path)) ++ ++ ++# ----------------------------------------------------------------------------- ++# TASK CONFIGURATION: ++# ----------------------------------------------------------------------------- ++CLEANUP_EMPTY_CONFIG = { ++ "directories": [], ++ "files": [], ++ "extra_directories": [], ++ "extra_files": [], ++ "excluded_directories": [], ++ "excluded_files": [], ++ "use_cleanup_python": False, ++} ++def make_cleanup_config(**kwargs): ++ config_data = CLEANUP_EMPTY_CONFIG.copy() ++ config_data.update(kwargs) ++ return config_data ++ ++ ++namespace = Collection(clean_all, clean_python) ++namespace.add_task(clean, default=True) ++namespace.add_task(git_clean) ++namespace.configure({ ++ "cleanup": make_cleanup_config( ++ files=["**/*.bak", "**/*.log", "**/*.tmp", "**/.DS_Store"], ++ excluded_directories=[".git", ".hg", ".bzr", ".svn"], ++ ), ++ "cleanup_all": make_cleanup_config( ++ directories=[".venv*", ".tox", "downloads", "tmp"], ++ ), ++ "git_clean": { ++ "interactive": True, ++ "force": False, ++ "path": ".", ++ "dry_run": False, ++ }, ++}) ++ ++ ++# -- EXTENSION-POINT: CLEANUP TASKS (called by: clean, clean_all task) ++# NOTE: Can be used by other tasklets to register cleanup tasks. ++cleanup_tasks = Collection("cleanup_tasks") ++cleanup_all_tasks = Collection("cleanup_all_tasks") ++ ++# -- EXTEND NORMAL CLEANUP-TASKS: ++# DISABLED: cleanup_tasks.add_task(clean_python) ++ ++# ----------------------------------------------------------------------------- ++# EXTENSION-POINT: CONFIGURATION HELPERS: Can be used from other task modules ++# ----------------------------------------------------------------------------- ++def config_add_cleanup_dirs(directories): ++ # pylint: disable=protected-access ++ the_cleanup_directories = namespace._configuration["cleanup"]["directories"] ++ the_cleanup_directories.extend(directories) ++ ++def config_add_cleanup_files(files): ++ # pylint: disable=protected-access ++ the_cleanup_files = namespace._configuration["cleanup"]["files"] ++ the_cleanup_files.extend(files) ++ # namespace.configure({"cleanup": {"files": files}}) ++ # print("DIAG cleanup.config.cleanup: %r" % namespace.configuration()) ++ ++def config_add_cleanup_all_dirs(directories): ++ # pylint: disable=protected-access ++ the_cleanup_directories = namespace._configuration["cleanup_all"]["directories"] ++ the_cleanup_directories.extend(directories) ++ ++def config_add_cleanup_all_files(files): ++ # pylint: disable=protected-access ++ the_cleanup_files = namespace._configuration["cleanup_all"]["files"] ++ the_cleanup_files.extend(files) +diff --git a/tasks/py.requirements.txt b/tasks/py.requirements.txt +index 9c82d11..ac19e94 100644 +--- a/tasks/py.requirements.txt ++++ b/tasks/py.requirements.txt +@@ -13,8 +13,8 @@ pycmd + six==1.15.0 + + # -- HINT: path.py => path (python-install-package was renamed for python3) +-path.py >= 11.5.0; python_version < '3.5' + path >= 13.1.0; python_version >= '3.5' ++path.py >= 11.5.0; python_version < '3.5' + + # -- PYTHON2 BACKPORTS: + pathlib; python_version <= '3.4' +diff --git a/tasks/release.py b/tasks/release.py +index dba85c8..e17a46f 100644 +--- a/tasks/release.py ++++ b/tasks/release.py +@@ -51,7 +51,7 @@ Configuration file for pypi repositories: + + from __future__ import absolute_import, print_function + from invoke import Collection, task +-from ._tasklet_cleanup import path_glob ++from .invoke_cleanup import path_glob + from ._dry_run import DryRunContext + + +diff --git a/tasks/test.py b/tasks/test.py +index bfa2d80..d6b4189 100644 +--- a/tasks/test.py ++++ b/tasks/test.py +@@ -9,7 +9,8 @@ import sys + from invoke import task, Collection + + # -- TASK-LIBRARY: +-from ._tasklet_cleanup import cleanup_tasks, cleanup_dirs, cleanup_files ++# PREPARED: from invoke_cleanup import cleanup_tasks, cleanup_dirs, cleanup_files ++from .invoke_cleanup import cleanup_tasks, cleanup_dirs, cleanup_files + + + # --------------------------------------------------------------------------- diff --git a/meta-python/recipes-devtools/python/python3-behave/0091-Docs-change-code-blocks-from-bash-to-console.patch b/meta-python/recipes-devtools/python/python3-behave/0091-Docs-change-code-blocks-from-bash-to-console.patch new file mode 100644 index 000000000..b849cc5e1 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0091-Docs-change-code-blocks-from-bash-to-console.patch @@ -0,0 +1,36 @@ +From 87f19edd9048494b22d7c18c737c54844b1a8138 Mon Sep 17 00:00:00 2001 +From: Daniel Lemm <61800298+ffe4@users.noreply.github.com> +Date: Fri, 26 Jun 2020 11:27:10 +0200 +Subject: [PATCH] Docs: change code blocks from bash to console + +--- + README.rst | 2 +- + docs/practical_tips.rst | 2 +- + 2 files changed, 2 insertions(+), 2 deletions(-) + +diff --git a/README.rst b/README.rst +index 4a905ab..22b0352 100644 +--- a/README.rst ++++ b/README.rst +@@ -76,7 +76,7 @@ In that directory create a file called "example_steps.py" containing: + + Run behave: + +-.. code-block:: bash ++.. code-block:: console + + $ behave + Feature: Showing off behave # features/example.feature:2 +diff --git a/docs/practical_tips.rst b/docs/practical_tips.rst +index b70569f..75bc736 100644 +--- a/docs/practical_tips.rst ++++ b/docs/practical_tips.rst +@@ -30,7 +30,7 @@ For example, if you want to use the feature files in the same directory for + testing the model layer and the UI layer, this can be done by using the + ``--stage`` option, like with: + +-.. code-block:: bash ++.. code-block:: console + + $ behave --stage=model features/ + $ behave --stage=ui features/ # NOTE: Normally used on a subset of features. diff --git a/meta-python/recipes-devtools/python/python3-behave/0092-Fix-typo-in-tutorial.patch b/meta-python/recipes-devtools/python/python3-behave/0092-Fix-typo-in-tutorial.patch new file mode 100644 index 000000000..69a82b784 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0092-Fix-typo-in-tutorial.patch @@ -0,0 +1,24 @@ +From b8c3a3f553f8f45342f366c98bde08c5d50b98ed Mon Sep 17 00:00:00 2001 +From: Alex McLarty +Date: Fri, 12 Jul 2019 08:22:31 +0100 +Subject: [PATCH] Fix typo in tutorial + +--- + docs/tutorial.rst | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/docs/tutorial.rst b/docs/tutorial.rst +index 04b2f63..27698a4 100644 +--- a/docs/tutorial.rst ++++ b/docs/tutorial.rst +@@ -157,8 +157,8 @@ basic actions. You may use a Scenario Outline to achieve this: + + Scenario Outline: Blenders + Given I put in a blender, +- when I switch the blender on +- then it should transform into ++ When I switch the blender on ++ Then it should transform into + + Examples: Amphibians + | thing | other thing | diff --git a/meta-python/recipes-devtools/python/python3-behave/0093-py.requirements-Use-PyHamcrest-2.0-for-python2.7.patch b/meta-python/recipes-devtools/python/python3-behave/0093-py.requirements-Use-PyHamcrest-2.0-for-python2.7.patch new file mode 100644 index 000000000..ef86a84c3 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0093-py.requirements-Use-PyHamcrest-2.0-for-python2.7.patch @@ -0,0 +1,80 @@ +From de1d5c096cabbbc63e597d1289350c4e33151f14 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Tue, 1 Dec 2020 23:19:51 +0100 +Subject: [PATCH] py.requirements: Use PyHamcrest < 2.0 for python2.7 + +--- + issue.features/py.requirements.txt | 3 ++- + py.requirements/ci.tox.txt | 6 ++++-- + py.requirements/ci.travis.txt | 7 +++++-- + py.requirements/testing.txt | 6 ++++-- + 4 files changed, 15 insertions(+), 7 deletions(-) + +diff --git a/issue.features/py.requirements.txt b/issue.features/py.requirements.txt +index 6e3cf83..f8a2f8d 100644 +--- a/issue.features/py.requirements.txt ++++ b/issue.features/py.requirements.txt +@@ -8,4 +8,5 @@ + # + # ============================================================================ + +-PyHamcrest == 2.0.2 ++PyHamcrest >= 2.0.2; python_version >= '3.0' ++PyHamcrest < 2.0; python_version < '3.0' +diff --git a/py.requirements/ci.tox.txt b/py.requirements/ci.tox.txt +index 20ae791..5bee524 100644 +--- a/py.requirements/ci.tox.txt ++++ b/py.requirements/ci.tox.txt +@@ -4,9 +4,11 @@ + + pytest < 5.0; python_version < '3.0' # pytest >= 4.2 + pytest >= 5.0; python_version >= '3.0' +-pytest-html >= 1.19.0 ++ ++pytest-html >= 1.19.0,<2.0 + mock >= 2.0 +-PyHamcrest == 2.0.2 ++PyHamcrest >= 2.0.2; python_version >= '3.0' ++PyHamcrest < 2.0; python_version < '3.0' + + # -- HINT: path.py => path (python-install-package was renamed for python3) + path.py >= 11.5.0; python_version < '3.5' +diff --git a/py.requirements/ci.travis.txt b/py.requirements/ci.travis.txt +index c69445c..372116a 100644 +--- a/py.requirements/ci.travis.txt ++++ b/py.requirements/ci.travis.txt +@@ -1,11 +1,14 @@ + # ============================================================================ + # PYTHON PACKAGE REQUIREMENTS FOR: behave -- ci.travis.txt + # ============================================================================ ++ + pytest < 5.0; python_version < '3.0' + pytest >= 5.0; python_version >= '3.0' +-pytest-html >= 1.19.0 ++ ++pytest-html >= 1.19.0,<2.0 + mock >= 2.0 +-PyHamcrest == 2.0.2 ++PyHamcrest >= 2.0.2; python_version >= '3.0' ++PyHamcrest < 2.0; python_version < '3.0' + + # -- NEEDED: By some tests (as proof of concept) + # NOTE: path.py-10.1 is required for python2.6 +diff --git a/py.requirements/testing.txt b/py.requirements/testing.txt +index d3bca18..fc8fd82 100644 +--- a/py.requirements/testing.txt ++++ b/py.requirements/testing.txt +@@ -6,9 +6,11 @@ + # PREPARED-FUTURE: behave4cmd0, behave4cmd + pytest < 5.0; python_version < '3.0' # pytest >= 4.2 + pytest >= 5.0; python_version >= '3.0' +-pytest-html >= 1.19.0 ++ ++pytest-html >= 1.19.0,<2.0 + mock >= 2.0 +-PyHamcrest == 2.0.2 ++PyHamcrest >= 2.0.2; python_version >= '3.0' ++PyHamcrest < 2.0; python_version < '3.0' + + # -- NEEDED: By some tests (as proof of concept) + # NOTE: path.py-10.1 is required for python2.6 diff --git a/meta-python/recipes-devtools/python/python3-behave/0094-UPDATE-PR-877-was-merged.patch b/meta-python/recipes-devtools/python/python3-behave/0094-UPDATE-PR-877-was-merged.patch new file mode 100644 index 000000000..d20deff2e --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0094-UPDATE-PR-877-was-merged.patch @@ -0,0 +1,21 @@ +From bce68e9ff14ebf7ee69ad7e75bf5f13ab475c462 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sat, 5 Dec 2020 00:33:22 +0100 +Subject: [PATCH] UPDATE: PR #877 was merged. + +--- + CHANGES.rst | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/CHANGES.rst b/CHANGES.rst +index d758364..4e20bb8 100644 +--- a/CHANGES.rst ++++ b/CHANGES.rst +@@ -58,6 +58,7 @@ MINOR: + + DOCUMENTATION: + ++* pull #877: docs: API reference - Capitalizing Step Keywords in example (provided by: Ibrian93) + * pull #731: Update links to Django docs (provided by: bittner) + * pull #722: DOC remove remaining pythonhosted links (provided by: leszekhanusz) + * pull #701: behave/runner.py docstrings (provided by: spitGlued) diff --git a/meta-python/recipes-devtools/python/python3-behave/0095-capitalizing-steps.patch b/meta-python/recipes-devtools/python/python3-behave/0095-capitalizing-steps.patch new file mode 100644 index 000000000..fcf7651d4 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0095-capitalizing-steps.patch @@ -0,0 +1,28 @@ +From da598db70063a591e065857cf5135070f6d85704 Mon Sep 17 00:00:00 2001 +From: Brian Icochea +Date: Sun, 15 Nov 2020 18:35:16 +0100 +Subject: [PATCH] capitalizing steps + +--- + docs/api.rst | 8 ++++---- + 1 file changed, 4 insertions(+), 4 deletions(-) + +diff --git a/docs/api.rst b/docs/api.rst +index 4763ad6..7463863 100644 +--- a/docs/api.rst ++++ b/docs/api.rst +@@ -74,10 +74,10 @@ the name of their preceding keyword, so given the following feature file: + .. code-block:: gherkin + + Given some known state +- and some other known state +- when some action is taken +- then some outcome is observed +- but some other outcome is not observed. ++ And some other known state ++ When some action is taken ++ Then some outcome is observed ++ But some other outcome is not observed. + + the first "and" step will be renamed internally to "given" and *behave* + will look for a step implementation decorated with either "given" or "step": diff --git a/meta-python/recipes-devtools/python/python3-behave/0096-FIX-invoke-task-develop.update-gherkin-that-aborted-.patch b/meta-python/recipes-devtools/python/python3-behave/0096-FIX-invoke-task-develop.update-gherkin-that-aborted-.patch new file mode 100644 index 000000000..05220be2f --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0096-FIX-invoke-task-develop.update-gherkin-that-aborted-.patch @@ -0,0 +1,56 @@ +From cdaa39ca2ac2ef603e94f26da575fa8dca160865 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Fri, 11 Dec 2020 20:51:44 +0100 +Subject: [PATCH] FIX: invoke-task develop.update-gherkin that aborted after + diff + +* Show now if "gherkin-languages.json" does not change +* Add --verbose option to show diff output if file has changed +--- + tasks/develop.py | 19 +++++++++++++++---- + 1 file changed, 15 insertions(+), 4 deletions(-) + +diff --git a/tasks/develop.py b/tasks/develop.py +index 9a21363..eb5fedd 100644 +--- a/tasks/develop.py ++++ b/tasks/develop.py +@@ -9,6 +9,7 @@ from invoke.util import cd + from path import Path + import requests + ++ + # ----------------------------------------------------------------------------- + # CONSTANTS: + # ----------------------------------------------------------------------------- +@@ -18,8 +19,8 @@ GHERKIN_LANGUAGES_URL = "https://raw.githubusercontent.com/cucumber/cucumber/mas + # ----------------------------------------------------------------------------- + # TASKS: + # ----------------------------------------------------------------------------- +-@task +-def update_gherkin(ctx, dry_run=False): ++@task(aliases=["update-languages"]) ++def update_gherkin(ctx, dry_run=False, verbose=False): + """Update "gherkin-languages.json" file from cucumber-repo. + + * Download "gherkin-languages.json" from cucumber repo +@@ -41,8 +42,18 @@ def update_gherkin(ctx, dry_run=False): + + print('Generating "i18n.py" ...') + ctx.run("./convert_gherkin-languages.py") +- ctx.run("diff i18n.py ../../behave/i18n.py") +- if not dry_run: ++ ++ # -- DIFF: Returns normally w/ non-zero exitcode => NEEDS: warn=True ++ languages_have_changed = False ++ result = ctx.run("diff i18n.py ../../behave/i18n.py", warn=True, hide=True) ++ languages_have_changed = not result.ok ++ if verbose and languages_have_changed: ++ # -- SHOW DIFF: ++ print(result.stdout) ++ ++ if not languages_have_changed: ++ print("NO_CHANGED: gherkin-languages.json") ++ elif not dry_run: + print("Updating behave/i18n.py ...") + Path("i18n.py").move("../../behave/i18n.py") + diff --git a/meta-python/recipes-devtools/python/python3-behave/0097-Test-against-PowerPC-CPU-support-Travis-867.patch b/meta-python/recipes-devtools/python/python3-behave/0097-Test-against-PowerPC-CPU-support-Travis-867.patch new file mode 100644 index 000000000..829eea2e4 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0097-Test-against-PowerPC-CPU-support-Travis-867.patch @@ -0,0 +1,22 @@ +From 6f9f088271161adf4ce7a4bdc840b16ddd95b39e Mon Sep 17 00:00:00 2001 +From: santosh653 <70637961+santosh653@users.noreply.github.com> +Date: Mon, 14 Dec 2020 12:05:50 -0500 +Subject: [PATCH] Test against PowerPC CPU support (Travis) (#867) + +Run test suite against both AMD and PowerPC architecture +--- + .travis.yml | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/.travis.yml b/.travis.yml +index 781a610..2b78d97 100644 +--- a/.travis.yml ++++ b/.travis.yml +@@ -1,3 +1,7 @@ ++arch: ++ - amd64 ++ - ppc64le ++ + language: python + sudo: false + dist: xenial # required for Python >= 3.7 diff --git a/meta-python/recipes-devtools/python/python3-behave/0098-Add-Context.attach-docs-and-test.patch b/meta-python/recipes-devtools/python/python3-behave/0098-Add-Context.attach-docs-and-test.patch new file mode 100644 index 000000000..ac0197202 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0098-Add-Context.attach-docs-and-test.patch @@ -0,0 +1,132 @@ +From e1be95bbc0080eacb20bc5b263a838784c2a1ea2 Mon Sep 17 00:00:00 2001 +From: Korijn van Golen +Date: Sat, 28 Nov 2020 11:39:28 +0100 +Subject: [PATCH] Add Context.attach, docs and test + +--- + behave/formatter/json.py | 6 +++-- + behave/runner.py | 11 +++++++++ + docs/formatters.rst | 18 ++++++++++++++ + features/formatter.json.feature | 42 +++++++++++++++++++++++++++++++++ + 4 files changed, 75 insertions(+), 2 deletions(-) + +diff --git a/behave/formatter/json.py b/behave/formatter/json.py +index 6da0d59..edfe3d7 100644 +--- a/behave/formatter/json.py ++++ b/behave/formatter/json.py +@@ -168,10 +168,12 @@ class JSONFormatter(Formatter): + self._step_index += 1 + + def embedding(self, mime_type, data): +- step = self.current_feature_element["steps"][-1] ++ step = self.current_feature_element["steps"][self._step_index] ++ if "embeddings" not in step: ++ step["embeddings"] = [] + step["embeddings"].append({ + "mime_type": mime_type, +- "data": base64.b64encode(data).replace("\n", ""), ++ "data": base64.b64encode(data).decode(self.stream.encoding or "utf-8"), + }) + + def eof(self): +diff --git a/behave/runner.py b/behave/runner.py +index d01bff0..c583caf 100644 +--- a/behave/runner.py ++++ b/behave/runner.py +@@ -475,6 +475,17 @@ class Context(object): + # -- AVOID DUPLICATES: + current_frame["@cleanups"].append(internal_cleanup_func) + ++ def attach(self, mime_type, data): ++ """Embeds data (e.g. a screenshot) in reports for all ++ formatters that support it, such as the JSON formatter. ++ ++ :param mime_type: MIME type of the binary data. ++ :param data: Bytes-like object to embed. ++ """ ++ is_compatible = lambda f: hasattr(f, "embedding") ++ for formatter in filter(is_compatible, self._runner.formatters): ++ formatter.embedding(mime_type, data) ++ + + @contextlib.contextmanager + def use_context_with_mode(context, mode): +diff --git a/docs/formatters.rst b/docs/formatters.rst +index a40fd8d..534468a 100644 +--- a/docs/formatters.rst ++++ b/docs/formatters.rst +@@ -116,3 +116,21 @@ teamcity :pypi:`behave-teamcity`, a formatter for Jetbrains TeamCity CI te + [behave.formatters] + allure = allure_behave.formatter:AllureFormatter + teamcity = behave_teamcity:TeamcityFormatter ++ ++ ++Embedding data (e.g. screenshots) in reports ++------------------------------------------------------------------------------ ++ ++You can embed data in reports with the :class:`~behave.runner.Context` method ++:func:`~behave.runner.Context.attach`, if you have configured a formatter that ++supports it. Currently only the JSON formatter supports embedding data. ++ ++For example: ++ ++.. code-block:: python ++ ++ @when(u'I open the Google webpage') ++ def step_impl(context): ++ context.browser.get('http://www.google.com') ++ img = context.browser.get_full_page_screenshot_as_png() ++ context.attach("image/png", img) +diff --git a/features/formatter.json.feature b/features/formatter.json.feature +index 96b28c7..67c97ae 100644 +--- a/features/formatter.json.feature ++++ b/features/formatter.json.feature +@@ -309,6 +309,48 @@ Feature: JSON Formatter + But note that "both matched arguments.values are provided as string" + + ++ Scenario: Use JSON formatter and embed binary data in report from two steps ++ Given a file named "features/json_embeddings.feature" with: ++ """ ++ Feature: ++ Scenario: Use embeddings ++ Given "foobar" as plain text ++ And "red" as plain text ++ """ ++ And a file named "features/steps/json_embeddings_steps.py" with: ++ """ ++ from behave import step ++ ++ @step('"{data}" as plain text') ++ def step_string(context, data): ++ context.attach("text/plain", data.encode("utf-8")) ++ """ ++ When I run "behave -f json.pretty features/json_embeddings.feature" ++ Then it should pass with: ++ """ ++ 1 feature passed, 0 failed, 0 skipped ++ 1 scenario passed, 0 failed, 0 skipped ++ """ ++ And the command output should contain: ++ """ ++ "embeddings": [ ++ { ++ "data": "Zm9vYmFy", ++ "mime_type": "text/plain" ++ } ++ ], ++ """ ++ And the command output should contain: ++ """ ++ "embeddings": [ ++ { ++ "data": "cmVk", ++ "mime_type": "text/plain" ++ } ++ ], ++ """ ++ ++ + @xfail + @regression_problem.with_duration + Scenario: Use JSON formatter with feature and one scenario with steps diff --git a/meta-python/recipes-devtools/python/python3-behave/0099-Add-Contributing-chapter.patch b/meta-python/recipes-devtools/python/python3-behave/0099-Add-Contributing-chapter.patch new file mode 100644 index 000000000..63b4e7ce5 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0099-Add-Contributing-chapter.patch @@ -0,0 +1,125 @@ +From 5838eafe65cf0ebcda3d192318015326a8afb52a Mon Sep 17 00:00:00 2001 +From: Peter Bittner +Date: Sat, 12 Dec 2020 19:45:26 +0100 +Subject: [PATCH] Add Contributing chapter + +--- + docs/conf.py | 2 +- + docs/contributing.rst | 82 +++++++++++++++++++++++++++++++++++++++++++ + docs/index.rst | 1 + + 3 files changed, 84 insertions(+), 1 deletion(-) + create mode 100644 docs/contributing.rst + +diff --git a/docs/conf.py b/docs/conf.py +index e55fb21..1579a36 100644 +--- a/docs/conf.py ++++ b/docs/conf.py +@@ -108,7 +108,7 @@ if USE_SPHINX_INTERNATIONAL: + # ----------------------------------------------------------------------------- + project = u"behave" + authors = u"Jens Engel, Benno Rice and Richard Jones" +-copyright = u"2012-2019, %s" % authors ++copyright = u"2012-2020, %s" % authors + + # The version info for the project you're documenting, acts as replacement for + # |version| and |release|, also used in various other places throughout the +diff --git a/docs/contributing.rst b/docs/contributing.rst +new file mode 100644 +index 0000000..78f36bd +--- /dev/null ++++ b/docs/contributing.rst +@@ -0,0 +1,82 @@ ++Contributing ++============ ++ ++If you find a bug you can fix or want to contribute an enhancement you're ++welcome to `open an issue`_ on GitHub or create a `pull request`_ directly. ++ ++.. _open an issue: https://github.com/behave/behave/issues ++.. _pull request: https://github.com/behave/behave/pulls ++ ++Using ``invoke`` for Development ++-------------------------------- ++ ++For most development tasks we have `invoke`_ commands. ++ ++Install all requirements for running tasks using Pip, e.g. ++ ++.. code-block:: console ++ ++ python3 -m pip install -r tasks/py.requirements.txt ++ ++Display all available ``invoke`` commands like this: ++ ++.. code-block:: console ++ ++ invoke -l ++ ++If you're curious, all ``invoke`` tasks are located in the ``tasks/`` ++folder. ++ ++.. _invoke: https://www.pyinvoke.org/ ++ ++Update Gherkin Language Specification ++------------------------------------- ++ ++An ``invoke`` command will download the latest Gherkin language ++specification and update the `behave/i18n.py`_ module: ++ ++.. code-block:: console ++ ++ invoke develop.update-gherkin ++ ++If there were changes this command will have updated two files: ++ ++#. ``etc/gherkin/gherkin-languages.json`` (original Cucumber JSON spec) ++#. ``behave/i18n.py`` (Python module generated from the JSON spec) ++ ++Put both under version control and open a PR to merge them. ++ ++.. _behave/i18n.py: ++ https://github.com/behave/behave/blob/master/behave/i18n.py ++ ++Update Documentation ++-------------------- ++ ++Our documentation is written in `reStructuredText`_, and built and hosted ++on `ReadTheDocs`_. Make your changes to the files in the ``docs/`` folder ++and build the documentation with: ++ ++.. code-block:: console ++ ++ invoke docs ++ ++or, alternatively, using Tox: ++ ++.. code-block:: console ++ ++ tox -e docs ++ ++.. hint:: ++ ++ Building the docs requires Sphinx and DocUtils. If your build fails ++ because those are missing, run: ++ ++ python3 -m pip install -r py.requirements/docs.txt ++ ++Once the docs are built successfully, ``sphinx`` will tell you where it ++generated the HTML output (typically ``build/docs/html``), which you can ++then inspect locally. ++ ++.. _reStructuredText: ++ https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html ++.. _ReadTheDocs: https://readthedocs.org/ +diff --git a/docs/index.rst b/docs/index.rst +index f0dd20e..e00079c 100644 +--- a/docs/index.rst ++++ b/docs/index.rst +@@ -43,6 +43,7 @@ Contents + comparison + new_and_noteworthy + more_info ++ contributing + appendix + + .. seealso:: diff --git a/meta-python/recipes-devtools/python/python3-behave/0100-Adapt-Tox-target-for-building-the-docs.patch b/meta-python/recipes-devtools/python/python3-behave/0100-Adapt-Tox-target-for-building-the-docs.patch new file mode 100644 index 000000000..4dd3996a3 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0100-Adapt-Tox-target-for-building-the-docs.patch @@ -0,0 +1,34 @@ +From 25c2601d1d8d8836c9673dcbbf852515297e926a Mon Sep 17 00:00:00 2001 +From: Peter Bittner +Date: Sat, 12 Dec 2020 19:46:29 +0100 +Subject: [PATCH] Adapt Tox target for building the docs + +This will generate the HTML docs in the same location as `invoke docs`. +--- + tox.ini | 8 ++------ + 1 file changed, 2 insertions(+), 6 deletions(-) + +diff --git a/tox.ini b/tox.ini +index b825921..8ccb58b 100644 +--- a/tox.ini ++++ b/tox.ini +@@ -77,12 +77,9 @@ setenv = + + + [testenv:docs] +-basepython= python2 + changedir = docs +-commands= +- sphinx-build -W -b html -D language=en -d {envtmpdir}/doctrees . {envtmpdir}/html/en +-deps= +- -r{toxinidir}/py.requirements/docs.txt ++commands = sphinx-build -W -b html -D language=en -d {toxinidir}/build/docs/doctrees . {toxinidir}/build/docs/html/en ++deps = -r{toxinidir}/py.requirements/docs.txt + + + [testenv:cleanroom2] +@@ -146,4 +143,3 @@ commands= + deps= + jit + {[testenv]deps} +- diff --git a/meta-python/recipes-devtools/python/python3-behave/0101-Mention-HTML-formatter-in-documentation.patch b/meta-python/recipes-devtools/python/python3-behave/0101-Mention-HTML-formatter-in-documentation.patch new file mode 100644 index 000000000..048e927bf --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0101-Mention-HTML-formatter-in-documentation.patch @@ -0,0 +1,83 @@ +From 3bfc68099983191d1bc095d9ff4491fff41157eb Mon Sep 17 00:00:00 2001 +From: Peter Bittner +Date: Tue, 15 Dec 2020 11:30:17 +0100 +Subject: [PATCH] Mention HTML formatter in documentation + +--- + docs/formatters.rst | 23 ++++++++++++----------- + 1 file changed, 12 insertions(+), 11 deletions(-) + +diff --git a/docs/formatters.rst b/docs/formatters.rst +index 534468a..6080fc4 100644 +--- a/docs/formatters.rst ++++ b/docs/formatters.rst +@@ -1,8 +1,8 @@ + .. _id.appendix.formatters: + +-============================================================================== ++======================== + Formatters and Reporters +-============================================================================== ++======================== + + :pypi:`behave` provides 2 different concepts for reporting results of a test run: + +@@ -15,7 +15,7 @@ The ``Reporter`` has a more coarse-grained API. + + + Reporters +------------------------------------------------------------------------------- ++--------- + + The following reporters are currently supported: + +@@ -28,7 +28,7 @@ summary Provides a summary of the test run. + + + Formatters +------------------------------------------------------------------------------- ++---------- + + The following formatters are currently supported: + +@@ -62,7 +62,7 @@ tags.location dry-run Shows tags and the location where they are used. + + + User-Defined Formatters +------------------------------------------------------------------------------- ++----------------------- + + Behave allows you to provide your own formatter (class):: + +@@ -96,16 +96,16 @@ to provide them. The formatter should use the attribute schema: + + + More Formatters +------------------------------------------------------------------------------- ++--------------- + +-The following formatters are currently known: ++The following contributed formatters are currently known: + + ============== ========================================================================= + Name Description + ============== ========================================================================= +-allure :pypi:`allure-behave`, an Allure formatter for behave: +- ``allure_behave.formatter:AllureFormatter`` +-teamcity :pypi:`behave-teamcity`, a formatter for Jetbrains TeamCity CI testruns ++allure :pypi:`allure-behave`, an Allure formatter for behave. ++html :pypi:`behave-html-formatter`, a simple HTML formatter for behave. ++teamcity :pypi:`behave-teamcity`, a formatter for JetBrains TeamCity CI testruns + with behave. + ============== ========================================================================= + +@@ -114,7 +114,8 @@ teamcity :pypi:`behave-teamcity`, a formatter for Jetbrains TeamCity CI te + # -- FILE: behave.ini + # FORMATTER ALIASES: behave -f allure ... + [behave.formatters] +- allure = allure_behave.formatter:AllureFormatter ++ allure = allure_behave.formatter:AllureFormatter ++ html = behave_html_formatter:HTMLFormatter + teamcity = behave_teamcity:TeamcityFormatter + + diff --git a/meta-python/recipes-devtools/python/python3-behave/0102-Use-console-highlighting-for-pip-install-docs.patch b/meta-python/recipes-devtools/python/python3-behave/0102-Use-console-highlighting-for-pip-install-docs.patch new file mode 100644 index 000000000..6b09b9360 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0102-Use-console-highlighting-for-pip-install-docs.patch @@ -0,0 +1,22 @@ +From 709c32be819afbbb7c7aecc392fac4986e9ae90f Mon Sep 17 00:00:00 2001 +From: Peter Bittner +Date: Tue, 15 Dec 2020 12:44:52 +0100 +Subject: [PATCH] Use console highlighting for `pip install` (docs) + +--- + docs/contributing.rst | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/docs/contributing.rst b/docs/contributing.rst +index 78f36bd..f3deb87 100644 +--- a/docs/contributing.rst ++++ b/docs/contributing.rst +@@ -71,6 +71,8 @@ or, alternatively, using Tox: + Building the docs requires Sphinx and DocUtils. If your build fails + because those are missing, run: + ++ .. code-block:: console ++ + python3 -m pip install -r py.requirements/docs.txt + + Once the docs are built successfully, ``sphinx`` will tell you where it diff --git a/meta-python/recipes-devtools/python/python3-behave/0103-docs-fix-simple-typo-tuorial-tutorial.patch b/meta-python/recipes-devtools/python/python3-behave/0103-docs-fix-simple-typo-tuorial-tutorial.patch new file mode 100644 index 000000000..f216c29a7 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0103-docs-fix-simple-typo-tuorial-tutorial.patch @@ -0,0 +1,52 @@ +From 7a122510a7d4843b5f8c64c0a34a807757184a26 Mon Sep 17 00:00:00 2001 +From: Tim Gates +Date: Sun, 27 Dec 2020 08:16:16 +1100 +Subject: [PATCH] docs: fix simple typo, tuorial -> tutorial + +There is a small typo in docs/more_info.rst. + +Should read `tutorial` rather than `tuorial`. +--- + docs/more_info.rst | 8 ++++---- + 1 file changed, 4 insertions(+), 4 deletions(-) + +diff --git a/docs/more_info.rst b/docs/more_info.rst +index d0b9fcd..0d87a9c 100644 +--- a/docs/more_info.rst ++++ b/docs/more_info.rst +@@ -66,7 +66,7 @@ Presentation Videos + * Benno Rice: `Making Your Application Behave`_ (30min), + 2012-08-12, PyCon Australia. + +-* Selenium: `First behave python tuorial with selenium`_ (8min), 2015-01-28, ++* Selenium: `First behave python tutorial with selenium`_ (8min), 2015-01-28, + http://www.seleniumframework.com/python-basic/first-behave-gherkin/ + + * Jessica Ingrasselino: `Automation with Python and Behave`_ (67min), 2015-12-16 +@@ -84,7 +84,7 @@ Presentation Videos + * Benno Rice: `Making Your Application Behave`_ (30min), + PyCon Australia, 2012-08-12 + +- * Selenium: `First behave python tuorial with selenium`_ (8min), 2015-01-28, ++ * Selenium: `First behave python tutorial with selenium`_ (8min), 2015-01-28, + http://www.seleniumframework.com/python-basic/first-behave-gherkin/ + + * Jessica Ingrasselino: `Automation with Python and Behave`_ (67min), 2015-12-16 +@@ -112,7 +112,7 @@ Presentation Videos + :width: 600 + :height: 400 + +- Selenium: `First behave python tuorial with selenium`_ ++ Selenium: `First behave python tutorial with selenium`_ + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :Date: 2015-01-28 +@@ -146,7 +146,7 @@ Presentation Videos + + + .. _`Making Your Application Behave`: https://www.youtube.com/watch?v=u8BOKuNkmhg +-.. _`First behave python tuorial with selenium`: https://www.youtube.com/watch?v=D24_QrGUCFk ++.. _`First behave python tutorial with selenium`: https://www.youtube.com/watch?v=D24_QrGUCFk + .. _`Automation with Python and Behave`: https://www.youtube.com/watch?v=e78c7h6DRDQ + .. _`Selenium Python Webdriver Tutorial - Behave (BDD)`: https://www.youtube.com/watch?v=mextSo0UExc + diff --git a/meta-python/recipes-devtools/python/python3-behave/0104-Add-support-for-python3.9-by-using-active-tags.patch b/meta-python/recipes-devtools/python/python3-behave/0104-Add-support-for-python3.9-by-using-active-tags.patch new file mode 100644 index 000000000..dcdbc4cf3 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0104-Add-support-for-python3.9-by-using-active-tags.patch @@ -0,0 +1,227 @@ +From 5c6fd7eaeba958759ccedaaa483fe213859cfa6d Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Mon, 12 Oct 2020 21:52:25 +0200 +Subject: [PATCH] Add support for python3.9 (by using active-tags). + +--- + features/step.async_steps.feature | 21 +++++++++++++++++++++ + issue.features/issue0330.feature | 6 ++++++ + issue.features/issue0446.feature | 4 ++++ + issue.features/issue0457.feature | 5 +++++ + issue.features/issue0657.feature | 3 +++ + 5 files changed, 39 insertions(+) + +diff --git a/features/step.async_steps.feature b/features/step.async_steps.feature +index 3a18fa9..06709d9 100644 +--- a/features/step.async_steps.feature ++++ b/features/step.async_steps.feature +@@ -32,6 +32,9 @@ Feature: Async-Test Support (async-step, ...) + + @use.with_python.version=3.5 + @use.with_python.version=3.6 ++ @use.with_python.version=3.7 ++ @use.with_python.version=3.8 ++ @use.with_python.version=3.9 + Scenario: Use async-step with @async_run_until_complete (async) + Given a new working directory + And a file named "features/steps/async_steps35.py" with: +@@ -63,6 +66,9 @@ Feature: Async-Test Support (async-step, ...) + @use.with_python.version=3.4 + @use.with_python.version=3.5 + @use.with_python.version=3.6 ++ @use.with_python.version=3.7 ++ @use.with_python.version=3.8 ++ @use.with_python.version=3.9 + Scenario: Use async-step with @async_run_until_complete (@coroutine) + Given a new working directory + And a file named "features/steps/async_steps34.py" with: +@@ -93,6 +99,9 @@ Feature: Async-Test Support (async-step, ...) + + @use.with_python.version=3.5 + @use.with_python.version=3.6 ++ @use.with_python.version=3.7 ++ @use.with_python.version=3.8 ++ @use.with_python.version=3.9 + Scenario: Use @async_run_until_complete(timeout=...) and TIMEOUT occurs (async) + Given a new working directory + And a file named "features/steps/async_steps_timeout35.py" with: +@@ -128,6 +137,9 @@ Feature: Async-Test Support (async-step, ...) + + @use.with_python.version=3.5 + @use.with_python.version=3.6 ++ @use.with_python.version=3.7 ++ @use.with_python.version=3.8 ++ @use.with_python.version=3.9 + @async_step_fails + Scenario: Use @async_run_until_complete and async-step fails + Given a new working directory +@@ -170,6 +182,9 @@ Feature: Async-Test Support (async-step, ...) + + @use.with_python.version=3.5 + @use.with_python.version=3.6 ++ @use.with_python.version=3.7 ++ @use.with_python.version=3.8 ++ @use.with_python.version=3.9 + @async_step_fails + Scenario: Use @async_run_until_complete and async-step raises error + Given a new working directory +@@ -213,6 +228,9 @@ Feature: Async-Test Support (async-step, ...) + @use.with_python.version=3.4 + @use.with_python.version=3.5 + @use.with_python.version=3.6 ++ @use.with_python.version=3.7 ++ @use.with_python.version=3.8 ++ @use.with_python.version=3.9 + Scenario: Use @async_run_until_complete(timeout=...) and TIMEOUT occurs (@coroutine) + Given a new working directory + And a file named "features/steps/async_steps_timeout34.py" with: +@@ -250,6 +268,9 @@ Feature: Async-Test Support (async-step, ...) + @use.with_python.version=3.4 + @use.with_python.version=3.5 + @use.with_python.version=3.6 ++ @use.with_python.version=3.7 ++ @use.with_python.version=3.8 ++ @use.with_python.version=3.9 + Scenario: Use async-dispatch and async-collect concepts + Given a new working directory + And a file named "features/steps/async_dispatch_steps.py" with: +diff --git a/issue.features/issue0330.feature b/issue.features/issue0330.feature +index 81cb6e2..be4d378 100644 +--- a/issue.features/issue0330.feature ++++ b/issue.features/issue0330.feature +@@ -71,6 +71,7 @@ Feature: Issue #330: Skipped scenarios are included in junit reports when --no-s + + + @not.with_python.version=3.8 ++ @not.with_python.version=3.9 + Scenario: Junit report for skipped feature is created with --show-skipped + When I run "behave --junit -t @tag1 --show-skipped @alice_and_bob.featureset" + Then it should pass with: +@@ -85,6 +86,7 @@ Feature: Issue #330: Skipped scenarios are included in junit reports when --no-s + """ + + @use.with_python.version=3.8 ++ @use.with_python.version=3.9 + Scenario: Junit report for skipped feature is created with --show-skipped + When I run "behave --junit -t @tag1 --show-skipped @alice_and_bob.featureset" + Then it should pass with: +@@ -101,6 +103,7 @@ Feature: Issue #330: Skipped scenarios are included in junit reports when --no-s + # + + @not.with_python.version=3.8 ++ @not.with_python.version=3.9 + Scenario: Junit report for skipped scenario is neither shown nor counted with --no-skipped + When I run "behave --junit -t @tag1 --no-skipped" + Then it should pass with: +@@ -121,6 +124,7 @@ Feature: Issue #330: Skipped scenarios are included in junit reports when --no-s + And note that "Charly2 is the skipped scenarion in charly.feature" + + @use.with_python.version=3.8 ++ @use.with_python.version=3.9 + Scenario: Junit report for skipped scenario is neither shown nor counted with --no-skipped + When I run "behave --junit -t @tag1 --no-skipped" + Then it should pass with: +@@ -144,6 +148,7 @@ Feature: Issue #330: Skipped scenarios are included in junit reports when --no-s + + + @not.with_python.version=3.8 ++ @not.with_python.version=3.9 + Scenario: Junit report for skipped scenario is shown and counted with --show-skipped + When I run "behave --junit -t @tag1 --show-skipped" + Then it should pass with: +@@ -165,6 +170,7 @@ Feature: Issue #330: Skipped scenarios are included in junit reports when --no-s + + + @use.with_python.version=3.8 ++ @use.with_python.version=3.9 + Scenario: Junit report for skipped scenario is shown and counted with --show-skipped + When I run "behave --junit -t @tag1 --show-skipped" + Then it should pass with: +diff --git a/issue.features/issue0446.feature b/issue.features/issue0446.feature +index 901bdec..12de37a 100644 +--- a/issue.features/issue0446.feature ++++ b/issue.features/issue0446.feature +@@ -59,6 +59,7 @@ Feature: Issue #446 -- Support scenario hook-errors with JUnitReporter + """ + + @not.with_python.version=3.8 ++ @not.with_python.version=3.9 + Scenario: Hook error in before_scenario() + When I run "behave -f plain --junit features/before_scenario_failure.feature" + Then it should fail with: +@@ -88,6 +89,7 @@ Feature: Issue #446 -- Support scenario hook-errors with JUnitReporter + + + @use.with_python.version=3.8 ++ @use.with_python.version=3.9 + Scenario: Hook error in before_scenario() + When I run "behave -f plain --junit features/before_scenario_failure.feature" + Then it should fail with: +@@ -121,6 +123,7 @@ Feature: Issue #446 -- Support scenario hook-errors with JUnitReporter + + + @not.with_python.version=3.8 ++ @not.with_python.version=3.9 + Scenario: Hook error in after_scenario() + When I run "behave -f plain --junit features/after_scenario_failure.feature" + Then it should fail with: +@@ -152,6 +155,7 @@ Feature: Issue #446 -- Support scenario hook-errors with JUnitReporter + + + @use.with_python.version=3.8 ++ @use.with_python.version=3.9 + Scenario: Hook error in after_scenario() + When I run "behave -f plain --junit features/after_scenario_failure.feature" + Then it should fail with: +diff --git a/issue.features/issue0457.feature b/issue.features/issue0457.feature +index 46f96e9..6d2f48f 100644 +--- a/issue.features/issue0457.feature ++++ b/issue.features/issue0457.feature +@@ -25,6 +25,7 @@ Feature: Issue #457 -- Double-quotes in error messages of JUnit XML reports + + + @not.with_python.version=3.8 ++ @not.with_python.version=3.9 + Scenario: Use failing assertation in a JUnit XML report + Given a file named "features/fails1.feature" with: + """ +@@ -46,6 +47,7 @@ Feature: Issue #457 -- Double-quotes in error messages of JUnit XML reports + """ + + @use.with_python.version=3.8 ++ @use.with_python.version=3.9 + Scenario: Use failing assertation in a JUnit XML report + Given a file named "features/fails1.feature" with: + """ +@@ -70,6 +72,7 @@ Feature: Issue #457 -- Double-quotes in error messages of JUnit XML reports + + + @not.with_python.version=3.8 ++ @not.with_python.version=3.9 + Scenario: Use exception in a JUnit XML report + Given a file named "features/fails2.feature" with: + """ +@@ -90,7 +93,9 @@ Feature: Issue #457 -- Double-quotes in error messages of JUnit XML reports + +Date: Mon, 12 Oct 2020 22:14:59 +0200 +Subject: [PATCH] PREFER: python3 from now on. + +--- + bin/behave | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/bin/behave b/bin/behave +index c02e8d3..9ebb584 100755 +--- a/bin/behave ++++ b/bin/behave +@@ -1,4 +1,4 @@ +-#!/usr/bin/env python ++#!/usr/bin/env python3 + # -*- coding: utf-8 -*- + + from __future__ import absolute_import diff --git a/meta-python/recipes-devtools/python/python3-behave/0106-REMOVE-invoke-scripts.patch b/meta-python/recipes-devtools/python/python3-behave/0106-REMOVE-invoke-scripts.patch new file mode 100644 index 000000000..cbbfe6d6b --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0106-REMOVE-invoke-scripts.patch @@ -0,0 +1,41 @@ +From c947c0726673bcb8d8d3114d0e39fc926040559e Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sat, 9 Jan 2021 21:52:04 +0100 +Subject: [PATCH] REMOVE: invoke scripts + +--- + bin/invoke | 8 -------- + bin/invoke.cmd | 9 --------- + 2 files changed, 17 deletions(-) + delete mode 100755 bin/invoke + delete mode 100644 bin/invoke.cmd + +diff --git a/bin/invoke b/bin/invoke +deleted file mode 100755 +index e9800e8..0000000 +--- a/bin/invoke ++++ /dev/null +@@ -1,8 +0,0 @@ +-#!/bin/sh +-#!/bin/bash +-# RUN INVOKE: From bundled ZIP file. +- +-HERE=$(dirname $0) +-export INVOKE_TASKS_USE_VENDOR_BUNDLES="yes" +- +-python ${HERE}/../tasks/_vendor/invoke.zip $* +diff --git a/bin/invoke.cmd b/bin/invoke.cmd +deleted file mode 100644 +index 9303432..0000000 +--- a/bin/invoke.cmd ++++ /dev/null +@@ -1,9 +0,0 @@ +-@echo off +-REM RUN INVOKE: From bundled ZIP file. +- +-setlocal +-set HERE=%~dp0 +-set INVOKE_TASKS_USE_VENDOR_BUNDLES="yes" +-if not defined PYTHON set PYTHON=python +- +-%PYTHON% %HERE%../tasks/_vendor/invoke.zip "%*" diff --git a/meta-python/recipes-devtools/python/python3-behave/0107-FIX-Deprecated-warnings-for-Python-3.x.patch b/meta-python/recipes-devtools/python/python3-behave/0107-FIX-Deprecated-warnings-for-Python-3.x.patch new file mode 100644 index 000000000..e4cb51acf --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0107-FIX-Deprecated-warnings-for-Python-3.x.patch @@ -0,0 +1,124 @@ +From 3fb11ef19d2265b6e29fc69191079b32c79225dd Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sat, 9 Jan 2021 21:52:42 +0100 +Subject: [PATCH] FIX: Deprecated warnings for Python 3.x + +--- + bin/json.format.py | 15 ++++++++++----- + bin/jsonschema_validate.py | 23 ++++++++++++++++------- + 2 files changed, 26 insertions(+), 12 deletions(-) + +diff --git a/bin/json.format.py b/bin/json.format.py +index b7eb05f..b75d88a 100755 +--- a/bin/json.format.py ++++ b/bin/json.format.py +@@ -10,8 +10,8 @@ LICENSE: BSD + 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 +@@ -29,6 +29,7 @@ except ImportError: + # CONSTANTS: + # ---------------------------------------------------------------------------- + DEFAULT_INDENT_SIZE = 2 ++PYTHON_VERSION = sys.version_info[:2] + + # ---------------------------------------------------------------------------- + # FUNCTIONS: +@@ -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 +@@ -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): +@@ -143,7 +148,7 @@ Format/Beautify one or more JSON file(s).""" + 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: +diff --git a/bin/jsonschema_validate.py b/bin/jsonschema_validate.py +index db2edb1..fe7596e 100755 +--- a/bin/jsonschema_validate.py ++++ b/bin/jsonschema_validate.py +@@ -18,11 +18,11 @@ from __future__ import absolute_import, print_function + __author__ = "Jens Engel" + __version__ = "0.1.0" + +-from jsonschema import validate + import argparse + import os.path + import sys + import textwrap ++from jsonschema import validate + try: + import json + except ImportError: +@@ -38,16 +38,28 @@ except ImportError: + HERE = os.path.dirname(__file__) + TOP = os.path.normpath(os.path.join(HERE, "..")) + SCHEMA = os.path.join(TOP, "etc", "json", "behave.json-schema") ++PYTHON_VERSION = sys.version_info[:2] + + + # ----------------------------------------------------------------------------- + # FUNCTIONS: + # ----------------------------------------------------------------------------- +-def jsonschema_validate(filename, schema, encoding=None): ++def json_loads(text, encoding=None): ++ kwargs = {} ++ if encoding and PYTHON_VERSION < (3, 1): ++ # -- NOTE: encoding keyword is deprecated since python 3.1 ++ kwargs["encoding"] = encoding ++ return json.loads(text, **kwargs) ++ ++def json_load(filename, encoding=None): + f = open(filename, "r") + contents = f.read() + f.close() +- data = json.loads(contents, encoding=encoding) ++ data = json_loads(contents, encoding=encoding) ++ return data ++ ++def jsonschema_validate(filename, schema, encoding=None): ++ data = json_load(filename, encoding=encoding) + return validate(data, schema) + + +@@ -89,10 +101,7 @@ def main(args=None): + parser.error("SCHEMA not found: %s" % options.schema) + + try: +- f = open(options.schema, "r") +- contents = f.read() +- f.close() +- schema = json.loads(contents, encoding=options.encoding) ++ schema = json_load(options.schema, encoding=options.encoding) + except Exception as e: + msg = "ERROR: %s: %s (while loading schema)" % (e.__class__.__name__, e) + sys.exit(msg) diff --git a/meta-python/recipes-devtools/python/python3-behave/0108-FIX-Python2-problems-in-virtualenvs-w-behave4cmd0-st.patch b/meta-python/recipes-devtools/python/python3-behave/0108-FIX-Python2-problems-in-virtualenvs-w-behave4cmd0-st.patch new file mode 100644 index 000000000..615d10bd6 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0108-FIX-Python2-problems-in-virtualenvs-w-behave4cmd0-st.patch @@ -0,0 +1,18 @@ +From 961e4f93259a0c1176a53a34d8e8de592c03db55 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sat, 9 Jan 2021 21:53:32 +0100 +Subject: [PATCH] FIX: Python2 problems in virtualenvs w/ behave4cmd0 steps. + +--- + bin/behave | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/bin/behave b/bin/behave +index 9ebb584..0f37dec 100755 +--- a/bin/behave ++++ b/bin/behave +@@ -1,3 +1,4 @@ ++#!/usr/bin/env python + #!/usr/bin/env python3 + # -*- coding: utf-8 -*- + diff --git a/meta-python/recipes-devtools/python/python3-behave/0109-FIX-Active-tag-logic.patch b/meta-python/recipes-devtools/python/python3-behave/0109-FIX-Active-tag-logic.patch new file mode 100644 index 000000000..d45aba3d4 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0109-FIX-Active-tag-logic.patch @@ -0,0 +1,875 @@ +From 2c38a8787409ddb50ff9ee93006d5e30a40110de Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sat, 9 Jan 2021 22:04:18 +0100 +Subject: [PATCH] FIX: Active-tag logic + +Fix active-tag computation logic if multiple active-tags exist +that use the same category. It is now possible to use +positive (use.with_xxx) and negative (not.with_xxx) tags +on the same model element (Feature, Scenario, ...). +--- + behave/api/runtime_constraint.py | 12 +- + behave/tag_matcher.py | 82 ++++- + features/tags.active_tags.feature | 22 +- + tests/functional/test_active_tags.py | 529 +++++++++++++++++++++++++++ + tests/unit/test_tag_matcher.py | 45 +-- + 5 files changed, 624 insertions(+), 66 deletions(-) + create mode 100644 tests/functional/test_active_tags.py + +diff --git a/behave/api/runtime_constraint.py b/behave/api/runtime_constraint.py +index 310e529..e5a36a0 100644 +--- a/behave/api/runtime_constraint.py ++++ b/behave/api/runtime_constraint.py +@@ -7,6 +7,8 @@ Simplifies to specify runtime constraints in + """ + + from __future__ import absolute_import ++import six ++import sys + from behave.exception import ConstraintError + + +@@ -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)) + +@@ -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)) +diff --git a/behave/tag_matcher.py b/behave/tag_matcher.py +index 5f9dce0..e2b1e82 100644 +--- a/behave/tag_matcher.py ++++ b/behave/tag_matcher.py +@@ -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 +@@ -10,6 +10,11 @@ import operator + 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.""" + +@@ -36,12 +41,13 @@ class TagMatcher(object): + 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 + ---------- +@@ -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 +@@ -81,12 +87,12 @@ class ActiveTagMatcher(TagMatcher): + # -- 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 + ... +@@ -116,7 +122,7 @@ class ActiveTagMatcher(TagMatcher): + 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%s)\.with_(?P\w+(\.\w+)*)%s(?P.*)$" + ignore_unknown_categories = True + use_exclude_reason = False +@@ -163,21 +169,49 @@ class ActiveTagMatcher(TagMatcher): + + 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. +@@ -191,20 +225,28 @@ class ActiveTagMatcher(TagMatcher): + # -- 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) +diff --git a/features/tags.active_tags.feature b/features/tags.active_tags.feature +index 4ab55c2..6adcb60 100644 +--- a/features/tags.active_tags.feature ++++ b/features/tags.active_tags.feature +@@ -240,9 +240,25 @@ Feature: Active Tags + | tags | enabled? | Comment | + | @use.with_foo=xxx @use.with_foo=other | yes | Enabled: tag1 | + | @use.with_foo=xxx @not.with_foo=other | yes | Enabled: tag1, tag2| +- | @use.with_foo=xxx @not.with_foo=xxx | yes | Enabled: tag1 (BAD-SPEC) | +- | @use.with_foo=other @not.with_foo=xxx | no | Enabled: none | +- | @not.with_foo=other @not.with_foo=xxx | yes | Enabled: tag1 | ++ | @use.with_foo=other @not.with_foo=xxx | no | Disabled: none | ++ | @not.with_foo=other @not.with_foo=xxx | no | Disabled: tag1 | ++ | @use.with_foo=xxx @not.with_foo=xxx | no | Disabled: tag1 (BAD-SPEC, CONFLICTS) | ++ ++ ++ Scenario: Tag logic with three active tags of same category ++ Given I setup the current values for active tags with: ++ | category | value | ++ | foo | xxx | ++ Then the following active tag combinations are enabled: ++ | tags | enabled? | Comment | ++ | @use.with_foo=xxx @use.with_foo=other @use.with_foo=other2| yes | Enabled: tag1 | ++ | @use.with_foo=xxx @use.with_foo=other @not.with_foo=other2| yes | Enabled: tag1 | ++ | @use.with_foo=xxx @not.with_foo=other @use.with_foo=other2| yes | Enabled: tag1 | ++ | @use.with_foo=xxx @not.with_foo=other @not.with_foo=other2| yes | Enabled: tag1 | ++ | @not.with_foo=xxx @use.with_foo=other @use.with_foo=other2| no | Disabled: tag1 | ++ | @not.with_foo=xxx @use.with_foo=other @not.with_foo=other2| no | Disabled: tag1 | ++ | @not.with_foo=xxx @not.with_foo=other @use.with_foo=other2| no | Disabled: tag1 | ++ | @not.with_foo=xxx @not.with_foo=other @not.with_foo=other2| no | Disabled: tag1 | + + + Scenario: Tag logic with unknown categories (case: ignored) +diff --git a/tests/functional/test_active_tags.py b/tests/functional/test_active_tags.py +new file mode 100644 +index 0000000..fd6138f +--- /dev/null ++++ b/tests/functional/test_active_tags.py +@@ -0,0 +1,529 @@ ++# -*- coding: utf-8 -*- ++""" ++Functionals tests for active tag-matcher (mod:`behave.tag_matcher`). ++""" ++ ++from __future__ import absolute_import, print_function ++import pytest ++from behave.tag_matcher import ActiveTagMatcher ++ ++# ============================================================================= ++# TEST DATA: ++# ============================================================================= ++# VALUE_PROVIDER = { ++# "foo": "alice", ++# "bar": "BOB", ++# } ++ ++# ============================================================================= ++# PYTEST FIXTURES: ++# ============================================================================= ++# @pytest.fixture ++# def active_tag_matcher(): ++# tag_matcher = ActiveTagMatcher(VALUE_PROVIDER) ++# return tag_matcher ++ ++ ++# ============================================================================= ++# TEST SUITE: ++# ============================================================================= ++class TestActivateTags(object): ++ VALUE_PROVIDER = { ++ "foo": "Frank", ++ "bar": "Bob", ++ # "OTHER": "VALUE", ++ } ++ ++ def check_should_run_with_active_tags(self, case, expected, tags): ++ # -- tag_matcher.should_run_with(tags).result := expected ++ case += " (tags: {tags})" ++ tag_matcher = ActiveTagMatcher(self.VALUE_PROVIDER) ++ actual_result1 = tag_matcher.should_run_with(tags) ++ actual_result2 = tag_matcher.should_exclude_with(tags) ++ assert expected == actual_result1, case.format(tags=tags) ++ assert (not expected) == actual_result2 ++ ++ @pytest.mark.parametrize("case, expected, tags", [ ++ ("use.with_foo=VALUE matches", True, ["use.with_foo=Frank"]), ++ ("use.with_foo=VALUE mismatches", False, ["use.with_foo=OTHER"]), ++ ("not.with_foo=VALUE matches", False, ["not.with_foo=Frank"]), ++ ("not.with_foo=VALUE mismatches", True, ["not.with_foo=OTHER"]), ++ ("NO_TAGS", True, []), ++ ]) ++ def test_one_tag_for_category1(self, case, expected, tags): ++ self.check_should_run_with_active_tags(case, expected, tags) ++ ++ @pytest.mark.parametrize("case, expected, tags", [ ++ ("use.with_bar=Bob matches", True, ["use.with_bar=Bob"]), ++ ("use.with_bar=VALUE mismatches", False, ["use.with_bar=OTHER"]), ++ ("not.with_bar=VALUE matches", False, ["not.with_bar=Bob"]), ++ ("not.with_bar=VALUE mismatches", True, ["not.with_bar=OTHER"]), ++ ]) ++ def test_one_tag_for_category2(self, case, expected, tags): ++ self.check_should_run_with_active_tags(case, expected, tags) ++ ++ # @pytest.mark.parametrize("case, expected, tags", [ ++ # ("use.with_OTHER=VALUE matches", True, ["use.with_OTHER=VALUE"]), ++ # ("use.with_OTHER=VALUE mismatches", False, ["use.with_OTHER=OTHER"]), ++ # ("not.with_OTHER=VALUE matches", False, ["not.with_OTHER=VALUE"]), ++ # ("not.with_OTHER=VALUE mismatches", True, ["not.with_OTHER=OTHER"]), ++ # ]) ++ # def test_one_tag_for_other_category(self, case, expected, tags): ++ # self.check_should_run_with_active_tags(case, expected, tags) ++ ++ @pytest.mark.parametrize("case, expected, tags", [ ++ ("2x use.with_foo=VALUE: one matches", True, ++ ["use.with_foo=Frank", "use.with_foo=OTHER"]), ++ ("2x not.with_foo=VALUE: one matches", False, ++ ["not.with_foo=Frank", "not.with_foo=OTHER"]), ++ ("1x use./not.with_foo=VALUE: use-matches", True, ++ ["use.with_foo=Frank", "not.with_foo=OTHER"]), ++ ("1x use./not.with_foo=VALUE: not-matches", False, ++ ["not.with_foo=Frank", "use.with_foo=OTHER"]), ++ ]) ++ def test_one_category_with_two_tags(self, case, expected, tags): ++ self.check_should_run_with_active_tags(case, expected, tags) ++ ++ @pytest.mark.parametrize("case, expected, tags", [ ++ ("3x use.with_foo=VALUE: one matches", True, ++ ["use.with_foo=Frank", "use.with_foo=OTHER_1", "use.with_foo=OTHER_2"]), ++ ("3x not.with_foo=VALUE: one matches", False, ++ ["not.with_foo=Frank", "not.with_foo=OTHER_1", "not.with_foo=OTHER_2"]), ++ ("2x use.with_foo=VALUE: use-matches", True, ++ ["use.with_foo=Frank", "use.with_foo=OTHER_1", "not.with_foo=OTHER_2"]), ++ ("2x not.with_foo=VALUE: not-matches", False, ++ ["not.with_foo=Frank", "not.with_foo=OTHER_1", "use.with_foo=OTHER_2"]), ++ ("1x use.with_foo=VALUE: use-matches", True, ++ ["use.with_foo=Frank", "not.with_foo=OTHER_1", "not.with_foo=OTHER_2"]), ++ ("1x not.with_foo=VALUE: not-matches", False, ++ ["not.with_foo=Frank", "use.with_foo=OTHER_1", "use.with_foo=OTHER_2"]), ++ ]) ++ def test_one_category_with_three_tags(self, case, expected, tags): ++ self.check_should_run_with_active_tags(case, expected, tags) ++ ++ @pytest.mark.parametrize("case, expected, tags", [ ++ # -- use.with_CATEGORY=VALUE ++ ("use.with_... 2x matches", True, ["use.with_foo=Frank", "use.with_bar=Bob"]), ++ ("use.with_... 1x matches", False, ["use.with_foo=Frank", "use.with_bar=OTHER"]), ++ ("use.with_... 1x matches", False, ["use.with_foo=OTHER", "use.with_bar=OTHER"]), ++ ("use.with_... 1x matches", False, ["use.with_foo=OTHER", "use.with_bar=Bob"]), ++ ("use.with_... 0x matches", False, ["use.with_foo=OTHER", "use.with_bar=OTHER"]), ++ # -- not.with_CATEGORY=VALUE ++ ("not.with_... 2x matches", False, ["not.with_foo=Frank", "not.with_bar=Bob"]), ++ ("not.with_... 1x matches", False, ["not.with_foo=Frank", "not.with_bar=OTHER"]), ++ ("not.with_... 1x matches", False, ["not.with_foo=OTHER", "not.with_bar=Bob"]), ++ ("not.with_... 0x matches", True, ["not.with_foo=OTHER", "not.with_bar=OTHER"]), ++ # -- use.with_CATEGORY_1=VALUE_1, not.with_CATEGORY_2=VALUE_2 ++ ("use./not.with_... use-matches", True, ["use.with_foo=Frank", "not.with_bar=OTHER"]), ++ ("use./not.with_... not-matches", False, ["use.with_foo=OTHER", "not.with_bar=Bob"]), ++ ("use./not.with_... 2x matches", False, ["use.with_foo=Frank", "not.with_bar=Bob"]), ++ ("use./not.with_... 0x matches", False, ["use.with_foo=OTHER", "not.with_bar=OTHER"]), ++ ]) ++ def test_two_categories_with_two_tags(self, case, expected, tags): ++ self.check_should_run_with_active_tags(case, expected, tags) ++ ++ @pytest.mark.parametrize("case, expected, tags", [ ++ # -- use.with_CATEGORY=VALUE ++ ("use.with_... 2x matches", True, ["use.with_foo=Frank", "use.with_foo=OTHER", "use.with_bar=Bob"]), ++ ("use.with_... 1x matches", False, ["use.with_foo=Frank", "use.with_foo=OTHER", "use.with_bar=OTHER"]), ++ ("use.with_... 1x matches", False, ["use.with_foo=OTHER", "use.with_foo=Frank", "use.with_bar=OTHER"]), ++ ("use.with_... 1x matches", False, ["use.with_foo=OTHER", "use.with_foo=OTHER2", "use.with_bar=Bob"]), ++ ("use.with_... 0x matches", False, ["use.with_foo=OTHER", "use.with_bar=OTHER2", "use.with_bar=OTHER"]), ++ # -- not.with_CATEGORY=VALUE ++ ("not.with_... 2x matches", False, ["not.with_foo=Frank", "not.with_foo=OTHER", "not.with_bar=Bob"]), ++ ("not.with_... 1x matches", False, ["not.with_foo=Frank", "not.with_foo=OTHER", "not.with_bar=OTHER"]), ++ ("not.with_... 1x matches", False, ["not.with_foo=OTHER", "not.with_foo=OTHER2", "not.with_bar=Bob"]), ++ ("not.with_... 0x matches", True, ["not.with_foo=OTHER", "not.with_foo=OTHER2", "not.with_bar=OTHER"]), ++ # -- use.with_CATEGORY_1=VALUE_1, not.with_CATEGORY_2=VALUE_2 ++ ("use./not.with_... use-matches", True, ["use.with_foo=Frank", "use.with_foo=OTHER", "not.with_bar=OTHER"]), ++ ("use./not.with_... not-matches", False, ["use.with_foo=OTHER", "use.with_foo=OTHER2", "not.with_bar=Bob"]), ++ ("use./not.with_... 2x matches", False, ["use.with_foo=Frank", "use.with_foo=OTHER", "not.with_bar=Bob"]), ++ ("use./not.with_... 0x matches", False, ["use.with_foo=OTHER", "use.with_foo=OTHER2", "not.with_bar=OTHER"]), ++ # -- not.with_CATEGORY_1=VALUE_1, use.with_CATEGORY_2=VALUE_2 ++ ("use./not.with_... not-matches", False, ["not.with_foo=Frank", "not.with_foo=OTHER", "use.with_bar=OTHER"]), ++ ("use./not.with_... use-matches", True, ["not.with_foo=OTHER", "not.with_foo=OTHER2", "use.with_bar=Bob"]), ++ ("use./not.with_... 2x matches", False, ["not.with_foo=Frank", "not.with_foo=OTHER", "use.with_bar=Bob"]), ++ ("use./not.with_... 0x matches", False, ["not.with_foo=OTHER", "not.with_foo=OTHER2", "use.with_bar=OTHER"]), ++ ]) ++ def test_two_categories_with_three_tags(self, case, expected, tags): ++ self.check_should_run_with_active_tags(case, expected, tags) ++ ++''' ++class Traits4ActiveTagMatcher(object): ++ TagMatcher = ActiveTagMatcher ++ value_provider = { ++ "foo": "alice", ++ "bar": "BOB", ++ } ++ ++ category1_enabled_tag = TagMatcher.make_category_tag("foo", "alice") ++ category1_disabled_tag = TagMatcher.make_category_tag("foo", "bob") ++ category1_disabled_tag2 = TagMatcher.make_category_tag("foo", "charly") ++ category1_similar_tag = TagMatcher.make_category_tag("foo", "alice2") ++ category2_enabled_tag = TagMatcher.make_category_tag("bar", "BOB") ++ category2_disabled_tag = TagMatcher.make_category_tag("bar", "CHARLY") ++ category2_similar_tag = TagMatcher.make_category_tag("bar", "BOB2") ++ unknown_category_tag = TagMatcher.make_category_tag("UNKNOWN", "one") ++ ++ # -- NEGATED TAGS: ++ category1_not_enabled_tag = \ ++ TagMatcher.make_category_tag("foo", "alice", "not_active") ++ category1_not_enabled_tag2 = \ ++ TagMatcher.make_category_tag("foo", "alice", "not") ++ category1_not_disabled_tag = \ ++ TagMatcher.make_category_tag("foo", "bob", "not_active") ++ category1_negated_similar_tag1 = \ ++ TagMatcher.make_category_tag("foo", "alice2", "not_active") ++ ++ ++ active_tags1 = [ ++ category1_enabled_tag, category1_disabled_tag, category1_similar_tag, ++ category1_not_enabled_tag, category1_not_enabled_tag2, ++ ] ++ active_tags2 = [ ++ category2_enabled_tag, category2_disabled_tag, category2_similar_tag, ++ ] ++ active_tags = active_tags1 + active_tags2 ++ ++ ++# -- REQUIRES: pytest ++class TestActiveTagMatcher2(object): ++ TagMatcher = ActiveTagMatcher ++ traits = Traits4ActiveTagMatcher ++ ++ @classmethod ++ def make_tag_matcher(cls): ++ value_provider = { ++ "foo": "alice", ++ "bar": "BOB", ++ } ++ tag_matcher = cls.TagMatcher(value_provider) ++ return tag_matcher ++ ++ @pytest.mark.parametrize("case, expected_len, tags", [ ++ ("case: Two enabled tags", 2, ++ [traits.category1_enabled_tag, traits.category2_enabled_tag]), ++ ("case: Active enabled and normal tag", 1, ++ [traits.category1_enabled_tag, "foo"]), ++ ("case: Active disabled and normal tag", 1, ++ [traits.category1_disabled_tag, "foo"]), ++ ("case: Normal and active negated tag", 1, ++ ["foo", traits.category1_not_enabled_tag]), ++ ("case: Two normal tags", 0, ++ ["foo", "bar"]), ++ ]) ++ def test_select_active_tags__with_two_tags(self, case, expected_len, tags): ++ tag_matcher = self.make_tag_matcher() ++ selected = tag_matcher.select_active_tags(tags) ++ selected = list(selected) ++ assert len(selected) == expected_len, case ++ ++ @pytest.mark.parametrize("case, expected, tags", [ ++ # -- GROUP: With positive logic (non-negated tags) ++ ("case P00: 2 disabled tags", True, ++ [ traits.category1_disabled_tag, traits.category2_disabled_tag]), ++ ("case P01: disabled and enabled tag", True, ++ [ traits.category1_disabled_tag, traits.category2_enabled_tag]), ++ ("case P10: enabled and disabled tag", True, ++ [ traits.category1_enabled_tag, traits.category2_disabled_tag]), ++ ("case P11: 2 enabled tags", False, # -- SHOULD-RUN ++ [ traits.category1_enabled_tag, traits.category2_enabled_tag]), ++ # -- GROUP: With negated tag ++ ("case N00: not-enabled and disabled tag", True, ++ [ traits.category1_not_enabled_tag, traits.category2_disabled_tag]), ++ ("case N01: not-enabled and enabled tag", True, ++ [ traits.category1_not_enabled_tag, traits.category2_enabled_tag]), ++ ("case N10: not-disabled and disabled tag", True, ++ [ traits.category1_not_disabled_tag, traits.category2_disabled_tag]), ++ ("case N11: not-disabled and enabled tag", False, # -- SHOULD-RUN ++ [ traits.category1_not_disabled_tag, traits.category2_enabled_tag]), ++ # -- GROUP: With unknown category ++ ("case U0x: disabled and unknown tag", True, ++ [ traits.category1_disabled_tag, traits.unknown_category_tag]), ++ ("case U1x: enabled and unknown tag", False, # -- SHOULD-RUN ++ [ traits.category1_enabled_tag, traits.unknown_category_tag]), ++ ]) ++ def test_should_exclude_with__combinations_of_2_categories(self, case, expected, tags): ++ tag_matcher = self.make_tag_matcher() ++ actual_result = tag_matcher.should_exclude_with(tags) ++ assert expected == actual_result, case ++ ++ @pytest.mark.parametrize("case, expected, tags", [ ++ # -- GROUP: With positive logic (non-negated tags) ++ ("case P00: 2 disabled tags", True, ++ [ traits.category1_disabled_tag, traits.category1_disabled_tag2]), ++ ("case P01: disabled and enabled tag", False, ++ [ traits.category1_disabled_tag, traits.category1_enabled_tag]), ++ ("case P10: enabled and disabled tag", False, ++ [ traits.category1_enabled_tag, traits.category1_disabled_tag]), ++ ("case P11: 2 enabled tags (same)", False, # -- SHOULD-RUN ++ [ traits.category1_enabled_tag, traits.category1_enabled_tag]), ++ # -- GROUP: With negated tag ++ ("case N00: not-enabled and disabled tag", True, ++ [ traits.category1_not_enabled_tag, traits.category1_disabled_tag]), ++ ("case N01: not-enabled and enabled tag", True, ++ [ traits.category1_not_enabled_tag, traits.category1_enabled_tag]), ++ ("case N10: not-disabled and disabled tag", True, ++ [ traits.category1_not_disabled_tag, traits.category1_disabled_tag]), ++ ("case N11: not-disabled and enabled tag", False, # -- SHOULD-RUN ++ [ traits.category1_not_disabled_tag, traits.category1_enabled_tag]), ++ ]) ++ def test_should_exclude_with__combinations_with_same_category(self, ++ case, expected, tags): ++ tag_matcher = self.make_tag_matcher() ++ print("tags: {}".format(tags) ) ++ print("tag_matcher.value: {}".format(tag_matcher.value_provider) ) ++ actual_result = tag_matcher.should_exclude_with(tags) ++ assert expected == actual_result, case ++ ++ ++class TestActiveTags(TestCase): ++ TagMatcher = ActiveTagMatcher ++ traits = Traits4ActiveTagMatcher ++ ++ @classmethod ++ def make_tag_matcher(cls): ++ tag_matcher = cls.TagMatcher(cls.traits.value_provider) ++ return tag_matcher ++ ++ def setUp(self): ++ self.tag_matcher = self.make_tag_matcher() ++ ++ def test_select_active_tags__basics(self): ++ active_tag = "active.with_CATEGORY=VALUE" ++ tags = ["foo", active_tag, "bar"] ++ selected = list(self.tag_matcher.select_active_tags(tags)) ++ self.assertEqual(len(selected), 1) ++ selected_tag, selected_match = selected[0] ++ self.assertEqual(selected_tag, active_tag) ++ ++ def test_select_active_tags__matches_tag_parts(self): ++ tags = ["active.with_CATEGORY=VALUE"] ++ selected = list(self.tag_matcher.select_active_tags(tags)) ++ self.assertEqual(len(selected), 1) ++ selected_tag, selected_match = selected[0] ++ self.assertEqual(selected_match.group("prefix"), "active") ++ self.assertEqual(selected_match.group("category"), "CATEGORY") ++ self.assertEqual(selected_match.group("value"), "VALUE") ++ ++ def test_select_active_tags__finds_tag_with_any_valid_tag_prefix(self): ++ TagMatcher = self.TagMatcher ++ for tag_prefix in TagMatcher.tag_prefixes: ++ tag = TagMatcher.make_category_tag("foo", "alice", tag_prefix) ++ tags = [ tag ] ++ selected = self.tag_matcher.select_active_tags(tags) ++ selected = list(selected) ++ self.assertEqual(len(selected), 1) ++ selected_tag0 = selected[0][0] ++ self.assertEqual(selected_tag0, tag) ++ self.assertTrue(selected_tag0.startswith(tag_prefix)) ++ ++ def test_select_active_tags__ignores_invalid_active_tags(self): ++ invalid_active_tags = [ ++ ("foo.alice", "case: Normal tag"), ++ ("with_foo=Frank", "case: Subset of an active tag"), ++ ("ACTIVE.with_foo.alice", "case: Wrong tag_prefix (uppercase)"), ++ ("only.with_foo.alice", "case: Wrong value_separator"), ++ ] ++ for invalid_tag, case in invalid_active_tags: ++ tags = [ invalid_tag ] ++ selected = self.tag_matcher.select_active_tags(tags) ++ selected = list(selected) ++ self.assertEqual(len(selected), 0, case) ++ ++ def test_select_active_tags__with_two_tags(self): ++ # XXX-JE-DUPLICATED: ++ traits = self.traits ++ test_patterns = [ ++ ("case: Two enabled tags", ++ [traits.category1_enabled_tag, traits.category2_enabled_tag]), ++ ("case: Active enabled and normal tag", ++ [traits.category1_enabled_tag, "foo"]), ++ ("case: Active disabled and normal tag", ++ [traits.category1_disabled_tag, "foo"]), ++ ("case: Active negated and normal tag", ++ [traits.category1_not_enabled_tag, "foo"]), ++ ] ++ for case, tags in test_patterns: ++ selected = self.tag_matcher.select_active_tags(tags) ++ selected = list(selected) ++ self.assertTrue(len(selected) >= 1, case) ++ ++ ++ def test_should_exclude_with__returns_false_with_enabled_tag(self): ++ traits = self.traits ++ tags1 = [ traits.category1_enabled_tag ] ++ tags2 = [ traits.category2_enabled_tag ] ++ self.assertEqual(False, self.tag_matcher.should_exclude_with(tags1)) ++ self.assertEqual(False, self.tag_matcher.should_exclude_with(tags2)) ++ ++ def test_should_exclude_with__returns_false_with_disabled_tag_and_more(self): ++ # -- NOTE: Need 1+ enabled active-tags of same category => ENABLED ++ # pylint: disable=line-too-long ++ traits = self.traits ++ test_patterns = [ ++ ([ traits.category1_enabled_tag, traits.category1_disabled_tag ], "case: first"), ++ ([ traits.category1_disabled_tag, traits.category1_enabled_tag ], "case: last"), ++ ([ "foo", traits.category1_enabled_tag, traits.category1_disabled_tag, "bar" ], "case: middle"), ++ ] ++ enabled = True # EXPECTED ++ for tags, case in test_patterns: ++ self.assertEqual(not enabled, self.tag_matcher.should_exclude_with(tags), ++ "%s: tags=%s" % (case, tags)) ++ ++ def test_should_exclude_with__returns_true_with_other_tag(self): ++ traits = self.traits ++ tags = [ traits.category1_disabled_tag ] ++ self.assertEqual(True, self.tag_matcher.should_exclude_with(tags)) ++ ++ def test_should_exclude_with__returns_true_with_other_tag_and_more(self): ++ traits = self.traits ++ test_patterns = [ ++ ([ traits.category1_disabled_tag, "foo" ], "case: first"), ++ ([ "foo", traits.category1_disabled_tag ], "case: last"), ++ ([ "foo", traits.category1_disabled_tag, "bar" ], "case: middle"), ++ ] ++ for tags, case in test_patterns: ++ self.assertEqual(True, self.tag_matcher.should_exclude_with(tags), ++ "%s: tags=%s" % (case, tags)) ++ ++ def test_should_exclude_with__returns_true_with_similar_tag(self): ++ traits = self.traits ++ tags = [ traits.category1_similar_tag ] ++ self.assertEqual(True, self.tag_matcher.should_exclude_with(tags)) ++ ++ def test_should_exclude_with__returns_true_with_similar_and_more(self): ++ traits = self.traits ++ test_patterns = [ ++ ([ traits.category1_similar_tag, "foo" ], "case: first"), ++ ([ "foo", traits.category1_similar_tag ], "case: last"), ++ ([ "foo", traits.category1_similar_tag, "bar" ], "case: middle"), ++ ] ++ for tags, case in test_patterns: ++ self.assertEqual(True, self.tag_matcher.should_exclude_with(tags), ++ "%s: tags=%s" % (case, tags)) ++ ++ def test_should_exclude_with__returns_false_without_category_tag(self): ++ test_patterns = [ ++ ([ ], "case: No tags"), ++ ([ "foo" ], "case: One tag"), ++ ([ "foo", "bar" ], "case: Two tags"), ++ ] ++ for tags, case in test_patterns: ++ self.assertEqual(False, self.tag_matcher.should_exclude_with(tags), ++ "%s: tags=%s" % (case, tags)) ++ ++ def test_should_exclude_with__returns_false_with_unknown_category_tag(self): ++ """Tags from unknown categories, not supported by value_provider, ++ should not be excluded. ++ """ ++ traits = self.traits ++ tags = [ traits.unknown_category_tag ] ++ self.assertEqual("use.with_UNKNOWN=one", traits.unknown_category_tag) ++ self.assertEqual(None, self.tag_matcher.value_provider.get("UNKNOWN")) ++ self.assertEqual(False, self.tag_matcher.should_exclude_with(tags)) ++ ++ def test_should_exclude_with__combinations_of_2_categories(self): ++ # XXX-JE-DUPLICATED: ++ traits = self.traits ++ test_patterns = [ ++ ("case P00: 2 disabled category tags", True, ++ [ traits.category1_disabled_tag, traits.category2_disabled_tag]), ++ ("case P01: disabled and enabled category tags", True, ++ [ traits.category1_disabled_tag, traits.category2_enabled_tag]), ++ ("case P10: enabled and disabled category tags", True, ++ [ traits.category1_enabled_tag, traits.category2_disabled_tag]), ++ ("case P11: 2 enabled category tags", False, # -- SHOULD-RUN ++ [ traits.category1_enabled_tag, traits.category2_enabled_tag]), ++ # -- SPECIAL CASE: With negated category ++ ("case N00: not-enabled and disabled category tags", True, ++ [ traits.category1_not_enabled_tag, traits.category2_disabled_tag]), ++ ("case N01: not-enabled and enabled category tags", True, ++ [ traits.category1_not_enabled_tag, traits.category2_enabled_tag]), ++ ("case N10: not-disabled and disabled category tags", True, ++ [ traits.category1_not_disabled_tag, traits.category2_disabled_tag]), ++ ("case N11: not-enabled and enabled category tags", False, # -- SHOULD-RUN ++ [ traits.category1_not_disabled_tag, traits.category2_enabled_tag]), ++ # -- SPECIAL CASE: With unknown category ++ ("case 0x: disabled and unknown category tags", True, ++ [ traits.category1_disabled_tag, traits.unknown_category_tag]), ++ ("case 1x: enabled and unknown category tags", False, # SHOULD-RUN ++ [ traits.category1_enabled_tag, traits.unknown_category_tag]), ++ ] ++ for case, expected, tags in test_patterns: ++ actual_result = self.tag_matcher.should_exclude_with(tags) ++ self.assertEqual(expected, actual_result, ++ "%s: tags=%s" % (case, tags)) ++ ++ def test_should_run_with__negates_result_of_should_exclude_with(self): ++ traits = self.traits ++ test_patterns = [ ++ ([ ], "case: No tags"), ++ ([ "foo" ], "case: One non-category tag"), ++ ([ "foo", "bar" ], "case: Two non-category tags"), ++ ([ traits.category1_enabled_tag ], "case: enabled tag"), ++ ([ traits.category1_enabled_tag, traits.category1_disabled_tag ], "case: enabled and other tag"), ++ ([ traits.category1_enabled_tag, "foo" ], "case: enabled and foo tag"), ++ ([ traits.category1_disabled_tag ], "case: other tag"), ++ ([ traits.category1_disabled_tag, "foo" ], "case: other and foo tag"), ++ ([ traits.category1_similar_tag ], "case: similar tag"), ++ ([ "foo", traits.category1_similar_tag ], "case: foo and similar tag"), ++ ] ++ for tags, case in test_patterns: ++ result1 = self.tag_matcher.should_run_with(tags) ++ result2 = self.tag_matcher.should_exclude_with(tags) ++ self.assertEqual(result1, not result2, "%s: tags=%s" % (case, tags)) ++ self.assertEqual(not result1, result2, "%s: tags=%s" % (case, tags)) ++ ++ ++class TestCompositeTagMatcher(TestCase): ++ ++ @staticmethod ++ def count_tag_matcher_with_result(tag_matchers, tags, result_value): ++ count = 0 ++ for tag_matcher in tag_matchers: ++ current_result = tag_matcher.should_exclude_with(tags) ++ if current_result == result_value: ++ count += 1 ++ return count ++ ++ def setUp(self): ++ predicate_false = lambda tags: False ++ predicate_contains_foo = lambda tags: any(x == "foo" for x in tags) ++ self.tag_matcher_false = PredicateTagMatcher(predicate_false) ++ self.tag_matcher_foo = PredicateTagMatcher(predicate_contains_foo) ++ tag_matchers = [ ++ self.tag_matcher_foo, ++ self.tag_matcher_false ++ ] ++ self.ctag_matcher = CompositeTagMatcher(tag_matchers) ++ ++ def test_should_exclude_with__returns_true_when_any_tag_matcher_returns_true(self): ++ test_patterns = [ ++ ("case: with foo", ["foo", "bar"]), ++ ("case: with foo2", ["foozy", "foo", "bar"]), ++ ] ++ for case, tags in test_patterns: ++ actual_result = self.ctag_matcher.should_exclude_with(tags) ++ self.assertEqual(True, actual_result, ++ "%s: tags=%s" % (case, tags)) ++ ++ actual_true_count = self.count_tag_matcher_with_result( ++ self.ctag_matcher.tag_matchers, tags, True) ++ self.assertEqual(1, actual_true_count) ++ ++ def test_should_exclude_with__returns_false_when_no_tag_matcher_return_true(self): ++ test_patterns = [ ++ ("case: without foo", ["fool", "bar"]), ++ ("case: without foo2", ["foozy", "bar"]), ++ ] ++ for case, tags in test_patterns: ++ actual_result = self.ctag_matcher.should_exclude_with(tags) ++ self.assertEqual(False, actual_result, ++ "%s: tags=%s" % (case, tags)) ++ ++ actual_true_count = self.count_tag_matcher_with_result( ++ self.ctag_matcher.tag_matchers, tags, True) ++ self.assertEqual(0, actual_true_count) ++''' ++ +diff --git a/tests/unit/test_tag_matcher.py b/tests/unit/test_tag_matcher.py +index a04c1d4..43f5af0 100644 +--- a/tests/unit/test_tag_matcher.py ++++ b/tests/unit/test_tag_matcher.py +@@ -128,9 +128,9 @@ class TestActiveTagMatcher2(object): + # -- GROUP: With negated tag + ("case N00: not-enabled and disabled tag", True, + [ traits.category1_not_enabled_tag, traits.category1_disabled_tag]), +- ("case N01: not-enabled and enabled tag", False, ++ ("case N01: not-enabled and enabled tag", True, + [ traits.category1_not_enabled_tag, traits.category1_enabled_tag]), +- ("case N10: not-disabled and disabled tag", False, ++ ("case N10: not-disabled and disabled tag", True, + [ traits.category1_not_disabled_tag, traits.category1_disabled_tag]), + ("case N11: not-disabled and enabled tag", False, # -- SHOULD-RUN + [ traits.category1_not_disabled_tag, traits.category1_enabled_tag]), +@@ -138,6 +138,8 @@ class TestActiveTagMatcher2(object): + def test_should_exclude_with__combinations_with_same_category(self, + case, expected, tags): + tag_matcher = self.make_tag_matcher() ++ print("tags: {}".format(tags) ) ++ print("tag_matcher.value: {}".format(tag_matcher.value_provider) ) + actual_result = tag_matcher.should_exclude_with(tags) + assert expected == actual_result, case + +@@ -224,6 +226,7 @@ class TestActiveTagMatcher1(TestCase): + + def test_should_exclude_with__returns_false_with_disabled_tag_and_more(self): + # -- NOTE: Need 1+ enabled active-tags of same category => ENABLED ++ # pylint: disable=line-too-long + traits = self.traits + test_patterns = [ + ([ traits.category1_enabled_tag, traits.category1_disabled_tag ], "case: first"), +@@ -283,7 +286,7 @@ class TestActiveTagMatcher1(TestCase): + """ + traits = self.traits + tags = [ traits.unknown_category_tag ] +- self.assertEqual("active.with_UNKNOWN=one", traits.unknown_category_tag) ++ self.assertEqual("use.with_UNKNOWN=one", traits.unknown_category_tag) + self.assertEqual(None, self.tag_matcher.value_provider.get("UNKNOWN")) + self.assertEqual(False, self.tag_matcher.should_exclude_with(tags)) + +@@ -376,42 +379,6 @@ class TestPredicateTagMatcher(TestCase): + self.assertEqual(False, predicate_always_false(tags)) + + +-class TestPredicateTagMatcher(TestCase): +- +- def test_exclude_with__mechanics(self): +- predicate_function_blueprint = lambda tags: False +- predicate_function = Mock(predicate_function_blueprint) +- predicate_function.return_value = True +- tag_matcher = PredicateTagMatcher(predicate_function) +- tags = [ "foo", "bar" ] +- self.assertEqual(True, tag_matcher.should_exclude_with(tags)) +- predicate_function.assert_called_once_with(tags) +- self.assertEqual(True, predicate_function(tags)) +- +- def test_should_exclude_with__returns_true_when_predicate_is_true(self): +- predicate_always_true = lambda tags: True +- tag_matcher1 = PredicateTagMatcher(predicate_always_true) +- tags = [ "foo", "bar" ] +- self.assertEqual(True, tag_matcher1.should_exclude_with(tags)) +- self.assertEqual(True, predicate_always_true(tags)) +- +- def test_should_exclude_with__returns_true_when_predicate_is_true2(self): +- # -- CASE: Use predicate function instead of lambda. +- def predicate_contains_foo(tags): +- return any(x == "foo" for x in tags) +- tag_matcher2 = PredicateTagMatcher(predicate_contains_foo) +- tags = [ "foo", "bar" ] +- self.assertEqual(True, tag_matcher2.should_exclude_with(tags)) +- self.assertEqual(True, predicate_contains_foo(tags)) +- +- def test_should_exclude_with__returns_false_when_predicate_is_false(self): +- predicate_always_false = lambda tags: False +- tag_matcher1 = PredicateTagMatcher(predicate_always_false) +- tags = [ "foo", "bar" ] +- self.assertEqual(False, tag_matcher1.should_exclude_with(tags)) +- self.assertEqual(False, predicate_always_false(tags)) +- +- + class TestCompositeTagMatcher(TestCase): + + @staticmethod diff --git a/meta-python/recipes-devtools/python/python3-behave/0110-FIX-Tests-w-more.features.patch b/meta-python/recipes-devtools/python/python3-behave/0110-FIX-Tests-w-more.features.patch new file mode 100644 index 000000000..35e26b0bd --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0110-FIX-Tests-w-more.features.patch @@ -0,0 +1,56 @@ +From 63e15e8f89e0e9144bda18df3e08e96e8c9bfe50 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sat, 9 Jan 2021 22:09:59 +0100 +Subject: [PATCH] FIX: Tests w/ more.features/ + +--- + py.requirements/ci.tox.txt | 2 ++ + py.requirements/ci.travis.txt | 2 ++ + tox.ini | 4 ++-- + 3 files changed, 6 insertions(+), 2 deletions(-) + +diff --git a/py.requirements/ci.tox.txt b/py.requirements/ci.tox.txt +index 5bee524..387e905 100644 +--- a/py.requirements/ci.tox.txt ++++ b/py.requirements/ci.tox.txt +@@ -13,3 +13,5 @@ PyHamcrest < 2.0; python_version < '3.0' + # -- HINT: path.py => path (python-install-package was renamed for python3) + path.py >= 11.5.0; python_version < '3.5' + path >= 13.1.0; python_version >= '3.5' ++ ++jsonschema +diff --git a/py.requirements/ci.travis.txt b/py.requirements/ci.travis.txt +index 372116a..cbc60c0 100644 +--- a/py.requirements/ci.travis.txt ++++ b/py.requirements/ci.travis.txt +@@ -16,6 +16,8 @@ PyHamcrest < 2.0; python_version < '3.0' + path.py >= 11.5.0; python_version < '3.5' + path >= 13.1.0; python_version >= '3.5' + ++jsonschema ++ + # -- NOTE: Travis.CI tweak related w/ invalid linecache2 tests. + # This problem does not exist if you use pip. + linecache2 >= 1.0; python_version < '3.0' +diff --git a/tox.ini b/tox.ini +index 8ccb58b..c145b51 100644 +--- a/tox.ini ++++ b/tox.ini +@@ -6,7 +6,7 @@ + # Use tox to run tasks (tests, ...) in a clean virtual environment. + # Afterwards you can run tox in offline mode, like: + # +-# tox -e py27 ++# tox -e py38 + # + # Tox can be configured for offline usage. + # Initialize local workspace once (download packages, create PyPI index): +@@ -28,7 +28,7 @@ + + [tox] + minversion = 2.3 +-envlist = py27, py37, py38, py36, py35, pypy, docs ++envlist = py39, py38, py27, py37, py36, py35, pypy3, pypy, docs + skip_missing_interpreters = True + sitepackages = False + indexserver = diff --git a/meta-python/recipes-devtools/python/python3-behave/0111-FIX-Some-regressions-with-Python-3.9.patch b/meta-python/recipes-devtools/python/python3-behave/0111-FIX-Some-regressions-with-Python-3.9.patch new file mode 100644 index 000000000..5d560ad3d --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0111-FIX-Some-regressions-with-Python-3.9.patch @@ -0,0 +1,741 @@ +From 3c207ac9ac83d3966a7a84b8782394dc603030cd Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sat, 9 Jan 2021 22:11:38 +0100 +Subject: [PATCH] FIX: Some regressions with Python 3.9 + +* async-steps: Use more stable active-tags now. +* JUnit XML related: Adapt to XML attribute ordering changes since Python 3.8. +* Prepare active-tags usage for Python 3.10 +* Behave jsonschema: Add some extra elements (related to: gherkin v6 Rule). +--- + .gitignore | 1 + + CHANGES.rst | 2 + + behave/python_feature.py | 81 +++++++++++++++++++ + etc/json/behave.json-schema | 28 ++++++- + .../features/async_dispatch.feature | 7 +- + .../async_step/features/async_run.feature | 7 +- + examples/async_step/features/environment.py | 16 ++-- + .../features/steps/_async_steps34.py | 4 +- + .../features/steps/async_dispatch_steps.py | 13 +-- + .../async_step/features/steps/async_steps.py | 6 +- + features/environment.py | 12 ++- + features/step.async_steps.feature | 64 ++++----------- + issue.features/issue0330.feature | 18 +++-- + issue.features/issue0446.feature | 12 ++- + issue.features/issue0457.feature | 12 ++- + issue.features/issue0657.feature | 11 +-- + more.features/environment.py | 10 +-- + more.features/run_examples.feature | 9 +-- + 18 files changed, 195 insertions(+), 118 deletions(-) + create mode 100644 behave/python_feature.py + +diff --git a/.gitignore b/.gitignore +index 9c5c33d..8d7c28e 100644 +--- a/.gitignore ++++ b/.gitignore +@@ -25,3 +25,4 @@ tools/virtualenvs + .ropeproject + nosetests.xml + rerun.txt ++testrun*.json +diff --git a/CHANGES.rst b/CHANGES.rst +index 4e20bb8..a3398d8 100644 +--- a/CHANGES.rst ++++ b/CHANGES.rst +@@ -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) +diff --git a/behave/python_feature.py b/behave/python_feature.py +new file mode 100644 +index 0000000..4e134de +--- /dev/null ++++ b/behave/python_feature.py +@@ -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), ++} +diff --git a/etc/json/behave.json-schema b/etc/json/behave.json-schema +index 9cf3e62..110e240 100644 +--- a/etc/json/behave.json-schema ++++ b/etc/json/behave.json-schema +@@ -28,10 +28,36 @@ + "type": "object", + "anyOf": [ + { "$ref": "#/definitions/Background" }, ++ { "$ref": "#/definitions/Rule" }, + { "$ref": "#/definitions/Scenario" }, + { "$ref": "#/definitions/ScenarioOutline" } + ] + }, ++ "Rule": { ++ "type": "object", ++ "description": "Represents a Rule object.", ++ "properties": { ++ "name": { "type": "string" }, ++ "keyword": { "type": "string" }, ++ "location": { "type": "string" }, ++ "status": { "type": "string" }, ++ "tags": { "$ref": "#/definitions/Tags" }, ++ "description": { "$ref": "#/definitions/MultiLineText" }, ++ "elements": { ++ "type": "array", ++ "items": { "$ref": "#/definitions/RuleElement" } ++ } ++ }, ++ "required": [ "name", "keyword", "location" ] ++ }, ++ "RuleElement": { ++ "type": "object", ++ "anyOf": [ ++ { "$ref": "#/definitions/Scenario" }, ++ { "$ref": "#/definitions/ScenarioOutline" }, ++ { "$ref": "#/definitions/Background" } ++ ] ++ }, + "Background": { + "type": "object", + "properties": { +@@ -169,4 +195,4 @@ + "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] + } + } +-} +\ No newline at end of file ++} +diff --git a/examples/async_step/features/async_dispatch.feature b/examples/async_step/features/async_dispatch.feature +index 18e9869..e0eba1e 100644 +--- a/examples/async_step/features/async_dispatch.feature ++++ b/examples/async_step/features/async_dispatch.feature +@@ -1,8 +1,5 @@ +-@use.with_python.version=3.4 +-@use.with_python.version=3.5 +-@use.with_python.version=3.6 +-@use.with_python.version=3.7 +-@use.with_python.version=3.8 ++@use.with_python_has_async_function=true ++@use.with_python_has_asyncio.coroutine_decorator=true + Feature: + Scenario: + Given I dispatch an async-call with param "Alice" +diff --git a/examples/async_step/features/async_run.feature b/examples/async_step/features/async_run.feature +index 29b8fa7..8b6e555 100644 +--- a/examples/async_step/features/async_run.feature ++++ b/examples/async_step/features/async_run.feature +@@ -1,8 +1,5 @@ +-@use.with_python.version=3.4 +-@use.with_python.version=3.5 +-@use.with_python.version=3.6 +-@use.with_python.version=3.7 +-@use.with_python.version=3.8 ++@use.with_python_has_async_function=true ++@use.with_python_has_asyncio.coroutine_decorator=true + Feature: + Scenario: + Given an async-step waits 0.3 seconds +diff --git a/examples/async_step/features/environment.py b/examples/async_step/features/environment.py +index 02c4d92..3fa9604 100644 +--- a/examples/async_step/features/environment.py ++++ b/examples/async_step/features/environment.py +@@ -2,7 +2,8 @@ + + from behave.tag_matcher import ActiveTagMatcher, setup_active_tag_values + from behave.api.runtime_constraint import require_min_python_version +-import sys ++from behave import python_feature ++ + + # ----------------------------------------------------------------------------- + # REQUIRE: python >= 3.4 +@@ -15,27 +16,24 @@ require_min_python_version("3.4") + # ----------------------------------------------------------------------------- + # -- MATCHES ANY TAGS: @use.with_{category}={value} + # NOTE: active_tag_value_provider provides category values for active tags. +-python_version = "%s.%s" % sys.version_info[:2] +-active_tag_value_provider = { +- "python.version": python_version, +-} ++active_tag_value_provider = python_feature.ACTIVE_TAG_VALUE_PROVIDER.copy() + active_tag_matcher = ActiveTagMatcher(active_tag_value_provider) + + + # ----------------------------------------------------------------------------- + # HOOKS: + # ----------------------------------------------------------------------------- +-def before_all(context): ++def before_all(ctx): + # -- SETUP ACTIVE-TAG MATCHER (with userdata): +- setup_active_tag_values(active_tag_value_provider, context.config.userdata) ++ setup_active_tag_values(active_tag_value_provider, ctx.config.userdata) + + +-def before_feature(context, feature): ++def before_feature(ctx, feature): + if active_tag_matcher.should_exclude_with(feature.tags): + feature.skip(reason=active_tag_matcher.exclude_reason) + + +-def before_scenario(context, scenario): ++def before_scenario(ctx, scenario): + if active_tag_matcher.should_exclude_with(scenario.effective_tags): + scenario.skip(reason=active_tag_matcher.exclude_reason) + +diff --git a/examples/async_step/features/steps/_async_steps34.py b/examples/async_step/features/steps/_async_steps34.py +index 556500f..fc4c8d1 100644 +--- a/examples/async_step/features/steps/_async_steps34.py ++++ b/examples/async_step/features/steps/_async_steps34.py +@@ -1,5 +1,5 @@ +-# -- REQUIRES: Python >= 3.4 and Python < 3.8 +-# HINT: Decorator @asyncio.coroutine is prohibited in python 3.8 ++# -- REQUIRES: Python >= 3.4 and Python < 3.10 (removed in: 3.10) ++# HINT: @asyncio.coroutine decorator is deprecated since python 3.8 + # USE: Async generator/coroutine instead. + + from behave import step +diff --git a/examples/async_step/features/steps/async_dispatch_steps.py b/examples/async_step/features/steps/async_dispatch_steps.py +index 222e54d..def9616 100644 +--- a/examples/async_step/features/steps/async_dispatch_steps.py ++++ b/examples/async_step/features/steps/async_dispatch_steps.py +@@ -1,8 +1,9 @@ + # -*- coding: UTF-8 -*- + # REQUIRES: Python >= 3.4/3.5 +-import sys ++ + from behave import given, then, step + from behave.api.async_step import use_or_create_async_context ++from behave.python_feature import PythonFeature + from hamcrest import assert_that, equal_to, empty + import asyncio + +@@ -10,13 +11,15 @@ import asyncio + # --------------------------------------------------------------------------- + # ASYNC EXAMPLE FUNCTION: + # --------------------------------------------------------------------------- +-python_version = "%s.%s" % sys.version_info[:2] +-if python_version >= "3.5": ++if PythonFeature.has_async_function(): ++ # -- USE: async-function as coroutine-function ++ # SINCE: Python 3.5 (preferred) + async def async_func(param): + await asyncio.sleep(0.2) + return str(param).upper() +-else: +- # -- HINT: Decorator @asyncio.coroutine is prohibited in python 3.8 ++elif PythonFeature.has_asyncio_coroutine_decorator(): ++ # -- USE: @asyncio.coroutine decorator ++ # SINCE: Python 3.4, deprecated since Python 3.8, removed in Python 3.10 + @asyncio.coroutine + def async_func(param): + yield from asyncio.sleep(0.2) +diff --git a/examples/async_step/features/steps/async_steps.py b/examples/async_step/features/steps/async_steps.py +index dc03c72..33d2392 100644 +--- a/examples/async_step/features/steps/async_steps.py ++++ b/examples/async_step/features/steps/async_steps.py +@@ -5,8 +5,8 @@ + from __future__ import absolute_import + import sys + +-python_version = "%s.%s" % sys.version_info[:2] +-if python_version >= "3.5": ++python_version = sys.version_info[:2] ++if python_version >= (3, 5): + import _async_steps35 +-elif python_version == "3.4": ++elif python_version == (3, 4): + import _async_steps34 +diff --git a/features/environment.py b/features/environment.py +index 3769ee4..6faf4e2 100644 +--- a/features/environment.py ++++ b/features/environment.py +@@ -4,22 +4,19 @@ + from __future__ import absolute_import, print_function + from behave.tag_matcher import ActiveTagMatcher, setup_active_tag_values + from behave4cmd0.setup_command_shell import setup_command_shell_processors4behave ++from behave import python_feature + import platform + import sys +-import six ++ + + # -- MATCHES ANY TAGS: @use.with_{category}={value} + # NOTE: active_tag_value_provider provides category values for active tags. +-python_version = "%s.%s" % sys.version_info[:2] + active_tag_value_provider = { +- "python2": str(six.PY2).lower(), +- "python3": str(six.PY3).lower(), +- "python.version": python_version, + # -- python.implementation: cpython, pypy, jython, ironpython + "python.implementation": platform.python_implementation().lower(), + "pypy": str("__pypy__" in sys.modules).lower(), +- "os": sys.platform, + } ++active_tag_value_provider.update(python_feature.ACTIVE_TAG_VALUE_PROVIDER) + active_tag_matcher = ActiveTagMatcher(active_tag_value_provider) + + +@@ -27,7 +24,7 @@ def print_active_tags_summary(): + active_tag_data = active_tag_value_provider + print("ACTIVE-TAG SUMMARY:") + print("use.with_python.version=%s" % active_tag_data.get("python.version")) +- # print("use.with_os=%s" % active_tag_data.get("os")) ++ print("use.with_os=%s" % active_tag_data.get("os")) + print() + + +@@ -53,6 +50,7 @@ def before_scenario(context, scenario): + if active_tag_matcher.should_exclude_with(scenario.effective_tags): + scenario.skip(reason=active_tag_matcher.exclude_reason) + ++ + # ----------------------------------------------------------------------------- + # SPECIFIC FUNCTIONALITY: + # ----------------------------------------------------------------------------- +diff --git a/features/step.async_steps.feature b/features/step.async_steps.feature +index 06709d9..5919397 100644 +--- a/features/step.async_steps.feature ++++ b/features/step.async_steps.feature +@@ -1,4 +1,5 @@ + @not.with_python2=true ++@use.with_python_has_coroutine=true + Feature: Async-Test Support (async-step, ...) + + As a test writer and step provider +@@ -16,11 +17,11 @@ Feature: Async-Test Support (async-step, ...) + . * an async-function as coroutine using async/await keywords (Python 3.5) + . * an async-function tagged with @asyncio.coroutine and using "yield from" + . +- . # -- EXAMPLE CASE 1 (since Python 3.5): ++ . # -- EXAMPLE CASE 1 (since Python 3.5; preferred): + . async def coroutine1(duration): + . await asyncio.sleep(duration) + . +- . # -- EXAMPLE CASE 2 (since Python 3.4): ++ . # -- EXAMPLE CASE 2 (since Python 3.4; deprecated since Python 3.8; removed in Python 3.10): + . @asyncio.coroutine + . def coroutine2(duration): + . yield from asyncio.sleep(duration) +@@ -30,12 +31,8 @@ Feature: Async-Test Support (async-step, ...) + . The async-step can directly interact with other async-functions. + + +- @use.with_python.version=3.5 +- @use.with_python.version=3.6 +- @use.with_python.version=3.7 +- @use.with_python.version=3.8 +- @use.with_python.version=3.9 +- Scenario: Use async-step with @async_run_until_complete (async) ++ @use.with_python_has_async_function=true ++ Scenario: Use async-step with @async_run_until_complete (async; requires: py.version >= 3.5) + Given a new working directory + And a file named "features/steps/async_steps35.py" with: + """ +@@ -63,13 +60,8 @@ Feature: Async-Test Support (async-step, ...) + """ + + +- @use.with_python.version=3.4 +- @use.with_python.version=3.5 +- @use.with_python.version=3.6 +- @use.with_python.version=3.7 +- @use.with_python.version=3.8 +- @use.with_python.version=3.9 +- Scenario: Use async-step with @async_run_until_complete (@coroutine) ++ @use.with_python_has_asyncio.coroutine_decorator=true ++ Scenario: Use async-step with @async_run_until_complete (@asyncio.coroutine) + Given a new working directory + And a file named "features/steps/async_steps34.py" with: + """ +@@ -97,12 +89,8 @@ Feature: Async-Test Support (async-step, ...) + Given an async-step waits 0.3 seconds ... passed in 0.3 + """ + +- @use.with_python.version=3.5 +- @use.with_python.version=3.6 +- @use.with_python.version=3.7 +- @use.with_python.version=3.8 +- @use.with_python.version=3.9 +- Scenario: Use @async_run_until_complete(timeout=...) and TIMEOUT occurs (async) ++ @use.with_python_has_async_function=true ++ Scenario: Use @async_run_until_complete(timeout=...) and TIMEOUT occurs (async-function) + Given a new working directory + And a file named "features/steps/async_steps_timeout35.py" with: + """ +@@ -135,13 +123,9 @@ Feature: Async-Test Support (async-step, ...) + Assertion Failed: TIMEOUT-OCCURED: timeout=0.1 + """ + +- @use.with_python.version=3.5 +- @use.with_python.version=3.6 +- @use.with_python.version=3.7 +- @use.with_python.version=3.8 +- @use.with_python.version=3.9 ++ @use.with_python_has_async_function=true + @async_step_fails +- Scenario: Use @async_run_until_complete and async-step fails ++ Scenario: Use @async_run_until_complete and async-step fails (async-function) + Given a new working directory + And a file named "features/steps/async_steps_fails35.py" with: + """ +@@ -180,13 +164,9 @@ Feature: Async-Test Support (async-step, ...) + Assertion Failed: XFAIL in async-step + """ + +- @use.with_python.version=3.5 +- @use.with_python.version=3.6 +- @use.with_python.version=3.7 +- @use.with_python.version=3.8 +- @use.with_python.version=3.9 ++ @use.with_python_has_async_function=true + @async_step_fails +- Scenario: Use @async_run_until_complete and async-step raises error ++ Scenario: Use @async_run_until_complete and async-step raises error (async-function) + Given a new working directory + And a file named "features/steps/async_steps_exception35.py" with: + """ +@@ -225,13 +205,8 @@ Feature: Async-Test Support (async-step, ...) + raise RuntimeError("XFAIL in async-step") + """ + +- @use.with_python.version=3.4 +- @use.with_python.version=3.5 +- @use.with_python.version=3.6 +- @use.with_python.version=3.7 +- @use.with_python.version=3.8 +- @use.with_python.version=3.9 +- Scenario: Use @async_run_until_complete(timeout=...) and TIMEOUT occurs (@coroutine) ++ @use.with_python_has_asyncio.coroutine_decorator=true ++ Scenario: Use @async_run_until_complete(timeout=...) and TIMEOUT occurs (@asyncio.coroutine) + Given a new working directory + And a file named "features/steps/async_steps_timeout34.py" with: + """ +@@ -265,13 +240,8 @@ Feature: Async-Test Support (async-step, ...) + Assertion Failed: TIMEOUT-OCCURED: timeout=0.2 + """ + +- @use.with_python.version=3.4 +- @use.with_python.version=3.5 +- @use.with_python.version=3.6 +- @use.with_python.version=3.7 +- @use.with_python.version=3.8 +- @use.with_python.version=3.9 +- Scenario: Use async-dispatch and async-collect concepts ++ @use.with_python_has_asyncio.coroutine_decorator=true ++ Scenario: Use async-dispatch and async-collect concepts (@asyncio.coroutine) + Given a new working directory + And a file named "features/steps/async_dispatch_steps.py" with: + """ +diff --git a/issue.features/issue0330.feature b/issue.features/issue0330.feature +index be4d378..56ac238 100644 +--- a/issue.features/issue0330.feature ++++ b/issue.features/issue0330.feature +@@ -72,7 +72,8 @@ Feature: Issue #330: Skipped scenarios are included in junit reports when --no-s + + @not.with_python.version=3.8 + @not.with_python.version=3.9 +- Scenario: Junit report for skipped feature is created with --show-skipped ++ @not.with_python.version=3.10 ++ Scenario: Junit report for skipped feature is created with --show-skipped (py.version < 3.8) + When I run "behave --junit -t @tag1 --show-skipped @alice_and_bob.featureset" + Then it should pass with: + """ +@@ -87,7 +88,8 @@ Feature: Issue #330: Skipped scenarios are included in junit reports when --no-s + + @use.with_python.version=3.8 + @use.with_python.version=3.9 +- Scenario: Junit report for skipped feature is created with --show-skipped ++ @use.with_python.version=3.10 ++ Scenario: Junit report for skipped feature is created with --show-skipped (py.version >= 3.8) + When I run "behave --junit -t @tag1 --show-skipped @alice_and_bob.featureset" + Then it should pass with: + """ +@@ -104,7 +106,8 @@ Feature: Issue #330: Skipped scenarios are included in junit reports when --no-s + + @not.with_python.version=3.8 + @not.with_python.version=3.9 +- Scenario: Junit report for skipped scenario is neither shown nor counted with --no-skipped ++ @not.with_python.version=3.10 ++ Scenario: Junit report for skipped scenario is neither shown nor counted with --no-skipped (py.version < 3.8) + When I run "behave --junit -t @tag1 --no-skipped" + Then it should pass with: + """ +@@ -125,7 +128,8 @@ Feature: Issue #330: Skipped scenarios are included in junit reports when --no-s + + @use.with_python.version=3.8 + @use.with_python.version=3.9 +- Scenario: Junit report for skipped scenario is neither shown nor counted with --no-skipped ++ @use.with_python.version=3.10 ++ Scenario: Junit report for skipped scenario is neither shown nor counted with --no-skipped (py.version >= 3.8) + When I run "behave --junit -t @tag1 --no-skipped" + Then it should pass with: + """ +@@ -149,7 +153,8 @@ Feature: Issue #330: Skipped scenarios are included in junit reports when --no-s + + @not.with_python.version=3.8 + @not.with_python.version=3.9 +- Scenario: Junit report for skipped scenario is shown and counted with --show-skipped ++ @not.with_python.version=3.10 ++ Scenario: Junit report for skipped scenario is shown and counted with --show-skipped (py.version < 3.8) + When I run "behave --junit -t @tag1 --show-skipped" + Then it should pass with: + """ +@@ -171,7 +176,8 @@ Feature: Issue #330: Skipped scenarios are included in junit reports when --no-s + + @use.with_python.version=3.8 + @use.with_python.version=3.9 +- Scenario: Junit report for skipped scenario is shown and counted with --show-skipped ++ @use.with_python.version=3.10 ++ Scenario: Junit report for skipped scenario is shown and counted with --show-skipped (py.version >= 3.8) + When I run "behave --junit -t @tag1 --show-skipped" + Then it should pass with: + """ +diff --git a/issue.features/issue0446.feature b/issue.features/issue0446.feature +index 12de37a..d7db764 100644 +--- a/issue.features/issue0446.feature ++++ b/issue.features/issue0446.feature +@@ -60,7 +60,8 @@ Feature: Issue #446 -- Support scenario hook-errors with JUnitReporter + + @not.with_python.version=3.8 + @not.with_python.version=3.9 +- Scenario: Hook error in before_scenario() ++ @not.with_python.version=3.10 ++ Scenario: Hook error in before_scenario() (py.version < 3.8) + When I run "behave -f plain --junit features/before_scenario_failure.feature" + Then it should fail with: + """ +@@ -90,7 +91,8 @@ Feature: Issue #446 -- Support scenario hook-errors with JUnitReporter + + @use.with_python.version=3.8 + @use.with_python.version=3.9 +- Scenario: Hook error in before_scenario() ++ @use.with_python.version=3.10 ++ Scenario: Hook error in before_scenario() (py.version >= 3.8) + When I run "behave -f plain --junit features/before_scenario_failure.feature" + Then it should fail with: + """ +@@ -124,7 +126,8 @@ Feature: Issue #446 -- Support scenario hook-errors with JUnitReporter + + @not.with_python.version=3.8 + @not.with_python.version=3.9 +- Scenario: Hook error in after_scenario() ++ @not.with_python.version=3.10 ++ Scenario: Hook error in after_scenario() (py.version < 3.8) + When I run "behave -f plain --junit features/after_scenario_failure.feature" + Then it should fail with: + """ +@@ -156,7 +159,8 @@ Feature: Issue #446 -- Support scenario hook-errors with JUnitReporter + + @use.with_python.version=3.8 + @use.with_python.version=3.9 +- Scenario: Hook error in after_scenario() ++ @use.with_python.version=3.10 ++ Scenario: Hook error in after_scenario() (py.version >= 3.8) + When I run "behave -f plain --junit features/after_scenario_failure.feature" + Then it should fail with: + """ +diff --git a/issue.features/issue0457.feature b/issue.features/issue0457.feature +index 6d2f48f..c14c7a4 100644 +--- a/issue.features/issue0457.feature ++++ b/issue.features/issue0457.feature +@@ -26,7 +26,8 @@ Feature: Issue #457 -- Double-quotes in error messages of JUnit XML reports + + @not.with_python.version=3.8 + @not.with_python.version=3.9 +- Scenario: Use failing assertation in a JUnit XML report ++ @not.with_python.version=3.10 ++ Scenario: Use failing assertation in a JUnit XML report (py.version < 3.8) + Given a file named "features/fails1.feature" with: + """ + Feature: +@@ -48,7 +49,8 @@ Feature: Issue #457 -- Double-quotes in error messages of JUnit XML reports + + @use.with_python.version=3.8 + @use.with_python.version=3.9 +- Scenario: Use failing assertation in a JUnit XML report ++ @use.with_python.version=3.10 ++ Scenario: Use failing assertation in a JUnit XML report (py.version >= 3.8) + Given a file named "features/fails1.feature" with: + """ + Feature: +@@ -73,7 +75,8 @@ Feature: Issue #457 -- Double-quotes in error messages of JUnit XML reports + + @not.with_python.version=3.8 + @not.with_python.version=3.9 +- Scenario: Use exception in a JUnit XML report ++ @not.with_python.version=3.10 ++ Scenario: Use exception in a JUnit XML report (py.version < 3.8) + Given a file named "features/fails2.feature" with: + """ + Feature: +@@ -96,7 +99,8 @@ Feature: Issue #457 -- Double-quotes in error messages of JUnit XML reports + + @use.with_python.version=3.8 + @use.with_python.version=3.9 +- Scenario: Use exception in a JUnit XML report ++ @use.with_python.version=3.10 ++ Scenario: Use exception in a JUnit XML report (py.version >= 3.8) + Given a file named "features/fails2.feature" with: + """ + Feature: +diff --git a/issue.features/issue0657.feature b/issue.features/issue0657.feature +index aeaefd2..a674a26 100644 +--- a/issue.features/issue0657.feature ++++ b/issue.features/issue0657.feature +@@ -1,15 +1,10 @@ +-@not.with_python2=true + @issue ++@not.with_python2=true + Feature: Issue #657 -- Allow async steps with timeouts to fail when they raise exceptions + +- +- @use.with_python.version=3.5 +- @use.with_python.version=3.6 +- @use.with_python.version=3.7 +- @use.with_python.version=3.8 +- @use.with_python.version=3.9 ++ @use.with_python_has_async_function=true + @async_step_fails +- Scenario: Use @async_run_until_complete and async-step fails ++ Scenario: Use @async_run_until_complete and async-step fails (py.version >= 3.8) + Given a new working directory + And a file named "features/steps/async_steps_fails35.py" with: + """ +diff --git a/more.features/environment.py b/more.features/environment.py +index 9d4302b..14d904c 100644 +--- a/more.features/environment.py ++++ b/more.features/environment.py +@@ -1,16 +1,14 @@ + # -*- coding: UTF-8 -*- + + from behave.tag_matcher import ActiveTagMatcher, setup_active_tag_values +-import sys ++from behave import python_feature + + # -- MATCHES ANY TAGS: @use.with_{category}={value} + # NOTE: active_tag_value_provider provides category values for active tags. +-python_version = "%s.%s" % sys.version_info[:2] +-active_tag_value_provider = { +- "python.version": python_version, +-} ++active_tag_value_provider = python_feature.ACTIVE_TAG_VALUE_PROVIDER.copy() + active_tag_matcher = ActiveTagMatcher(active_tag_value_provider) + ++ + # ----------------------------------------------------------------------------- + # HOOKS: + # ----------------------------------------------------------------------------- +@@ -18,10 +16,12 @@ def before_all(context): + # -- SETUP ACTIVE-TAG MATCHER (with userdata): + setup_active_tag_values(active_tag_value_provider, context.config.userdata) + ++ + def before_feature(context, feature): + if active_tag_matcher.should_exclude_with(feature.tags): + feature.skip(reason=active_tag_matcher.exclude_reason) + ++ + def before_scenario(context, scenario): + if active_tag_matcher.should_exclude_with(scenario.effective_tags): + scenario.skip(reason=active_tag_matcher.exclude_reason) +diff --git a/more.features/run_examples.feature b/more.features/run_examples.feature +index 4778866..14acf98 100644 +--- a/more.features/run_examples.feature ++++ b/more.features/run_examples.feature +@@ -29,13 +29,8 @@ Feature: Ensure that all examples are usable + features/rule_fails.feature:16 F2 -- Fails + """ + +- +- @use.with_python.version=3.4 +- @use.with_python.version=3.5 +- @use.with_python.version=3.6 +- @use.with_python.version=3.7 +- @use.with_python.version=3.8 +- Scenario: examples/async_step (needs: py34 or newer) ++ @use.with_python_has_coroutine=true ++ Scenario: examples/async_step (requires: python.version >= 3.4) + Given I use the directory "examples/async_step" as working directory + When I run "behave features/" + Then it should pass diff --git a/meta-python/recipes-devtools/python/python3-behave/0112-docs-Update-new-and-noteworthy.patch b/meta-python/recipes-devtools/python/python3-behave/0112-docs-Update-new-and-noteworthy.patch new file mode 100644 index 000000000..d8ecc09ac --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0112-docs-Update-new-and-noteworthy.patch @@ -0,0 +1,84 @@ +From d38703153f7df501ceb48395f756b2427ee15e57 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sat, 9 Jan 2021 23:48:52 +0100 +Subject: [PATCH] docs: Update new and noteworthy + +* Add section on improved active-tag logic +--- + docs/conf.py | 2 +- + docs/new_and_noteworthy_v1.2.7.rst | 45 ++++++++++++++++++++++++++++++ + 2 files changed, 46 insertions(+), 1 deletion(-) + +diff --git a/docs/conf.py b/docs/conf.py +index 1579a36..cece86a 100644 +--- a/docs/conf.py ++++ b/docs/conf.py +@@ -108,7 +108,7 @@ if USE_SPHINX_INTERNATIONAL: + # ----------------------------------------------------------------------------- + project = u"behave" + authors = u"Jens Engel, Benno Rice and Richard Jones" +-copyright = u"2012-2020, %s" % authors ++copyright = u"2012-2021, %s" % authors + + # The version info for the project you're documenting, acts as replacement for + # |version| and |release|, also used in various other places throughout the +diff --git a/docs/new_and_noteworthy_v1.2.7.rst b/docs/new_and_noteworthy_v1.2.7.rst +index b7242f7..2eb94ad 100644 +--- a/docs/new_and_noteworthy_v1.2.7.rst ++++ b/docs/new_and_noteworthy_v1.2.7.rst +@@ -9,6 +9,7 @@ Summary: + to match partial tag names, like: ``@foo.*`` + * `Select-by-location for Scenario Containers`_ (Feature, Rule, ScenarioOutline) + * `Support for emojis in feature files and steps`_ ++* `Improve Active-Tags Logic`_ + + .. _`Example Mapping`: https://cucumber.io/blog/example-mapping-introduction/ + .. _`Example Mapping Webinar`: https://cucumber.io/blog/example-mapping-webinar/ +@@ -152,3 +153,47 @@ Support for Emojis in Feature Files and Steps + .. literalinclude:: ../features/steps/i18n_emoji_steps.py + :prepend: # -- FILE: features/steps/i18n_emoji_steps.py + :language: python ++ ++ ++Improve Active-Tags Logic ++------------------------------------------------------------------------------- ++ ++The active-tag computation logic was slightly changed (and fixed): ++ ++* if multiple active-tags with same category are used ++* combination of positive active-tags (``use.with_{category}={value}``) and ++ negative active-tags (``not.with_{category}={value}``) with same category ++ are now supported ++ ++All active-tags with same category are combined into one category tag-group. ++The following logical expression is used for active-tags with the 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 ++ ++ ++EXAMPLE: ++ ++.. code-block:: gherkin ++ ++ Feature: Active-Tag Example ++ ++ @use.with_browser=Safari ++ @use.with_browser=Chrome ++ @not.with_browser=Firefox ++ Scenario: Use one active-tag group/category ++ ++ HINT: Only executed with web browser Safari and Chrome, Firefox is explicitly excluded. ++ ... ++ ++ @use.with_browser=Firefox ++ @use.with_os=linux ++ @use.with_os=darwin ++ Scenario: Use two active-tag groups/categories ++ ++ HINT 1: Only executed with browser: Firefox ++ HINT 2: Only executed on OS: Linux and Darwin (macOS) ++ ... diff --git a/meta-python/recipes-devtools/python/python3-behave/0113-Add-diagnostic-helper-function-to-print-the-current-.patch b/meta-python/recipes-devtools/python/python3-behave/0113-Add-diagnostic-helper-function-to-print-the-current-.patch new file mode 100644 index 000000000..83b6b982e --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0113-Add-diagnostic-helper-function-to-print-the-current-.patch @@ -0,0 +1,278 @@ +From 18261d6c2dfd563ac542eb69d12a8ad648575c67 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sun, 10 Jan 2021 13:40:26 +0100 +Subject: [PATCH] * Add diagnostic helper function to print the current values + of active-tags. * Add helper classes for active-tag value providers. + +--- + behave/compat/collections.py | 25 ++++++ + behave/tag_matcher.py | 145 +++++++++++++++++++++++++++++++--- + features/environment.py | 9 +-- + issue.features/environment.py | 9 +-- + 4 files changed, 166 insertions(+), 22 deletions(-) + +diff --git a/behave/compat/collections.py b/behave/compat/collections.py +index 00444f4..f4eea2c 100644 +--- a/behave/compat/collections.py ++++ b/behave/compat/collections.py +@@ -18,3 +18,28 @@ except ImportError: # pragma: no cover + warnings.warn(message) + # -- BACKWARD-COMPATIBLE: Better than nothing (for behave use case). + OrderedDict = dict ++ ++try: ++ from collections import UserDict ++except ImportError: # pragma: no cover ++ class UserDict(object): ++ """Emulate collections.UserDict class in python3.""" ++ def __init__(self, data=None): ++ if data is None: ++ data = {} ++ self.data = data ++ ++ def __len__(self): ++ return len(self.data) ++ ++ def __iter__(self): ++ return len(self.data) ++ ++ def keys(self): ++ return self.data.keys() ++ ++ def values(self): ++ return self.data.values() ++ ++ def items(self): ++ return self.data.items() +diff --git a/behave/tag_matcher.py b/behave/tag_matcher.py +index e2b1e82..78d7061 100644 +--- a/behave/tag_matcher.py ++++ b/behave/tag_matcher.py +@@ -4,17 +4,16 @@ 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 ++from __future__ import absolute_import, print_function + import re +-import operator + import six ++from ._types import Unknown ++from .compat.collections import UserDict + + +-def bool_to_string(value): +- """Converts a Boolean value into its normalized string representation.""" +- return str(bool(value)).lower() +- +- ++# ----------------------------------------------------------------------------- ++# CLASSES FOR: Active-Tags and ActiveTagMatchers ++# ----------------------------------------------------------------------------- + class TagMatcher(object): + """Abstract base class that defines the TagMatcher protocol.""" + +@@ -220,8 +219,8 @@ class ActiveTagMatcher(TagMatcher): + # -- CASE: Empty group is always enabled (CORNER-CASE). + return True + +- current_value = self.value_provider.get(group_category, None) +- if current_value is None and self.ignore_unknown_categories: ++ current_value = self.value_provider.get(group_category, Unknown) ++ if current_value is Unknown and self.ignore_unknown_categories: + # -- CASE: Unknown category, ignore it. + return True + +@@ -320,6 +319,115 @@ class CompositeTagMatcher(TagMatcher): + return False + + ++# ----------------------------------------------------------------------------- ++# ACTIVE TAG VALUE PROVIDER CLASSES: ++# ----------------------------------------------------------------------------- ++class IActiveTagValueProvider(object): ++ """Protocol/Interface for active-tag value providers.""" ++ ++ def get(self, category, default=None): ++ return NotImplemented ++ ++ ++class ActiveTagValueProvider(UserDict): ++ def __init__(self, data=None): ++ if data is None: ++ data = {} ++ UserDict.__init__(self, data) ++ ++ @staticmethod ++ def use_value(value): ++ if callable(value): ++ # -- RE-EVALUATE VALUE: Each time ++ value_func = value ++ value = value_func() ++ return value ++ ++ def __getitem__(self, name): ++ value = self.data[name] ++ return self.use_value(value) ++ ++ def get(self, category, default=None): ++ value = self.data.get(category, default) ++ return self.use_value(value) ++ ++ def values(self): ++ for value in self.data.values(self): ++ yield self.use_value(value) ++ ++ def items(self): ++ for category, value in self.data.items(): ++ yield (category, self.use_value(value)) ++ ++ def categories(self): ++ return self.keys() ++ ++ ++class CompositeActiveTagValueProvider(ActiveTagValueProvider): ++ """Provides a composite helper class to resolve active-tag values ++ from a list of value-providers. ++ """ ++ ++ def __init__(self, value_providers=None): ++ if value_providers is None: ++ value_providers = [] ++ super(CompositeActiveTagValueProvider, self).__init__() ++ self.value_providers = list(value_providers) ++ ++ def get(self, category, default=None): ++ # -- FIRST: Check category cached-map (=self.data) ++ value = self.data.get(category, Unknown) ++ if value is Unknown: ++ # -- NOT DISCOVERED: Search over value_providers. ++ for value_provider in self.value_providers: ++ value = value_provider.get(category, Unknown) ++ if value is Unknown: ++ continue ++ ++ # -- FOUND CATEGORY: ++ self.data[category] = value ++ break ++ # -- FOUND-CATEGORY or NOT-FOUND: ++ if value is Unknown: ++ value = default ++ ++ return self.use_value(value) ++ ++ # -- MORE: Provide a dict-like interface. ++ def keys(self): ++ for value_provider in self.value_providers: ++ try: ++ for category in value_provider.keys(): ++ yield category ++ except AttributeError: ++ # -- keys() method not supported. ++ pass ++ ++ def values(self): ++ for category in self.keys(): ++ value = self.get(category) ++ yield value ++ ++ def items(self): ++ for category in self.keys(): ++ value = self.get(category) ++ yield (category, value) ++ ++ ++ ++# ----------------------------------------------------------------------------- ++# UTILITY FUNCTIONS: ++# ----------------------------------------------------------------------------- ++def bool_to_string(value): ++ """Converts a boolean active-tag value into its normalized ++ string representation. ++ ++ :param value: Boolean value to use (or value converted into bool). ++ :returns: Boolean value converted into a normalized string. ++ """ ++ return str(bool(value)).lower() ++ ++ + def setup_active_tag_values(active_tag_values, data): + """Setup/update active_tag values with dict-like data. + Only values for keys that are already present are updated. +@@ -330,3 +438,22 @@ def setup_active_tag_values(active_tag_values, data): + for category in list(active_tag_values.keys()): + if category in data: + active_tag_values[category] = data[category] ++ ++ ++def print_active_tags(active_tag_value_provider, categories=None): ++ """Print a summary of the current active-tag values.""" ++ if categories is None: ++ try: ++ categories = list(active_tag_value_provider) ++ except TypeError: # TypeError: object is not iterable ++ categories = [] ++ ++ active_tag_data = active_tag_value_provider ++ print("ACTIVE-TAGS:") ++ for category in categories: ++ active_tag_value = active_tag_data.get(category) ++ print("use.with_{category}={value}".format( ++ category=category, value=active_tag_value)) ++ ++ # -- FINALLY: TRAILING NEW-LINE ++ print() +diff --git a/features/environment.py b/features/environment.py +index 6faf4e2..72ebaa0 100644 +--- a/features/environment.py ++++ b/features/environment.py +@@ -2,7 +2,8 @@ + # FILE: features/environemnt.py + + from __future__ import absolute_import, print_function +-from behave.tag_matcher import ActiveTagMatcher, setup_active_tag_values ++from behave.tag_matcher import \ ++ ActiveTagMatcher, setup_active_tag_values, print_active_tags + from behave4cmd0.setup_command_shell import setup_command_shell_processors4behave + from behave import python_feature + import platform +@@ -21,11 +22,7 @@ active_tag_matcher = ActiveTagMatcher(active_tag_value_provider) + + + def print_active_tags_summary(): +- active_tag_data = active_tag_value_provider +- print("ACTIVE-TAG SUMMARY:") +- print("use.with_python.version=%s" % active_tag_data.get("python.version")) +- print("use.with_os=%s" % active_tag_data.get("os")) +- print() ++ print_active_tags(active_tag_value_provider, ["python.version", "os"]) + + + # ----------------------------------------------------------------------------- +diff --git a/issue.features/environment.py b/issue.features/environment.py +index 7e48ee0..ab85e6c 100644 +--- a/issue.features/environment.py ++++ b/issue.features/environment.py +@@ -13,7 +13,7 @@ import sys + import platform + import os.path + import six +-from behave.tag_matcher import ActiveTagMatcher ++from behave.tag_matcher import ActiveTagMatcher, print_active_tags + from behave4cmd0.setup_command_shell import setup_command_shell_processors4behave + # PREPARED: from behave.tag_matcher import setup_active_tag_values + +@@ -94,12 +94,7 @@ active_tag_matcher = ActiveTagMatcher(active_tag_value_provider) + + + def print_active_tags_summary(): +- active_tag_data = active_tag_value_provider +- print("ACTIVE-TAG SUMMARY:") +- print("use.with_python.version=%s" % active_tag_data.get("python.version")) +- # print("use.with_platform=%s" % active_tag_data.get("platform")) +- # print("use.with_os=%s" % active_tag_data.get("os")) +- print() ++ print_active_tags(active_tag_value_provider, ["python.version", "os"]) + + + # --------------------------------------------------------------------------- diff --git a/meta-python/recipes-devtools/python/python3-behave/0114-UPDATE-i18n-gherkin-languages.json-from-cucumber-rep.patch b/meta-python/recipes-devtools/python/python3-behave/0114-UPDATE-i18n-gherkin-languages.json-from-cucumber-rep.patch new file mode 100644 index 000000000..9d370c299 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0114-UPDATE-i18n-gherkin-languages.json-from-cucumber-rep.patch @@ -0,0 +1,541 @@ +From 715079cc08a7672d51c376a9d884873469ffa009 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Tue, 12 Jan 2021 13:21:44 +0100 +Subject: [PATCH] UPDATE: i18n/gherkin-languages.json from cucumber repository. + +CONTAINS: PR #827 (Estonian language updates contained) +LAST COMMIT: 2021-01-07 (in Cucumber repository) +LANGUAGE CHANGES: + +* Swedish: Rule keyword +* Telugu: Fixing ISO 639-1 code +* Russian: Rule keyword +* Hebrew: Rule keyword +* German: Addition keywords +* Estonian: Rule keyword, ScenarioOutline keyword +* Indonesian: Given keywords, whitespace +--- + behave/i18n.py | 92 ++++++++++++------ + etc/gherkin/gherkin-languages.json | 145 +++++++++++++++++++++++++---- + features/cmdline.lang_list.feature | 64 +++++++++---- + issue.features/issue0309.feature | 2 +- + 4 files changed, 240 insertions(+), 63 deletions(-) + +diff --git a/behave/i18n.py b/behave/i18n.py +index 2781afe..a572626 100644 +--- a/behave/i18n.py ++++ b/behave/i18n.py +@@ -188,16 +188,19 @@ languages = \ + 'then': ['* ', 'Så '], + 'when': ['* ', 'Når ']}, + 'de': {'and': ['* ', 'Und '], +- 'background': ['Grundlage'], ++ 'background': ['Grundlage', ++ 'Hintergrund', ++ 'Voraussetzungen', ++ 'Vorbedingungen'], + 'but': ['* ', 'Aber '], + 'examples': ['Beispiele'], +- 'feature': ['Funktionalität'], ++ 'feature': ['Funktionalität', 'Funktion'], + 'given': ['* ', 'Angenommen ', 'Gegeben sei ', 'Gegeben seien '], + 'name': 'German', + 'native': 'Deutsch', +- 'rule': ['Rule'], ++ 'rule': ['Rule', 'Regel'], + 'scenario': ['Beispiel', 'Szenario'], +- 'scenario_outline': ['Szenariogrundriss'], ++ 'scenario_outline': ['Szenariogrundriss', 'Szenarien'], + 'then': ['* ', 'Dann '], + 'when': ['* ', 'Wenn ']}, + 'el': {'and': ['* ', 'Και '], +@@ -344,9 +347,9 @@ languages = \ + 'given': ['* ', 'Eeldades '], + 'name': 'Estonian', + 'native': 'eesti keel', +- 'rule': ['Rule'], ++ 'rule': ['Reegel'], + 'scenario': ['Juhtum', 'Stsenaarium'], +- 'scenario_outline': ['Raamstjuhtum', 'Raamstsenaarium'], ++ 'scenario_outline': ['Raamjuhtum', 'Raamstsenaarium'], + 'then': ['* ', 'Siis '], + 'when': ['* ', 'Kui ']}, + 'fa': {'and': ['* ', 'و '], +@@ -455,7 +458,7 @@ languages = \ + 'given': ['* ', 'בהינתן '], + 'name': 'Hebrew', + 'native': 'עברית', +- 'rule': ['Rule'], ++ 'rule': ['כלל'], + 'scenario': ['דוגמא', 'תרחיש'], + 'scenario_outline': ['תבנית תרחיש'], + 'then': ['* ', 'אז ', 'אזי '], +@@ -518,17 +521,22 @@ languages = \ + 'then': ['* ', 'Akkor '], + 'when': ['* ', 'Majd ', 'Ha ', 'Amikor ']}, + 'id': {'and': ['* ', 'Dan '], +- 'background': ['Dasar'], +- 'but': ['* ', 'Tapi '], +- 'examples': ['Contoh'], ++ 'background': ['Dasar', 'Latar Belakang'], ++ 'but': ['* ', 'Tapi ', 'Tetapi '], ++ 'examples': ['Contoh', 'Misal'], + 'feature': ['Fitur'], +- 'given': ['* ', 'Dengan '], ++ 'given': ['* ', ++ 'Dengan ', ++ 'Diketahui ', ++ 'Diasumsikan ', ++ 'Bila ', ++ 'Jika '], + 'name': 'Indonesian', + 'native': 'Bahasa Indonesia', +- 'rule': ['Rule'], ++ 'rule': ['Rule', 'Aturan'], + 'scenario': ['Skenario'], +- 'scenario_outline': ['Skenario konsep'], +- 'then': ['* ', 'Maka '], ++ 'scenario_outline': ['Skenario konsep', 'Garis-Besar Skenario'], ++ 'then': ['* ', 'Maka ', 'Kemudian '], + 'when': ['* ', 'Ketika ']}, + 'is': {'and': ['* ', 'Og '], + 'background': ['Bakgrunnur'], +@@ -699,6 +707,32 @@ languages = \ + 'scenario_outline': ['Сценарын төлөвлөгөө'], + 'then': ['* ', 'Тэгэхэд ', 'Үүний дараа '], + 'when': ['* ', 'Хэрэв ']}, ++ 'mr': {'and': ['* ', 'आणि ', 'तसेच '], ++ 'background': ['पार्श्वभूमी'], ++ 'but': ['* ', 'पण ', 'परंतु '], ++ 'examples': ['उदाहरण'], ++ 'feature': ['वैशिष्ट्य', 'सुविधा'], ++ 'given': ['* ', 'जर', 'दिलेल्या प्रमाणे '], ++ 'name': 'Marathi', ++ 'native': 'मराठी', ++ 'rule': ['नियम'], ++ 'scenario': ['परिदृश्य'], ++ 'scenario_outline': ['परिदृश्य रूपरेखा'], ++ 'then': ['* ', 'मग ', 'तेव्हा '], ++ 'when': ['* ', 'जेव्हा ']}, ++ 'ne': {'and': ['* ', 'र ', 'अनी '], ++ 'background': ['पृष्ठभूमी'], ++ 'but': ['* ', 'तर '], ++ 'examples': ['उदाहरण', 'उदाहरणहरु'], ++ 'feature': ['सुविधा', 'विशेषता'], ++ 'given': ['* ', 'दिइएको ', 'दिएको ', 'यदि '], ++ 'name': 'Nepali', ++ 'native': 'नेपाली', ++ 'rule': ['नियम'], ++ 'scenario': ['परिदृश्य'], ++ 'scenario_outline': ['परिदृश्य रूपरेखा'], ++ 'then': ['* ', 'त्यसपछि ', 'अनी '], ++ 'when': ['* ', 'जब ']}, + 'nl': {'and': ['* ', 'En '], + 'background': ['Achtergrond'], + 'but': ['* ', 'Maar '], +@@ -797,7 +831,7 @@ languages = \ + 'given': ['* ', 'Допустим ', 'Дано ', 'Пусть '], + 'name': 'Russian', + 'native': 'русский', +- 'rule': ['Rule'], ++ 'rule': ['Правило'], + 'scenario': ['Пример', 'Сценарий'], + 'scenario_outline': ['Структура сценария'], + 'then': ['* ', 'То ', 'Затем ', 'Тогда '], +@@ -873,7 +907,7 @@ languages = \ + 'given': ['* ', 'Givet '], + 'name': 'Swedish', + 'native': 'Svenska', +- 'rule': ['Rule'], ++ 'rule': ['Regel'], + 'scenario': ['Scenario'], + 'scenario_outline': ['Abstrakt Scenario', 'Scenariomall'], + 'then': ['* ', 'Så '], +@@ -891,6 +925,19 @@ languages = \ + 'scenario_outline': ['காட்சி சுருக்கம்', 'காட்சி வார்ப்புரு'], + 'then': ['* ', 'அப்பொழுது '], + 'when': ['* ', 'எப்போது ']}, ++ 'te': {'and': ['* ', 'మరియు '], ++ 'background': ['నేపథ్యం'], ++ 'but': ['* ', 'కాని '], ++ 'examples': ['ఉదాహరణలు'], ++ 'feature': ['గుణము'], ++ 'given': ['* ', 'చెప్పబడినది '], ++ 'name': 'Telugu', ++ 'native': 'తెలుగు', ++ 'rule': ['Rule'], ++ 'scenario': ['ఉదాహరణ', 'సన్నివేశం'], ++ 'scenario_outline': ['కథనం'], ++ 'then': ['* ', 'అప్పుడు '], ++ 'when': ['* ', 'ఈ పరిస్థితిలో ']}, + 'th': {'and': ['* ', 'และ '], + 'background': ['แนวคิด'], + 'but': ['* ', 'แต่ '], +@@ -904,19 +951,6 @@ languages = \ + 'scenario_outline': ['สรุปเหตุการณ์', 'โครงสร้างของเหตุการณ์'], + 'then': ['* ', 'ดังนั้น '], + 'when': ['* ', 'เมื่อ ']}, +- 'tl': {'and': ['* ', 'మరియు '], +- 'background': ['నేపథ్యం'], +- 'but': ['* ', 'కాని '], +- 'examples': ['ఉదాహరణలు'], +- 'feature': ['గుణము'], +- 'given': ['* ', 'చెప్పబడినది '], +- 'name': 'Telugu', +- 'native': 'తెలుగు', +- 'rule': ['Rule'], +- 'scenario': ['ఉదాహరణ', 'సన్నివేశం'], +- 'scenario_outline': ['కథనం'], +- 'then': ['* ', 'అప్పుడు '], +- 'when': ['* ', 'ఈ పరిస్థితిలో ']}, + 'tlh': {'and': ['* ', "'ej ", 'latlh '], + 'background': ["mo'"], + 'but': ['* ', "'ach ", "'a "], +diff --git a/etc/gherkin/gherkin-languages.json b/etc/gherkin/gherkin-languages.json +index 29cbca1..6069664 100644 +--- a/etc/gherkin/gherkin-languages.json ++++ b/etc/gherkin/gherkin-languages.json +@@ -605,7 +605,10 @@ + "Und " + ], + "background": [ +- "Grundlage" ++ "Grundlage", ++ "Hintergrund", ++ "Voraussetzungen", ++ "Vorbedingungen" + ], + "but": [ + "* ", +@@ -615,7 +618,8 @@ + "Beispiele" + ], + "feature": [ +- "Funktionalität" ++ "Funktionalität", ++ "Funktion" + ], + "given": [ + "* ", +@@ -626,14 +630,16 @@ + "name": "German", + "native": "Deutsch", + "rule": [ +- "Rule" ++ "Rule", ++ "Regel" + ], + "scenario": [ + "Beispiel", + "Szenario" + ], + "scenarioOutline": [ +- "Szenariogrundriss" ++ "Szenariogrundriss", ++ "Szenarien" + ], + "then": [ + "* ", +@@ -1127,14 +1133,14 @@ + "name": "Estonian", + "native": "eesti keel", + "rule": [ +- "Rule" ++ "Reegel" + ], + "scenario": [ + "Juhtum", + "Stsenaarium" + ], + "scenarioOutline": [ +- "Raamstjuhtum", ++ "Raamjuhtum", + "Raamstsenaarium" + ], + "then": [ +@@ -1465,7 +1471,7 @@ + "name": "Hebrew", + "native": "עברית", + "rule": [ +- "Rule" ++ "כלל" + ], + "scenario": [ + "דוגמא", +@@ -1692,36 +1698,46 @@ + "Dan " + ], + "background": [ +- "Dasar" ++ "Dasar", ++ "Latar Belakang" + ], + "but": [ + "* ", +- "Tapi " ++ "Tapi ", ++ "Tetapi " + ], + "examples": [ +- "Contoh" ++ "Contoh", ++ "Misal" + ], + "feature": [ + "Fitur" + ], + "given": [ + "* ", +- "Dengan " ++ "Dengan ", ++ "Diketahui ", ++ "Diasumsikan ", ++ "Bila ", ++ "Jika " + ], + "name": "Indonesian", + "native": "Bahasa Indonesia", + "rule": [ +- "Rule" ++ "Rule", ++ "Aturan" + ], + "scenario": [ + "Skenario" + ], + "scenarioOutline": [ +- "Skenario konsep" ++ "Skenario konsep", ++ "Garis-Besar Skenario" + ], + "then": [ + "* ", +- "Maka " ++ "Maka ", ++ "Kemudian " + ], + "when": [ + "* ", +@@ -2330,6 +2346,54 @@ + "Хэрэв " + ] + }, ++ "ne": { ++ "and": [ ++ "* ", ++ "र ", ++ "अनी " ++ ], ++ "background": [ ++ "पृष्ठभूमी" ++ ], ++ "but": [ ++ "* ", ++ "तर " ++ ], ++ "examples": [ ++ "उदाहरण", ++ "उदाहरणहरु" ++ ], ++ "feature": [ ++ "सुविधा", ++ "विशेषता" ++ ], ++ "given": [ ++ "* ", ++ "दिइएको ", ++ "दिएको ", ++ "यदि " ++ ], ++ "name": "Nepali", ++ "native": "नेपाली", ++ "rule": [ ++ "नियम" ++ ], ++ "scenario": [ ++ "परिदृश्य" ++ ], ++ "scenarioOutline": [ ++ "परिदृश्य रूपरेखा" ++ ], ++ "then": [ ++ "* ", ++ "त्यसपछि ", ++ "अनी " ++ ], ++ "when": [ ++ "* ", ++ "जब " ++ ] ++ }, + "nl": { + "and": [ + "* ", +@@ -2665,7 +2729,7 @@ + "name": "Russian", + "native": "русский", + "rule": [ +- "Rule" ++ "Правило" + ], + "scenario": [ + "Пример", +@@ -2933,7 +2997,7 @@ + "name": "Swedish", + "native": "Svenska", + "rule": [ +- "Rule" ++ "Regel" + ], + "scenario": [ + "Scenario" +@@ -3046,7 +3110,7 @@ + "เมื่อ " + ] + }, +- "tl": { ++ "te": { + "and": [ + "* ", + "మరియు " +@@ -3510,5 +3574,52 @@ + "* ", + "當" + ] ++ }, ++ "mr": { ++ "and": [ ++ "* ", ++ "आणि ", ++ "तसेच " ++ ], ++ "background": [ ++ "पार्श्वभूमी" ++ ], ++ "but": [ ++ "* ", ++ "पण ", ++ "परंतु " ++ ], ++ "examples": [ ++ "उदाहरण" ++ ], ++ "feature": [ ++ "वैशिष्ट्य", ++ "सुविधा" ++ ], ++ "given": [ ++ "* ", ++ "जर", ++ "दिलेल्या प्रमाणे " ++ ], ++ "name": "Marathi", ++ "native": "मराठी", ++ "rule": [ ++ "नियम" ++ ], ++ "scenario": [ ++ "परिदृश्य" ++ ], ++ "scenarioOutline": [ ++ "परिदृश्य रूपरेखा" ++ ], ++ "then": [ ++ "* ", ++ "मग ", ++ "तेव्हा " ++ ], ++ "when": [ ++ "* ", ++ "जेव्हा " ++ ] + } + } +diff --git a/features/cmdline.lang_list.feature b/features/cmdline.lang_list.feature +index f77e747..20822ec 100644 +--- a/features/cmdline.lang_list.feature ++++ b/features/cmdline.lang_list.feature +@@ -39,21 +39,53 @@ Feature: Command-line options: Use behave --lang-list + fa: فارسی / Persian + fi: suomi / Finnish + fr: français / French +- """ +- And the command output should contain: +- """ +- sv: Svenska / Swedish +- ta: தமிழ் / Tamil +- th: ไทย / Thai +- tl: తెలుగు / Telugu +- tlh: tlhIngan / Klingon +- tr: Türkçe / Turkish +- tt: Татарча / Tatar +- uk: Українська / Ukrainian +- ur: اردو / Urdu +- uz: Узбекча / Uzbek +- vi: Tiếng Việt / Vietnamese +- zh-CN: 简体中文 / Chinese simplified +- zh-TW: 繁體中文 / Chinese traditional ++ ga: Gaeilge / Irish ++ gj: ગુજરાતી / Gujarati ++ gl: galego / Galician ++ he: עברית / Hebrew ++ hi: हिंदी / Hindi ++ hr: hrvatski / Croatian ++ ht: kreyòl / Creole ++ hu: magyar / Hungarian ++ id: Bahasa Indonesia / Indonesian ++ is: Íslenska / Icelandic ++ it: italiano / Italian ++ ja: 日本語 / Japanese ++ jv: Basa Jawa / Javanese ++ ka: ქართველი / Georgian ++ kn: ಕನ್ನಡ / Kannada ++ ko: 한국어 / Korean ++ lt: lietuvių kalba / Lithuanian ++ lu: Lëtzebuergesch / Luxemburgish ++ lv: latviešu / Latvian ++ mk-Cyrl: Македонски / Macedonian ++ mk-Latn: Makedonski (Latinica) / Macedonian (Latin) ++ mn: монгол / Mongolian ++ mr: मराठी / Marathi ++ ne: नेपाली / Nepali ++ nl: Nederlands / Dutch ++ no: norsk / Norwegian ++ pa: ਪੰਜਾਬੀ / Panjabi ++ pl: polski / Polish ++ pt: português / Portuguese ++ ro: română / Romanian ++ ru: русский / Russian ++ sk: Slovensky / Slovak ++ sl: Slovenski / Slovenian ++ sr-Cyrl: Српски / Serbian ++ sr-Latn: Srpski (Latinica) / Serbian (Latin) ++ sv: Svenska / Swedish ++ ta: தமிழ் / Tamil ++ te: తెలుగు / Telugu ++ th: ไทย / Thai ++ tlh: tlhIngan / Klingon ++ tr: Türkçe / Turkish ++ tt: Татарча / Tatar ++ uk: Українська / Ukrainian ++ ur: اردو / Urdu ++ uz: Узбекча / Uzbek ++ vi: Tiếng Việt / Vietnamese ++ zh-CN: 简体中文 / Chinese simplified ++ zh-TW: 繁體中文 / Chinese traditional + """ + But the command output should not contain "Traceback" +diff --git a/issue.features/issue0309.feature b/issue.features/issue0309.feature +index c8a8857..b50e32d 100644 +--- a/issue.features/issue0309.feature ++++ b/issue.features/issue0309.feature +@@ -52,8 +52,8 @@ Feature: Issue #309 -- behave --lang-list fails on Python3 + """ + sv: Svenska / Swedish + ta: தமிழ் / Tamil ++ te: తెలుగు / Telugu + th: ไทย / Thai +- tl: తెలుగు / Telugu + tlh: tlhIngan / Klingon + tr: Türkçe / Turkish + tt: Татарча / Tatar diff --git a/meta-python/recipes-devtools/python/python3-behave/0115-UPDATE-CHANGES-Related-to-PR-895-and-827.patch b/meta-python/recipes-devtools/python/python3-behave/0115-UPDATE-CHANGES-Related-to-PR-895-and-827.patch new file mode 100644 index 000000000..44532ab9b --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0115-UPDATE-CHANGES-Related-to-PR-895-and-827.patch @@ -0,0 +1,22 @@ +From cfbf492ce859c817703e4c986d9ab9dcea30223a Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Tue, 12 Jan 2021 13:40:58 +0100 +Subject: [PATCH] UPDATE CHANGES: Related to PR #895 and #827 + +--- + CHANGES.rst | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/CHANGES.rst b/CHANGES.rst +index a3398d8..ff82132 100644 +--- a/CHANGES.rst ++++ b/CHANGES.rst +@@ -28,6 +28,8 @@ ENHANCEMENTS: + * Use cucumber "gherkin-languages.json" now (simplify: Gherkin v6 aliases, language usage) + * Support emojis in ``*.feature`` files and steps + * Select-by-location: Add support for "Scenario container" (Feature, Rule, ScenarioOutline) (related to: #391) ++* pull #895: UPDATE: i18n/gherkin-languages.json from cucumber repository #895 (related to: #827) ++* pull #827: Fixed keyword translation in Estonian #827 (provided by: ookull) + * issue #740: Enhancement: possibility to add cleanup to be called upon leaving outer context stack frames (submitted by: nizwiz, dcvmoole) + * issue #678: Scenario Outline: Support tags with commas and semicolons (provided by: lawnmowerlatte, pull #679) + * issue #675: Feature files cannot be found within symlink directories (provided by: smadness, pull #680) diff --git a/meta-python/recipes-devtools/python/python3-behave/0116-FIX-CI-TRAVIS-For-python-2.7-builds-mock-4.0-only-fo.patch b/meta-python/recipes-devtools/python/python3-behave/0116-FIX-CI-TRAVIS-For-python-2.7-builds-mock-4.0-only-fo.patch new file mode 100644 index 000000000..78127d14e --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0116-FIX-CI-TRAVIS-For-python-2.7-builds-mock-4.0-only-fo.patch @@ -0,0 +1,39 @@ +From ac1d98381de62b900af89aee5127c901fbe82c38 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Tue, 12 Jan 2021 13:51:02 +0100 +Subject: [PATCH] FIX CI-TRAVIS: For python 2.7 builds (mock >= 4.0 only for + python.version >= 3.6) + +--- + py.requirements/ci.travis.txt | 3 ++- + py.requirements/testing.txt | 3 ++- + 2 files changed, 4 insertions(+), 2 deletions(-) + +diff --git a/py.requirements/ci.travis.txt b/py.requirements/ci.travis.txt +index cbc60c0..1d6e050 100644 +--- a/py.requirements/ci.travis.txt ++++ b/py.requirements/ci.travis.txt +@@ -6,7 +6,8 @@ pytest < 5.0; python_version < '3.0' + pytest >= 5.0; python_version >= '3.0' + + pytest-html >= 1.19.0,<2.0 +-mock >= 2.0 ++mock < 4.0; python_version < '3.6' ++mock >= 4.0; python_version >= '3.6' + PyHamcrest >= 2.0.2; python_version >= '3.0' + PyHamcrest < 2.0; python_version < '3.0' + +diff --git a/py.requirements/testing.txt b/py.requirements/testing.txt +index fc8fd82..9230c1f 100644 +--- a/py.requirements/testing.txt ++++ b/py.requirements/testing.txt +@@ -8,7 +8,8 @@ pytest < 5.0; python_version < '3.0' # pytest >= 4.2 + pytest >= 5.0; python_version >= '3.0' + + pytest-html >= 1.19.0,<2.0 +-mock >= 2.0 ++mock < 4.0; python_version < '3.6' ++mock >= 4.0; python_version >= '3.6' + PyHamcrest >= 2.0.2; python_version >= '3.0' + PyHamcrest < 2.0; python_version < '3.0' + diff --git a/meta-python/recipes-devtools/python/python3-behave/0117-Adds-the-ability-to-use-a-custom-runner-in-the-behav.patch b/meta-python/recipes-devtools/python/python3-behave/0117-Adds-the-ability-to-use-a-custom-runner-in-the-behav.patch new file mode 100644 index 000000000..17ee076d8 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0117-Adds-the-ability-to-use-a-custom-runner-in-the-behav.patch @@ -0,0 +1,126 @@ +From 346aa055cd0cde2cc6a1fe92d874644b06dbdb17 Mon Sep 17 00:00:00 2001 +From: kingbuzzman +Date: Fri, 19 Jun 2020 09:18:51 -0400 +Subject: [PATCH] Adds the ability to use a custom runner in the behave command + +--- + behave/__main__.py | 2 +- + behave/configuration.py | 16 ++++++++++++++++ + docs/behave.rst | 5 +++++ + tests/unit/test_configuration.py | 19 +++++++++++++++++++ + 4 files changed, 41 insertions(+), 1 deletion(-) + +diff --git a/behave/__main__.py b/behave/__main__.py +index 3cae36d..edb99c4 100644 +--- a/behave/__main__.py ++++ b/behave/__main__.py +@@ -215,7 +215,7 @@ def main(args=None): + :return: 0, if successful. Non-zero, in case of errors/failures. + """ + config = Configuration(args) +- return run_behave(config) ++ return run_behave(config, runner_class=config.runner_class) + + + if __name__ == "__main__": +diff --git a/behave/configuration.py b/behave/configuration.py +index 65e2e3e..04c014a 100644 +--- a/behave/configuration.py ++++ b/behave/configuration.py +@@ -8,6 +8,7 @@ import re + import sys + import shlex + import six ++from importlib import import_module + from six.moves import configparser + + from behave.model import ScenarioOutline +@@ -65,6 +66,16 @@ class LogLevel(object): + # ----------------------------------------------------------------------------- + # CONFIGURATION SCHEMA: + # ----------------------------------------------------------------------------- ++ ++def valid_python_module(path): ++ try: ++ module_path, class_name = path.rsplit('.', 1) ++ module = import_module(module_path) ++ return getattr(module, class_name) ++ except (ValueError, AttributeError, ImportError): ++ raise argparse.ArgumentTypeError("No module named '%s' was found." % path) ++ ++ + options = [ + (("-c", "--no-color"), + dict(action="store_false", dest="color", +@@ -111,6 +122,11 @@ options = [ + dict(metavar="PATH", dest="junit_directory", + default="reports", + help="""Directory in which to store JUnit reports.""")), ++ ++ (("--runner-class",), ++ dict(action="store", ++ default="behave.runner.Runner", type=valid_python_module, ++ help="Tells Behave to use a specific runner. (default: %(default)s)")), + + ((), # -- CONFIGFILE only + dict(dest="default_format", +diff --git a/docs/behave.rst b/docs/behave.rst +index 25ce523..8c1c125 100644 +--- a/docs/behave.rst ++++ b/docs/behave.rst +@@ -55,6 +55,11 @@ You may see the same information presented below at any time using ``behave + + Directory in which to store JUnit reports. + ++.. option:: --runner-class ++ ++ This allows you to use your own custom runner. The default is ++ ``behave.runner.Runner``. ++ + .. option:: -f, --format + + Specify a formatter. If none is specified the default formatter is +diff --git a/tests/unit/test_configuration.py b/tests/unit/test_configuration.py +index c96cf63..025a6d0 100644 +--- a/tests/unit/test_configuration.py ++++ b/tests/unit/test_configuration.py +@@ -5,6 +5,7 @@ import six + import pytest + from behave import configuration + from behave.configuration import Configuration, UserData ++from behave.runner import Runner as BaseRunner + from unittest import TestCase + + +@@ -37,6 +38,10 @@ if sys.platform.startswith("win"): + ROOTDIR_PREFIX = os.environ.get("BEHAVE_ROOTDIR_PREFIX", ROOTDIR_PREFIX_DEFAULT) + + ++class CustomTestRunner(BaseRunner): ++ """Custom, dummy runner""" ++ ++ + class TestConfiguration(object): + + def test_read_file(self): +@@ -92,6 +97,20 @@ class TestConfiguration(object): + assert "STAGE2_environment.py" == config.environment_file + del os.environ["BEHAVE_STAGE"] + ++ def test_settings_runner_class(self, capsys): ++ config = Configuration("") ++ assert BaseRunner == config.runner_class ++ ++ def test_settings_runner_class_custom(self, capsys): ++ config = Configuration(["--runner-class=tests.unit.test_configuration.CustomTestRunner"]) ++ assert CustomTestRunner == config.runner_class ++ ++ def test_settings_runner_class_invalid(self, capsys): ++ with pytest.raises(SystemExit): ++ Configuration(["--runner-class=does.not.exist.Runner"]) ++ captured = capsys.readouterr() ++ assert "No module named 'does.not.exist.Runner' was found." in captured.err ++ + + class TestConfigurationUserData(TestCase): + """Test userdata aspects in behave.configuration.Configuration class.""" diff --git a/meta-python/recipes-devtools/python/python3-behave/0118-Allow-forcing-color-with-color-always.patch b/meta-python/recipes-devtools/python/python3-behave/0118-Allow-forcing-color-with-color-always.patch new file mode 100644 index 000000000..dc39f98db --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0118-Allow-forcing-color-with-color-always.patch @@ -0,0 +1,59 @@ +From ccccbe478ac05d6093a076f61aaa00d8867b8760 Mon Sep 17 00:00:00 2001 +From: Marc Abramowitz +Date: Thu, 3 Aug 2017 07:29:38 -0700 +Subject: [PATCH] Allow forcing color with --color=always + +even if stdout is not a tty (e.g.: Jenkins) +--- + behave/configuration.py | 7 ++++--- + behave/formatter/pretty.py | 12 +++++++++--- + 2 files changed, 13 insertions(+), 6 deletions(-) + +diff --git a/behave/configuration.py b/behave/configuration.py +index 04c014a..1b0bc2b 100644 +--- a/behave/configuration.py ++++ b/behave/configuration.py +@@ -82,9 +82,10 @@ options = [ + help="Disable the use of ANSI color escapes.")), + + (("--color",), +- dict(action="store_true", dest="color", +- help="""Use ANSI color escapes. This is the default +- behaviour. This switch is used to override a ++ dict(dest="color", choices=["never", "always", "auto"], ++ default="auto", const="auto", nargs="?", ++ help="""Use ANSI color escapes. Defaults to %(const)r. ++ This switch is used to override a + configuration file setting.""")), + + (("-d", "--dry-run"), +diff --git a/behave/formatter/pretty.py b/behave/formatter/pretty.py +index 794e1d7..b97438a 100644 +--- a/behave/formatter/pretty.py ++++ b/behave/formatter/pretty.py +@@ -66,9 +66,7 @@ class PrettyFormatter(Formatter): + super(PrettyFormatter, self).__init__(stream_opener, config) + # -- ENSURE: Output stream is open. + self.stream = self.open() +- isatty = getattr(self.stream, "isatty", lambda: True) +- stream_supports_colors = isatty() +- self.monochrome = not config.color or not stream_supports_colors ++ self.monochrome = self._get_monochrome(config) + self.show_source = config.show_source + self.show_timings = config.show_timings + self.show_multiline = config.show_multiline +@@ -83,6 +81,14 @@ class PrettyFormatter(Formatter): + self.indentations = [] + self.step_lines = 0 + ++ def _get_monochrome(self, config): ++ isatty = getattr(self.stream, "isatty", lambda: True) ++ if config.color == 'always': ++ return False ++ elif config.color == 'never': ++ return True ++ else: ++ return not isatty() + + def reset(self): + # -- UNUSED: self.tag_statement = None diff --git a/meta-python/recipes-devtools/python/python3-behave/0119-Allow-color-with-no-value-followed-by-posarg.patch b/meta-python/recipes-devtools/python/python3-behave/0119-Allow-color-with-no-value-followed-by-posarg.patch new file mode 100644 index 000000000..cc4e52676 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0119-Allow-color-with-no-value-followed-by-posarg.patch @@ -0,0 +1,43 @@ +From 0821f7664961e3dae0d0f6bcf939b1a63ccdaa80 Mon Sep 17 00:00:00 2001 +From: Marc Abramowitz +Date: Thu, 3 Aug 2017 09:11:22 -0700 +Subject: [PATCH] Allow --color with no value followed by posarg + +Allow commands like `--color features/whizbang.feature` to work +Without this, argparse will treat the positional arg as the value to +--color and we'd get: + argument --color: invalid choice: 'features/whizbang.feature' +--- + behave/configuration.py | 12 +++++++++++- + 1 file changed, 11 insertions(+), 1 deletion(-) + +diff --git a/behave/configuration.py b/behave/configuration.py +index 1b0bc2b..0fdfd5e 100644 +--- a/behave/configuration.py ++++ b/behave/configuration.py +@@ -83,7 +83,7 @@ options = [ + + (("--color",), + dict(dest="color", choices=["never", "always", "auto"], +- default="auto", const="auto", nargs="?", ++ default=None, const="auto", nargs="?", + help="""Use ANSI color escapes. Defaults to %(const)r. + This switch is used to override a + configuration file setting.""")), +@@ -562,6 +562,16 @@ class Configuration(object): + # -- AUTO-DISCOVER: Verbose mode from command-line args. + verbose = ("-v" in command_args) or ("--verbose" in command_args) + ++ # Allow commands like `--color features/whizbang.feature` to work ++ # Without this, argparse will treat the positional arg as the value to ++ # --color and we'd get: ++ # argument --color: invalid choice: 'features/whizbang.feature' ++ # (choose from 'never', 'always', 'auto') ++ if '--color' in command_args: ++ color_arg_pos = command_args.index('--color') ++ if os.path.exists(command_args[color_arg_pos + 1]): ++ command_args.insert(color_arg_pos + 1, '--') ++ + self.version = None + self.tags_help = None + self.lang_list = None diff --git a/meta-python/recipes-devtools/python/python3-behave/0120-Add-BEHAVE_COLOR-env-var.patch b/meta-python/recipes-devtools/python/python3-behave/0120-Add-BEHAVE_COLOR-env-var.patch new file mode 100644 index 000000000..63975a42b --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0120-Add-BEHAVE_COLOR-env-var.patch @@ -0,0 +1,31 @@ +From a59dc80e1a8292e3a465c69e79bd7591ed513eb7 Mon Sep 17 00:00:00 2001 +From: Marc Abramowitz +Date: Thu, 3 Aug 2017 13:29:55 -0700 +Subject: [PATCH] Add BEHAVE_COLOR env var + +--- + behave/configuration.py | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/behave/configuration.py b/behave/configuration.py +index 0fdfd5e..e7d385d 100644 +--- a/behave/configuration.py ++++ b/behave/configuration.py +@@ -83,7 +83,7 @@ options = [ + + (("--color",), + dict(dest="color", choices=["never", "always", "auto"], +- default=None, const="auto", nargs="?", ++ default=os.getenv('BEHAVE_COLOR'), const="auto", nargs="?", + help="""Use ANSI color escapes. Defaults to %(const)r. + This switch is used to override a + configuration file setting.""")), +@@ -507,7 +507,7 @@ class Configuration(object): + """Configuration object for behave and behave runners.""" + # pylint: disable=too-many-instance-attributes + defaults = dict( +- color=sys.platform != "win32", ++ color='never' if sys.platform == "win32" else os.getenv('BEHAVE_COLOR', 'auto'), + show_snippets=True, + show_skipped=True, + dry_run=False, diff --git a/meta-python/recipes-devtools/python/python3-behave/0121-fix-malformed-table-rows-warning.patch b/meta-python/recipes-devtools/python/python3-behave/0121-fix-malformed-table-rows-warning.patch new file mode 100644 index 000000000..9c54c8abe --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0121-fix-malformed-table-rows-warning.patch @@ -0,0 +1,33 @@ +From 8463e94459382e8e21d0b5bd19a23356bc674c28 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Pablo=20Dom=C3=ADnguez?= + +Date: Thu, 9 Sep 2021 12:19:21 +0200 +Subject: [PATCH] fix: malformed table rows warning + +--- + behave/parser.py | 5 +++++ + 1 file changed, 5 insertions(+) + +diff --git a/behave/parser.py b/behave/parser.py +index 58c68be..b71adfe 100644 +--- a/behave/parser.py ++++ b/behave/parser.py +@@ -41,6 +41,7 @@ Keyword aliases: + # pylint: enable=line-too-long + + from __future__ import absolute_import, with_statement ++import logging + import re + import sys + import six +@@ -644,6 +645,10 @@ class Parser(object): + self.state = "steps" + return self.action_steps(line) + ++ if not re.match(r"^(|.+)\|$", line): ++ logger = logging.getLogger("behave") ++ logger.warning(u"Malformed table row at %s: line %i", self.feature.filename, self.line) ++ + # -- SUPPORT: Escaped-pipe(s) in Gherkin cell values. + # Search for pipe(s) that are not preceeded with an escape char. + cells = [cell.replace("\\|", "|").strip() diff --git a/meta-python/recipes-devtools/python/python3-behave/0122-FIX-955-setup-Remove-attribute-use_2to3.patch b/meta-python/recipes-devtools/python/python3-behave/0122-FIX-955-setup-Remove-attribute-use_2to3.patch new file mode 100644 index 000000000..34886e1f3 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0122-FIX-955-setup-Remove-attribute-use_2to3.patch @@ -0,0 +1,42 @@ +From e7353c14b2a6b877eb31ebd029c613c91f8b7f11 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Sun, 12 Sep 2021 16:07:32 +0200 +Subject: [PATCH] FIX #955: setup: Remove attribute 'use_2to3' + +REASON: +* This attribute is deprecated since setuptools >= v58.0.2 (2021-09-06). +* 2to3 conversion should not be needed anymore. + Currently, code should run on python2 and python3 (by using six, etc.). +--- + setup.py | 7 +++---- + 1 file changed, 3 insertions(+), 4 deletions(-) + +diff --git a/setup.py b/setup.py +index fd89bda..ba407fd 100644 +--- a/setup.py ++++ b/setup.py +@@ -118,8 +118,8 @@ setup( + "pylint", + ], + }, +- # MAYBE-DISABLE: use_2to3 +- use_2to3= bool(python_version >= 3.0), ++ # DISABLED: use_2to3= bool(python_version >= 3.0), ++ # DEPRECATED SINCE: setuptools v58.0.2 (2021-09-06) + license="BSD", + classifiers=[ + "Development Status :: 4 - Beta", +@@ -129,12 +129,11 @@ setup( + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", +- "Programming Language :: Python :: 3.3", +- "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", ++ "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: Jython", + "Programming Language :: Python :: Implementation :: PyPy", diff --git a/meta-python/recipes-devtools/python/python3-behave/0123-Add-info-for-fixed-issue-955.patch b/meta-python/recipes-devtools/python/python3-behave/0123-Add-info-for-fixed-issue-955.patch new file mode 100644 index 000000000..7806d9e9c --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-behave/0123-Add-info-for-fixed-issue-955.patch @@ -0,0 +1,21 @@ +From c6e1f74e9e670ba789467888bc34d64e71999a24 Mon Sep 17 00:00:00 2001 +From: jenisys +Date: Mon, 20 Sep 2021 16:13:59 +0200 +Subject: [PATCH] Add info for fixed issue #955 + +--- + CHANGES.rst | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/CHANGES.rst b/CHANGES.rst +index ff82132..880fd91 100644 +--- a/CHANGES.rst ++++ b/CHANGES.rst +@@ -42,6 +42,7 @@ FIXED: + + * FIXED: Some tests related to python3.9 + * FIXED: active-tag logic if multiple tags with same category exists. ++* issue #955: setup: Remove attribute 'use_2to3' (submitted by: krisgesling) + * 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) diff --git a/meta-python/recipes-devtools/python/python3-behave_1.2.6.bb b/meta-python/recipes-devtools/python/python3-behave_1.2.6.bb index 1dcc7d218..745d1e2b2 100644 --- a/meta-python/recipes-devtools/python/python3-behave_1.2.6.bb +++ b/meta-python/recipes-devtools/python/python3-behave_1.2.6.bb @@ -4,8 +4,131 @@ LICENSE = "BSD-2-Clause" LIC_FILES_CHKSUM = "file://LICENSE;md5=d950439e8ea6ed233e4288f5e1a49c06" PV .= "+git${SRCREV}" -SRCREV = "9520119376046aeff73804b5f1ea05d87a63f370" -SRC_URI += "git://github.com/behave/behave;branch=master;protocol=https" +SRCREV = "c0f3faf47ff05f549ec049599eb2f24069b0e51e" +SRC_URI += "file://0001-FIXES-725-ScenarioOutlineBuilder-was-not-copying-des.patch \ + file://0002-UPDATE-FIXED-725.patch \ + file://0003-ADD-TEST-to-verify-that-issue-725-is-fixed.patch \ + file://0004-FIX-SummaryReporter-counts-computation-when-Rules-ar.patch \ + file://0005-SummaryReporter.print_summary-Simplify-if-Rules-are-.patch \ + file://0006-Formatter-Add-basic-support-output-for-Rules.patch \ + file://0007-BUMP-VERSION-1.2.7.dev1-was-1.2.7.dev0.patch \ + file://0008-Correct-examples-and-docstring.patch \ + file://0009-FIX-feature.run_items-processing-with-Rule-s.patch \ + file://0010-docs-Disable-sphinx-intl-support-for-READTHEDOCS.patch \ + file://0011-Cleanup-Dependent-package-versions-in-requirements.patch \ + file://0012-docs-conf.py-tweaks.patch \ + file://0013-FIX-Misspelled-after_rule-hook-was-after_after.patch \ + file://0014-Add-hints-on-Gherkin-v6-grammar-issues.patch \ + file://0015-README-ReST-tweaks.patch \ + file://0016-Example-using-Gherkin-v6-grammar.patch \ + file://0017-PREPARE-Python-3.8-support.patch \ + file://0018-py3.8-Try-to-fix-logging.Formatter-validate-problem.patch \ + file://0019-travis.ci-Temporarily-move-py38-dev-to-front-build-f.patch \ + file://0020-travis.ci-Tweaks-for-faster-builds-temporarily.patch \ + file://0021-FIX-py3.8-logging.Formatter.validate-problem.patch \ + file://0022-PREPARE-FOR-Python-3.8-asyncio.coroutine-is-deprecat.patch \ + file://0023-UPDATE-Add-755-info.patch \ + file://0024-FIX-WARNING-Related-to-docstring-example-and-weird-b.patch \ + file://0025-FIX-invalid-escape-sequence-warnings-w-regex-pattern.patch \ + file://0026-DEPRECATING-CLEANUP-Move-deprecated-tag-matcher-clas.patch \ + file://0027-Comment-tweaks.patch \ + file://0028-FIX-warnings-related-to-invalid-escapes-in-regex.patch \ + file://0029-Steps-catalog-should-not-break-configured-rerun-sett.patch \ + file://0030-Add-feature-w-rules-and-failing-scenarios-enable-via.patch \ + file://0031-examples-gherkin_v6-Tweak-ScenarioOutline-Examples-t.patch \ + file://0032-Add-info-on-merged-pull-588.patch \ + file://0033-Tweak-tests-required-by-pytest-5.0.patch \ + file://0034-CLEANUP-Use-pytest-instead-of-nose-to-remove-nose.im.patch \ + file://0035-REMOVE-DEPENDENCY-nose-to-avoid-nose.importer-warnin.patch \ + file://0036-FIX-Remove-test-from-pytest-run.patch \ + file://0037-Select-by-location-Add-support-for-Scenario-containe.patch \ + file://0038-docs-Add-description-for-Select-by-location-for-Scen.patch \ + file://0039-tests-Fix-warnings-for-python3.patch \ + file://0040-Use-cucumber-tag-expressions-1.1.2-to-fix-warnings.patch \ + file://0041-MENTION-ENHANCEMENT-Support-emojis-in-feature-files-.patch \ + file://0042-FIX-Invalid-escape-char-in-regex-w-python3.patch \ + file://0043-Example-related-to-question-in-756.patch \ + file://0044-FIX-python3.8-regressions-on-CI-server.patch \ + file://0045-UPDATE-Mark-issue-755-as-fixed.patch \ + file://0046-UPDATE-Cucumber-gherkin-languages.json.patch \ + file://0047-gherkin-Adding-Rule-keyword-translation-in-portugues.patch \ + file://0048-Tweaks-to-update-generate-from-gherkin-languages.jso.patch \ + file://0049-EXAMPLE-Tweak-naming-to-fixture.behave.no_background.patch \ + file://0050-EXAMPLE-Cleanup-Gherkin-v6-README.patch \ + file://0051-Improve-support-for-feature.background-inheritance-f.patch \ + file://0052-Add-support-for-runtime-constraints.patch \ + file://0053-Use-runtime-constraints.patch \ + file://0054-CLEANUP-Remove-deprecated-parts.patch \ + file://0055-CLEANUP-Remove-deprecated-parts.patch \ + file://0056-more.features-Perform-more-Gherkin-v6-checks-and-run.patch \ + file://0057-UTIL-Correct-URL-and-python-module-old-was-broken-no.patch \ + file://0058-UTIL-Formatting-tweaks.patch \ + file://0059-Fixed-a-bug-where-use_fixture_by_tag-didn-t-return-t.patch \ + file://0060-Added-issue-unit-test.patch \ + file://0061-Merge-pull-request-767-with-minor-tweaks.patch \ + file://0062-CHECK-Issue-766-PrettyFormatter-UnicodeError.patch \ + file://0063-FIX-issue-772-ScenarioOutline.Examples-without-table.patch \ + file://0064-FIX-issue-772-ScenarioOutline.Examples-without-table.patch \ + file://0065-Nibble-TravisCI-to-wake-up.patch \ + file://0066-Tweak-pytest-version-selection.patch \ + file://0067-Tweak-pytest-configuration-to-silence-JUnit-XML-dial.patch \ + file://0068-Add-basic-feature-test-for-wildcard-pattern-matching.patch \ + file://0069-UPDATE-dependencies-path.py-path-pytest.patch \ + file://0070-pytest-Disable-DeprecatedWarning-from-distutils-pack.patch \ + file://0071-CLEANUP-Add-ContextMode-enum-related-to-797.patch \ + file://0072-Cleanup-comments.patch \ + file://0073-FIX-sphinx-build-problem-async_steps3x.py.patch \ + file://0074-docs-Rename-page-parse_expressions-was-parse_builtin.patch \ + file://0075-docs-parse_expression-add-links-to-parse_type-module.patch \ + file://0076-BUMP-VERSION-1.2.7.dev2-was-1.2.7.dev1.patch \ + file://0077-Gherkin-parser-Cleanups-related-to-question-in-800-P.patch \ + file://0078-Clarify-select-by-name-uses-regex-pattern-related-to.patch \ + file://0079-FIX-Cross-reference-problem-copy-paste-in-Rule-class.patch \ + file://0080-DOCS-Update-API-description-for-Runner-Operation.patch \ + file://0081-FIX-DOCS-Runner-operations-typo.patch \ + file://0082-issue-740-Enhancement-Context.add_cleanup-with-layer.patch \ + file://0083-UPDATE-parse-1.18.0-parse-versions-1.16.0-.-1.17.x-h.patch \ + file://0084-RELATED-TO-Duplicated-steps-AmbiguousStepErrors.patch \ + file://0085-Add-renovate.json.patch \ + file://0086-PRPEPARE-RENOVATE-With-adaptions.patch \ + file://0087-Pin-dependencies.patch \ + file://0088-renovate-Extend-pip-requirements-file-list.patch \ + file://0089-PIN-REQUIREMENTS-Extend-to-all-places.patch \ + file://0090-tasks-Add-invoke_cleanup-replaces-_tasklet_cleanup-r.patch \ + file://0091-Docs-change-code-blocks-from-bash-to-console.patch \ + file://0092-Fix-typo-in-tutorial.patch \ + file://0093-py.requirements-Use-PyHamcrest-2.0-for-python2.7.patch \ + file://0094-UPDATE-PR-877-was-merged.patch \ + file://0095-capitalizing-steps.patch \ + file://0096-FIX-invoke-task-develop.update-gherkin-that-aborted-.patch \ + file://0097-Test-against-PowerPC-CPU-support-Travis-867.patch \ + file://0098-Add-Context.attach-docs-and-test.patch \ + file://0099-Add-Contributing-chapter.patch \ + file://0100-Adapt-Tox-target-for-building-the-docs.patch \ + file://0101-Mention-HTML-formatter-in-documentation.patch \ + file://0102-Use-console-highlighting-for-pip-install-docs.patch \ + file://0103-docs-fix-simple-typo-tuorial-tutorial.patch \ + file://0104-Add-support-for-python3.9-by-using-active-tags.patch \ + file://0105-PREFER-python3-from-now-on.patch \ + file://0106-REMOVE-invoke-scripts.patch \ + file://0107-FIX-Deprecated-warnings-for-Python-3.x.patch \ + file://0108-FIX-Python2-problems-in-virtualenvs-w-behave4cmd0-st.patch \ + file://0109-FIX-Active-tag-logic.patch \ + file://0110-FIX-Tests-w-more.features.patch \ + file://0111-FIX-Some-regressions-with-Python-3.9.patch \ + file://0112-docs-Update-new-and-noteworthy.patch \ + file://0113-Add-diagnostic-helper-function-to-print-the-current-.patch \ + file://0114-UPDATE-i18n-gherkin-languages.json-from-cucumber-rep.patch \ + file://0115-UPDATE-CHANGES-Related-to-PR-895-and-827.patch \ + file://0116-FIX-CI-TRAVIS-For-python-2.7-builds-mock-4.0-only-fo.patch \ + file://0117-Adds-the-ability-to-use-a-custom-runner-in-the-behav.patch \ + file://0118-Allow-forcing-color-with-color-always.patch \ + file://0119-Allow-color-with-no-value-followed-by-posarg.patch \ + file://0120-Add-BEHAVE_COLOR-env-var.patch \ + file://0121-fix-malformed-table-rows-warning.patch \ + file://0122-FIX-955-setup-Remove-attribute-use_2to3.patch \ + file://0123-Add-info-for-fixed-issue-955.patch \ + " S = "${WORKDIR}/git" @@ -16,3 +139,5 @@ RDEPENDS:${PN} += " \ ${PYTHON_PN}-setuptools \ ${PYTHON_PN}-six \ " + +PV = "6" -- 2.25.1