diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index f0ce22d68..67caa8103 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -29,11 +29,15 @@ jobs: matrix: os: - ubuntu-latest - python-version: [ '3.x', 'pypy-3.8', 'pypy-2.7' ] - # DISABLED: python-version: [ '3.x', '2.x' ] - # include: - # - os: macos-latest - # python-version: '3.x' + # - windows-latest + - macos-13 + python-version: + - '3.12' + - '3.11' + - '3.10' + - '3.9' + - '3.8' + - 'pypy-3.8' steps: - uses: actions/checkout@v4 @@ -54,4 +58,4 @@ jobs: - name: run acceptance tests run: make acceptance - working-directory: python \ No newline at end of file + working-directory: python diff --git a/CHANGELOG.md b/CHANGELOG.md index 1020b98f1..9d6de54e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,9 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt - [.NET] Enable warnings as errors - [Ruby] Initial rubocop autofixes (Mainly to style) ([#266](https://github.com/cucumber/gherkin/pull/266)) +### Removed +- [Python] Drop compatibility for python 2. Supported python versions are 3.8, 3.9, 3.10, 3.12 + ## [29.0.0] - 2024-08-12 ### Added - (i18n) Added Gujarati translation for "Rule" ([#249](https://github.com/cucumber/gherkin/pull/249)) diff --git a/python/bin/gherkin b/python/bin/gherkin index b211ef27f..ce7f5bedf 100755 --- a/python/bin/gherkin +++ b/python/bin/gherkin @@ -1,18 +1,13 @@ #!/usr/bin/env sh -# Use "make GHERKIN_PYTHON_VERSION=python2 ..." to use python2 if [ -z "$GHERKIN_PYTHON_VERSION" ]; then - if [ -x "$(command -v python)" ] - then - GHERKIN_PYTHON_VERSION=python - elif [ -x "$(command -v python3)" ] + if [ -x "$(command -v python3)" ] then GHERKIN_PYTHON_VERSION=python3 - elif [ -x "$(command -v python2)" ] + elif [ -x "$(command -v python)" ] then - GHERKIN_PYTHON_VERSION=python2 - else - echo "Neiter python, python3 or python2 found on PATH, exiting" + GHERKIN_PYTHON_VERSION=python + echo "Neither python3 or python found on PATH, exiting" exit 1 fi fi diff --git a/python/bin/gherkin-generate-tokens b/python/bin/gherkin-generate-tokens index 04e102f20..61ea45a27 100755 --- a/python/bin/gherkin-generate-tokens +++ b/python/bin/gherkin-generate-tokens @@ -1,18 +1,13 @@ #!/usr/bin/env sh -# Use "make GHERKIN_PYTHON_VERSION=python2 ..." to use python2 if [ -z "$GHERKIN_PYTHON_VERSION" ]; then - if [ -x "$(command -v python)" ] - then - GHERKIN_PYTHON_VERSION=python - elif [ -x "$(command -v python3)" ] + if [ -x "$(command -v python3)" ] then GHERKIN_PYTHON_VERSION=python3 - elif [ -x "$(command -v python2)" ] + elif [ -x "$(command -v python)" ] then - GHERKIN_PYTHON_VERSION=python2 - else - echo "Neiter python, python3 or python2 found on PATH, exiting" + GHERKIN_PYTHON_VERSION=python + echo "Neither python3 or python found on PATH, exiting" exit 1 fi fi diff --git a/python/bin/gherkin_generate_tokens.py b/python/bin/gherkin_generate_tokens.py index f512273f8..636599917 100644 --- a/python/bin/gherkin_generate_tokens.py +++ b/python/bin/gherkin_generate_tokens.py @@ -1,17 +1,11 @@ -import codecs import os import sys -if sys.version_info < (3, 0): - import codecs sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) from gherkin.token_scanner import TokenScanner from gherkin.token_formatter_builder import TokenFormatterBuilder from gherkin.parser import Parser files = sys.argv[1:] -if sys.version_info < (3, 0) and os.name != 'nt': # for Python2 unless on Windows native - UTF8Writer = codecs.getwriter('utf8') - sys.stdout = UTF8Writer(sys.stdout) parser = Parser(TokenFormatterBuilder()) for file in files: scanner = TokenScanner(file) diff --git a/python/gherkin-python.razor b/python/gherkin-python.razor index 844c4fd32..9bc1dbd94 100755 --- a/python/gherkin-python.razor +++ b/python/gherkin-python.razor @@ -41,7 +41,7 @@ RULE_TYPE = [ ] -class ParserContext(object): +class ParserContext: def __init__(self, token_scanner, token_matcher, token_queue, errors): self.token_scanner = token_scanner self.token_matcher = token_matcher @@ -49,16 +49,13 @@ class ParserContext(object): self.errors = errors -class @(Model.ParserClassName)(object): +class @(Model.ParserClassName): def __init__(self, ast_builder=None): self.ast_builder = ast_builder if ast_builder is not None else AstBuilder() self.stop_at_first_error = False def parse(self, token_scanner_or_str, token_matcher=None): - if sys.version_info < (3, 0): - token_scanner = TokenScanner(token_scanner_or_str) if isinstance(token_scanner_or_str, basestring) else token_scanner_or_str - else: - token_scanner = TokenScanner(token_scanner_or_str) if isinstance(token_scanner_or_str, str) else token_scanner_or_str + token_scanner = TokenScanner(token_scanner_or_str) if isinstance(token_scanner_or_str, str) else token_scanner_or_str self.ast_builder.reset() if token_matcher is None: token_matcher = TokenMatcher() diff --git a/python/gherkin/__main__.py b/python/gherkin/__main__.py index 55d96540e..e128c056d 100644 --- a/python/gherkin/__main__.py +++ b/python/gherkin/__main__.py @@ -1,14 +1,6 @@ import os from optparse import OptionParser import sys -if sys.version_info < (3, 0): - string_type = basestring - if os.name != 'nt': - import codecs - UTF8Writer = codecs.getwriter('utf8') - sys.stdout = UTF8Writer(sys.stdout) -else: - string_type = str sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) import json diff --git a/python/gherkin/ast_builder.py b/python/gherkin/ast_builder.py index 7c85fef19..05c564cc5 100644 --- a/python/gherkin/ast_builder.py +++ b/python/gherkin/ast_builder.py @@ -2,7 +2,7 @@ from .errors import AstBuilderException from .stream.id_generator import IdGenerator -class AstBuilder(object): +class AstBuilder: def __init__(self, id_generator=None): self.id_generator = id_generator if self.id_generator is None: diff --git a/python/gherkin/ast_node.py b/python/gherkin/ast_node.py index 595d4ba89..bcf901e16 100644 --- a/python/gherkin/ast_node.py +++ b/python/gherkin/ast_node.py @@ -1,7 +1,7 @@ from collections import defaultdict -class AstNode(object): +class AstNode: def __init__(self, rule_type): self.rule_type = rule_type diff --git a/python/gherkin/count_symbols.py b/python/gherkin/count_symbols.py deleted file mode 100644 index da00ea1af..000000000 --- a/python/gherkin/count_symbols.py +++ /dev/null @@ -1,6 +0,0 @@ -import sys - -if sys.version_info[0] == 3: - from .count_symbols_py3_plus import count_symbols -else: - from .count_symbols_py2 import count_symbols diff --git a/python/gherkin/count_symbols_py2.py b/python/gherkin/count_symbols_py2.py deleted file mode 100644 index 1363da84a..000000000 --- a/python/gherkin/count_symbols_py2.py +++ /dev/null @@ -1,6 +0,0 @@ -import re - -REGEX_ASTRAL_SYMBOLS = re.compile(ur'[\uD800-\uDBFF][\uDC00-\uDFFF]', re.UNICODE) - -def count_symbols(string): - return len(REGEX_ASTRAL_SYMBOLS.sub('_', string)) diff --git a/python/gherkin/count_symbols_py3_plus.py b/python/gherkin/count_symbols_py3_plus.py deleted file mode 100644 index d326c1502..000000000 --- a/python/gherkin/count_symbols_py3_plus.py +++ /dev/null @@ -1,2 +0,0 @@ -def count_symbols(string): - return len(string) diff --git a/python/gherkin/dialect.py b/python/gherkin/dialect.py index 2289bf69b..31592f3a6 100644 --- a/python/gherkin/dialect.py +++ b/python/gherkin/dialect.py @@ -7,11 +7,11 @@ os.path.dirname(__file__), 'gherkin-languages.json') -with io.open(DIALECT_FILE_PATH, 'r', encoding='utf-8') as file: +with open(DIALECT_FILE_PATH, encoding='utf-8') as file: DIALECTS = json.load(file) -class Dialect(object): +class Dialect: @classmethod def for_name(cls, name): diff --git a/python/gherkin/errors.py b/python/gherkin/errors.py index 4c956c427..9b5ddff77 100644 --- a/python/gherkin/errors.py +++ b/python/gherkin/errors.py @@ -5,14 +5,14 @@ class ParserError(Exception): class ParserException(ParserError): def __init__(self, message, location): self.location = location - super(ParserException, self).__init__('(' + str(location['line']) + ':' + + super().__init__('(' + str(location['line']) + ':' + str(location['column'] if 'column' in location else 0) + '): ' + message) class NoSuchLanguageException(ParserException): def __init__(self, language, location): - super(NoSuchLanguageException, self).__init__('Language not supported: ' + language, + super().__init__('Language not supported: ' + language, location) @@ -23,7 +23,7 @@ class AstBuilderException(ParserException): class UnexpectedEOFException(ParserException): def __init__(self, received_token, expected_token_types, state_comment): message = 'unexpected end of file, expected: ' + ', '.join(expected_token_types) - super(UnexpectedEOFException, self).__init__(message, received_token.location) + super().__init__(message, received_token.location) class UnexpectedTokenException(ParserException): @@ -34,12 +34,12 @@ def __init__(self, received_token, expected_token_types, state_comment): location = (received_token.location if column else {'line': received_token.location['line'], 'column': received_token.line.indent + 1}) - super(UnexpectedTokenException, self).__init__(message, location) + super().__init__(message, location) class CompositeParserException(ParserError): def __init__(self, errors): self.errors = errors - super(CompositeParserException, self).__init__("Parser errors:\n" + + super().__init__("Parser errors:\n" + '\n'.join([error.args[0] for error in errors])) diff --git a/python/gherkin/gherkin_line.py b/python/gherkin/gherkin_line.py index 8ca945282..f396eab15 100644 --- a/python/gherkin/gherkin_line.py +++ b/python/gherkin/gherkin_line.py @@ -2,7 +2,7 @@ from .errors import ParserException -class GherkinLine(object): +class GherkinLine: def __init__(self, line_text, line_number): self._line_text = line_text self._line_number = line_number @@ -76,7 +76,7 @@ def split_table_cells(self, row): @property def tags(self): column = self.indent + 1 - uncommented_line = re.split(r"\s#", self._trimmed_line_text.strip(), 2)[0] + uncommented_line = re.split(r"\s#", self._trimmed_line_text.strip(), maxsplit=2)[0] items = uncommented_line.strip().split('@') tags = [] for item in items[1:]: diff --git a/python/gherkin/inout.py b/python/gherkin/inout.py index bedb1544d..60129d335 100644 --- a/python/gherkin/inout.py +++ b/python/gherkin/inout.py @@ -1,11 +1,10 @@ -from __future__ import print_function import json from .parser import Parser from .token_scanner import TokenScanner from .pickles.compiler import compile from .errors import ParserException, CompositeParserException -class Inout(object): +class Inout: def __init__(self, print_source, print_ast, print_pickles): self.print_source = print_source self.print_ast = print_ast diff --git a/python/gherkin/parser.py b/python/gherkin/parser.py index 1c0baad9b..bd0de1feb 100644 --- a/python/gherkin/parser.py +++ b/python/gherkin/parser.py @@ -43,7 +43,7 @@ ] -class ParserContext(object): +class ParserContext: def __init__(self, token_scanner, token_matcher, token_queue, errors): self.token_scanner = token_scanner self.token_matcher = token_matcher @@ -51,16 +51,13 @@ def __init__(self, token_scanner, token_matcher, token_queue, errors): self.errors = errors -class Parser(object): +class Parser: def __init__(self, ast_builder=None): self.ast_builder = ast_builder if ast_builder is not None else AstBuilder() self.stop_at_first_error = False def parse(self, token_scanner_or_str, token_matcher=None): - if sys.version_info < (3, 0): - token_scanner = TokenScanner(token_scanner_or_str) if isinstance(token_scanner_or_str, basestring) else token_scanner_or_str - else: - token_scanner = TokenScanner(token_scanner_or_str) if isinstance(token_scanner_or_str, str) else token_scanner_or_str + token_scanner = TokenScanner(token_scanner_or_str) if isinstance(token_scanner_or_str, str) else token_scanner_or_str self.ast_builder.reset() if token_matcher is None: token_matcher = TokenMatcher() diff --git a/python/gherkin/pickles/compiler.py b/python/gherkin/pickles/compiler.py index 07f5ca91f..22c04a822 100644 --- a/python/gherkin/pickles/compiler.py +++ b/python/gherkin/pickles/compiler.py @@ -1,10 +1,8 @@ import re - -from ..count_symbols import count_symbols from ..stream.id_generator import IdGenerator -class Compiler(object): +class Compiler: def __init__(self, id_generator=None): self.id_generator = id_generator if self.id_generator is None: @@ -158,7 +156,7 @@ def _interpolate(self, name, variable_cells, value_cells): # For the case of trailing backslash, re-escaping backslashes are needed reescaped_value = re.sub(r'\\', r'\\\\', value_cell['value']) name = re.sub( - u'<{0[value]}>'.format(variable_cell), + '<{0[value]}>'.format(variable_cell), reescaped_value, name ) diff --git a/python/gherkin/stream/gherkin_events.py b/python/gherkin/stream/gherkin_events.py index 23dcb15d1..b26b647dd 100644 --- a/python/gherkin/stream/gherkin_events.py +++ b/python/gherkin/stream/gherkin_events.py @@ -46,8 +46,6 @@ def enum(self, source_event): 'pickle': pickle } except CompositeParserException as e: - for event in create_errors(e.errors, uri): - yield event + yield from create_errors(e.errors, uri) except ParserError as e: - for event in create_errors([e], uri): - yield event + yield from create_errors([e], uri) diff --git a/python/gherkin/stream/id_generator.py b/python/gherkin/stream/id_generator.py index f172cc8b4..5e8bbc84b 100644 --- a/python/gherkin/stream/id_generator.py +++ b/python/gherkin/stream/id_generator.py @@ -1,4 +1,4 @@ -class IdGenerator(object): +class IdGenerator: def __init__(self): self._id_counter = 0 diff --git a/python/gherkin/stream/source_events.py b/python/gherkin/stream/source_events.py index 946b6bc62..17f6cabf6 100644 --- a/python/gherkin/stream/source_events.py +++ b/python/gherkin/stream/source_events.py @@ -1,10 +1,8 @@ -import io - def source_event(path): event = { 'source': { 'uri': path, - 'data': io.open(path, 'r', encoding='utf8', newline='').read(), + 'data': open(path, encoding='utf8', newline='').read(), 'mediaType': 'text/x.cucumber.gherkin+plain' } } diff --git a/python/gherkin/token.py b/python/gherkin/token.py index 4536ecb59..015ef8259 100644 --- a/python/gherkin/token.py +++ b/python/gherkin/token.py @@ -1,4 +1,4 @@ -class Token(object): +class Token: def __init__(self, gherkin_line, location): self.line = gherkin_line self.location = location diff --git a/python/gherkin/token_matcher.py b/python/gherkin/token_matcher.py index 265cf5835..6daf648bd 100644 --- a/python/gherkin/token_matcher.py +++ b/python/gherkin/token_matcher.py @@ -3,19 +3,8 @@ from .dialect import Dialect from .errors import NoSuchLanguageException -# Source: https://stackoverflow.com/a/8348914 -try: - import textwrap - textwrap.indent -except AttributeError: # undefined function (wasn't added until Python 3.3) - def indent(text, amount, ch=' '): - padding = amount * ch - return ''.join(padding+line for line in text.splitlines(True)) -else: - def indent(text, amount, ch=' '): - return textwrap.indent(text, amount * ch) - -class TokenMatcher(object): + +class TokenMatcher: LANGUAGE_RE = re.compile(r"^\s*#\s*language\s*:\s*([a-zA-Z\-_]+)\s*$") def __init__(self, dialect_name='en'): diff --git a/python/gherkin/token_matcher_markdown.py b/python/gherkin/token_matcher_markdown.py index 68f0737c6..79543bcd2 100644 --- a/python/gherkin/token_matcher_markdown.py +++ b/python/gherkin/token_matcher_markdown.py @@ -7,10 +7,10 @@ class GherkinInMarkdownTokenMatcher(TokenMatcher): def __init__(self, dialect_name='en'): - super(GherkinInMarkdownTokenMatcher, self).__init__(dialect_name) + super().__init__(dialect_name) def reset(self): - super(GherkinInMarkdownTokenMatcher, self).reset() + super().reset() self.matched_feature_line=False def match_FeatureLine(self, token): @@ -142,7 +142,7 @@ def _default_docstring_content_type(): def _match_title_line(self, prefix, keywords, keywordSuffix, token, token_type): keywords_or_list="|".join(map(lambda x: re.escape(x), keywords)) - match = re.search(u'{}({}){}(.*)'.format(prefix, keywords_or_list, keywordSuffix), token.line.get_line_text()) + match = re.search(f'{prefix}({keywords_or_list}){keywordSuffix}(.*)', token.line.get_line_text()) indent = token.line.indent if(match): diff --git a/python/gherkin/token_scanner.py b/python/gherkin/token_scanner.py index 531b51b25..3a4c24f6e 100644 --- a/python/gherkin/token_scanner.py +++ b/python/gherkin/token_scanner.py @@ -1,11 +1,10 @@ import io import os -import sys from .token import Token from .gherkin_line import GherkinLine -class TokenScanner(object): +class TokenScanner: """ The scanner reads a gherkin doc (typically read from a `.feature` file) and creates a token for each line. @@ -19,15 +18,9 @@ class TokenScanner(object): def __init__(self, path_or_str): if os.path.exists(path_or_str): - self.io = io.open(path_or_str, 'r', encoding='utf8') + self.io = open(path_or_str, encoding='utf8') else: - if sys.version_info < (3, 0): - if isinstance(path_or_str, str): - self.io = io.StringIO(unicode(path_or_str, encoding='utf8')) - else: - self.io = io.StringIO(path_or_str) - else: - self.io = io.StringIO(path_or_str) + self.io = io.StringIO(path_or_str) self.line_number = 0 def read(self): diff --git a/python/requirements.txt b/python/requirements.txt index 798e128ec..15a818a52 100644 --- a/python/requirements.txt +++ b/python/requirements.txt @@ -1,6 +1,5 @@ # -- FOR TESTING: -pytest <8.4; python_version < '3.0' -pytest >= 5.0; python_version >= '3.0' +pytest >= 5.0 # MAYBE: For pytest HTML reports. # pytest-html diff --git a/python/setup.py b/python/setup.py index 5e974c408..f6419717a 100644 --- a/python/setup.py +++ b/python/setup.py @@ -1,4 +1,3 @@ -# coding: utf-8 from setuptools import setup setup(name="gherkin-official", packages=["gherkin", "gherkin.pickles", "gherkin.stream"], @@ -13,9 +12,14 @@ keywords=["gherkin", "cucumber", "bdd"], scripts=["bin/gherkin"], classifiers=["Programming Language :: Python", - "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ], platforms = ['any'], package_data={"gherkin": ["gherkin-languages.json"]}, + python_requires=">=3.8", ) diff --git a/python/test/count_symbols_test.py b/python/test/count_symbols_test.py index fa35f6d83..89ecb2e46 100644 --- a/python/test/count_symbols_test.py +++ b/python/test/count_symbols_test.py @@ -1,18 +1,16 @@ # coding=utf-8 -from gherkin.count_symbols import count_symbols - def test_count_length_of_astral_point_symbols_correctly(): string = u'\U0001f63b' - assert 1 == count_symbols(string) + assert 1 == len(string) def test_count_length_of_ascii_symbols_correctly(): string = u'hello' - assert 5 == count_symbols(string) + assert 5 == len(string) def test_count_length_of_latin_symbols_correctly(): string = u'Scénario' - assert 8, count_symbols(string) + assert 8, len(string) diff --git a/python/tox.ini b/python/tox.ini index 0f476afa8..6033180ac 100644 --- a/python/tox.ini +++ b/python/tox.ini @@ -4,17 +4,16 @@ # REQUIRES: pip install tox # DESCRIPTION: # Use tox to run tasks (tests, ...) in a clean virtual environment. -# For example, use an installed Python 3.9 or Python 2.7, like: +# For example, use an installed Python 3.9, like: # # tox -e py39 -# tox -e py27 # # SEE ALSO: # * https://tox.wiki/en/latest/config.html # ============================================================================ [tox] -envlist = py310, py39, py27 +envlist = py312, py311, py310, py39, py38 # ----------------------------------------------------------------------------- # TEST ENVIRONMENTS: