diff options
| author | Jon Dufresne <jon.dufresne@gmail.com> | 2020-05-24 06:44:09 -0700 |
|---|---|---|
| committer | Jon Dufresne <jon.dufresne@gmail.com> | 2020-08-31 17:27:38 -0700 |
| commit | 3e3892f939031d58d98275ce8a237689225d299a (patch) | |
| tree | 93b64d7fd8aa1cc19335db11c915dfc59376eae9 | |
| parent | 28a2acdd9b307c95d2111f1f831b1a5afc634691 (diff) | |
| download | sqlparse-3e3892f939031d58d98275ce8a237689225d299a.tar.gz | |
Remove support for end-of-life Pythons
Python 2.7 and 3.4 are end-of-life. They are no longer receiving bug
fixes, including for security issues. Python 2.7 went EOL on 2020-01-01
and 3.4 on 2019-03-18. For additional details on support Python
versions, see:
Supported: https://devguide.python.org/#status-of-python-branches
EOL: https://devguide.python.org/devcycle/#end-of-life-branches
Removing support for EOL Pythons will reduce testing and maintenance
resources while allowing the library to move towards modern Python 3.
Using pypinfo, we can show the PyPI download statistics, showing less
than 10% of users are using Python 2.7.
| python_version | percent | download_count |
| -------------- | ------: | -------------: |
| 3.7 | 45.36% | 3,056,010 |
| 3.6 | 26.46% | 1,782,778 |
| 3.8 | 12.22% | 823,213 |
| 2.7 | 9.97% | 671,459 |
| 3.5 | 5.86% | 394,846 |
| 3.4 | 0.10% | 6,700 |
| 3.9 | 0.03% | 2,346 |
| 2.6 | 0.00% | 57 |
| 3.3 | 0.00% | 21 |
| 3.10 | 0.00% | 6 |
| Total | | 6,737,436 |
Library users who continue to use Python 2.7 will still be able to
install previous versions of sqlparse.
Compatibility shims have been dropped, simplifying the code.
Using pyupgrade, the codebase has been updated to take advantage of
modern syntax <https://github.com/asottile/pyupgrade>.
The wheel is no longer marked as "universal" as it is now Python 3 only.
41 files changed, 131 insertions, 256 deletions
diff --git a/.travis.yml b/.travis.yml index cbf7bf9..9f8fb77 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,10 @@ language: python python: - - "2.7" - - "3.4" - "3.5" - "3.6" - "3.7" - "3.8" - "nightly" - - "pypy" - "pypy3" matrix: @@ -1,7 +1,10 @@ Development Version ------------------- -Nothing yet. ¯\_(ツ)_/¯ +Notable Changes + +* Remove support for end-of-life Python 2.7 and 3.4. Python 3.5+ is now + required. Release 0.3.1 (Feb 29, 2020) @@ -9,13 +9,8 @@ python-sqlparse - Parse SQL statements sqlparse is a non-validating SQL parser for Python. It provides support for parsing, splitting and formatting SQL statements. -The module is compatible with Python 2.7 and Python 3 (>= 3.4) -and released under the terms of the `New BSD license -<https://opensource.org/licenses/BSD-3-Clause>`_. - -.. note:: - - Support for Python<3.4 (including 2.x) will be dropped soon. +The module is compatible with Python 3.5+ and released under the terms of the +`New BSD license <https://opensource.org/licenses/BSD-3-Clause>`_. Visit the project page at https://github.com/andialbrecht/sqlparse for further information about this project. diff --git a/docs/source/conf.py b/docs/source/conf.py index 70bd69a..a5be640 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # python-sqlparse documentation build configuration file, created by # sphinx-quickstart on Thu Feb 26 08:19:28 2009. # diff --git a/docs/source/intro.rst b/docs/source/intro.rst index 330184a..41e4d23 100644 --- a/docs/source/intro.rst +++ b/docs/source/intro.rst @@ -120,7 +120,7 @@ To check out the latest sources of this module run to check out the latest sources from the repository. -:mod:`sqlparse` is currently tested under Python 2.7, >=3.4 and pypy. Tests are +:mod:`sqlparse` is currently tested under Python 3.5+ and pypy. Tests are automatically run on each commit and for each pull request on Travis: https://travis-ci.org/andialbrecht/sqlparse diff --git a/examples/column_defs_lowlevel.py b/examples/column_defs_lowlevel.py index 6bd5072..2c5f3b8 100644 --- a/examples/column_defs_lowlevel.py +++ b/examples/column_defs_lowlevel.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # # Copyright (C) 2009-2018 the sqlparse authors and contributors # <see AUTHORS file> diff --git a/examples/extract_table_names.py b/examples/extract_table_names.py index 8f0065e..4f6b4b0 100644 --- a/examples/extract_table_names.py +++ b/examples/extract_table_names.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # # Copyright (C) 2009-2018 the sqlparse authors and contributors # <see AUTHORS file> @@ -32,8 +31,7 @@ def extract_from_part(parsed): for item in parsed.tokens: if from_seen: if is_subselect(item): - for x in extract_from_part(item): - yield x + yield from extract_from_part(item) elif item.ttype is Keyword: return else: @@ -67,4 +65,4 @@ if __name__ == '__main__': """ tables = ', '.join(extract_tables(sql)) - print('Tables: {0}'.format(tables)) + print('Tables: {}'.format(tables)) @@ -1,6 +1,3 @@ -[bdist_wheel] -universal = 1 - [metadata] license_file = LICENSE @@ -8,8 +5,6 @@ license_file = LICENSE xfail_strict = True [flake8] -exclude = - sqlparse/compat.py ignore = W503, E731 @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # # Copyright (C) 2009-2018 the sqlparse authors and contributors # <see AUTHORS file> @@ -84,17 +83,15 @@ setup( description='Non-validating SQL parser', long_description=LONG_DESCRIPTION, license='BSD', - python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", + python_requires=">=3.5", classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', diff --git a/sqlparse/__init__.py b/sqlparse/__init__.py index b475061..5dcd982 100644 --- a/sqlparse/__init__.py +++ b/sqlparse/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2009-2018 the sqlparse authors and contributors # <see AUTHORS file> @@ -16,7 +15,6 @@ from sqlparse import tokens from sqlparse import filters from sqlparse import formatter -from sqlparse.compat import text_type __version__ = '0.3.2.dev0' __all__ = ['engine', 'filters', 'formatter', 'sql', 'tokens', 'cli'] @@ -58,7 +56,7 @@ def format(sql, encoding=None, **options): options = formatter.validate_options(options) stack = formatter.build_filter_stack(stack, options) stack.postprocess.append(filters.SerializerUnicode()) - return u''.join(stack.run(sql, encoding)) + return ''.join(stack.run(sql, encoding)) def split(sql, encoding=None): @@ -69,4 +67,4 @@ def split(sql, encoding=None): :returns: A list of strings. """ stack = engine.FilterStack() - return [text_type(stmt).strip() for stmt in stack.run(sql, encoding)] + return [str(stmt).strip() for stmt in stack.run(sql, encoding)] diff --git a/sqlparse/__main__.py b/sqlparse/__main__.py index 867d75d..3e6d325 100644 --- a/sqlparse/__main__.py +++ b/sqlparse/__main__.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # # Copyright (C) 2009-2018 the sqlparse authors and contributors # <see AUTHORS file> diff --git a/sqlparse/cli.py b/sqlparse/cli.py index 25555a5..957d220 100755 --- a/sqlparse/cli.py +++ b/sqlparse/cli.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # # Copyright (C) 2009-2018 the sqlparse authors and contributors # <see AUTHORS file> @@ -23,10 +22,8 @@ Why does this file exist, and why not put this in __main__? import argparse import sys from io import TextIOWrapper -from codecs import open, getreader import sqlparse -from sqlparse.compat import PY2 from sqlparse.exceptions import SQLParseError @@ -62,16 +59,16 @@ def create_parser(): metavar='CHOICE', dest='keyword_case', choices=_CASE_CHOICES, - help='change case of keywords, CHOICE is one of {0}'.format( - ', '.join('"{0}"'.format(x) for x in _CASE_CHOICES))) + help='change case of keywords, CHOICE is one of {}'.format( + ', '.join('"{}"'.format(x) for x in _CASE_CHOICES))) group.add_argument( '-i', '--identifiers', metavar='CHOICE', dest='identifier_case', choices=_CASE_CHOICES, - help='change case of identifiers, CHOICE is one of {0}'.format( - ', '.join('"{0}"'.format(x) for x in _CASE_CHOICES))) + help='change case of identifiers, CHOICE is one of {}'.format( + ', '.join('"{}"'.format(x) for x in _CASE_CHOICES))) group.add_argument( '-l', '--language', @@ -153,7 +150,7 @@ def create_parser(): def _error(msg): """Print msg and optionally exit with return code exit_.""" - sys.stderr.write(u'[ERROR] {0}\n'.format(msg)) + sys.stderr.write('[ERROR] {}\n'.format(msg)) return 1 @@ -162,29 +159,26 @@ def main(args=None): args = parser.parse_args(args) if args.filename == '-': # read from stdin - if PY2: - data = getreader(args.encoding)(sys.stdin).read() - else: - wrapper = TextIOWrapper(sys.stdin.buffer, encoding=args.encoding) - try: - data = wrapper.read() - finally: - wrapper.detach() + wrapper = TextIOWrapper(sys.stdin.buffer, encoding=args.encoding) + try: + data = wrapper.read() + finally: + wrapper.detach() else: try: - with open(args.filename, 'r', args.encoding) as f: + with open(args.filename, encoding=args.encoding) as f: data = ''.join(f.readlines()) - except IOError as e: + except OSError as e: return _error( - u'Failed to read {0}: {1}'.format(args.filename, e)) + 'Failed to read {}: {}'.format(args.filename, e)) close_stream = False if args.outfile: try: - stream = open(args.outfile, 'w', args.encoding) + stream = open(args.outfile, 'w', encoding=args.encoding) close_stream = True - except IOError as e: - return _error(u'Failed to open {0}: {1}'.format(args.outfile, e)) + except OSError as e: + return _error('Failed to open {}: {}'.format(args.outfile, e)) else: stream = sys.stdout @@ -192,7 +186,7 @@ def main(args=None): try: formatter_opts = sqlparse.formatter.validate_options(formatter_opts) except SQLParseError as e: - return _error(u'Invalid options: {0}'.format(e)) + return _error('Invalid options: {}'.format(e)) s = sqlparse.format(data, **formatter_opts) stream.write(s) diff --git a/sqlparse/compat.py b/sqlparse/compat.py deleted file mode 100644 index d2214be..0000000 --- a/sqlparse/compat.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2009-2018 the sqlparse authors and contributors -# <see AUTHORS file> -# -# This module is part of python-sqlparse and is released under -# the BSD License: https://opensource.org/licenses/BSD-3-Clause - -"""Python 2/3 compatibility. - -This module only exists to avoid a dependency on six -for very trivial stuff. We only need to take care of -string types, buffers and metaclasses. - -Parts of the code is copied directly from six: -https://bitbucket.org/gutworth/six -""" - -import sys -from io import TextIOBase - -PY2 = sys.version_info[0] == 2 -PY3 = sys.version_info[0] == 3 - - -if PY3: - def unicode_compatible(cls): - return cls - - text_type = str - string_types = (str,) - from io import StringIO - file_types = (StringIO, TextIOBase) - - -elif PY2: - def unicode_compatible(cls): - cls.__unicode__ = cls.__str__ - cls.__str__ = lambda x: x.__unicode__().encode('utf-8') - return cls - - text_type = unicode - string_types = (str, unicode,) - from StringIO import StringIO - file_types = (file, StringIO, TextIOBase) diff --git a/sqlparse/engine/__init__.py b/sqlparse/engine/__init__.py index 0b3f3eb..67ecc6e 100644 --- a/sqlparse/engine/__init__.py +++ b/sqlparse/engine/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2009-2018 the sqlparse authors and contributors # <see AUTHORS file> diff --git a/sqlparse/engine/filter_stack.py b/sqlparse/engine/filter_stack.py index fc77fd6..295c2c1 100644 --- a/sqlparse/engine/filter_stack.py +++ b/sqlparse/engine/filter_stack.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2009-2018 the sqlparse authors and contributors # <see AUTHORS file> @@ -13,7 +12,7 @@ from sqlparse.engine import grouping from sqlparse.engine.statement_splitter import StatementSplitter -class FilterStack(object): +class FilterStack: def __init__(self): self.preprocess = [] self.stmtprocess = [] diff --git a/sqlparse/engine/grouping.py b/sqlparse/engine/grouping.py index e7a4211..e895ce0 100644 --- a/sqlparse/engine/grouping.py +++ b/sqlparse/engine/grouping.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2009-2018 the sqlparse authors and contributors # <see AUTHORS file> diff --git a/sqlparse/engine/statement_splitter.py b/sqlparse/engine/statement_splitter.py index 1e9af3c..fb22c65 100644 --- a/sqlparse/engine/statement_splitter.py +++ b/sqlparse/engine/statement_splitter.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2009-2018 the sqlparse authors and contributors # <see AUTHORS file> @@ -9,7 +8,7 @@ from sqlparse import sql, tokens as T -class StatementSplitter(object): +class StatementSplitter: """Filter that split stream at individual statements""" def __init__(self): diff --git a/sqlparse/exceptions.py b/sqlparse/exceptions.py index 01e60f7..687c20c 100644 --- a/sqlparse/exceptions.py +++ b/sqlparse/exceptions.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2009-2018 the sqlparse authors and contributors # <see AUTHORS file> diff --git a/sqlparse/filters/__init__.py b/sqlparse/filters/__init__.py index c60d84d..ced03ea 100644 --- a/sqlparse/filters/__init__.py +++ b/sqlparse/filters/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2009-2018 the sqlparse authors and contributors # <see AUTHORS file> diff --git a/sqlparse/filters/aligned_indent.py b/sqlparse/filters/aligned_indent.py index 85b11e5..445d022 100644 --- a/sqlparse/filters/aligned_indent.py +++ b/sqlparse/filters/aligned_indent.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2009-2018 the sqlparse authors and contributors # <see AUTHORS file> @@ -7,11 +6,10 @@ # the BSD License: https://opensource.org/licenses/BSD-3-Clause from sqlparse import sql, tokens as T -from sqlparse.compat import text_type from sqlparse.utils import offset, indent -class AlignedIndentFilter(object): +class AlignedIndentFilter: join_words = (r'((LEFT\s+|RIGHT\s+|FULL\s+)?' r'(INNER\s+|OUTER\s+|STRAIGHT\s+)?|' r'(CROSS\s+|NATURAL\s+)?)?JOIN\b') @@ -73,7 +71,7 @@ class AlignedIndentFilter(object): end_token = tlist.token_next_by(m=(T.Keyword, 'END'))[1] cases.append((None, [end_token])) - condition_width = [len(' '.join(map(text_type, cond))) if cond else 0 + condition_width = [len(' '.join(map(str, cond))) if cond else 0 for cond, _ in cases] max_cond_width = max(condition_width) @@ -82,8 +80,7 @@ class AlignedIndentFilter(object): stmt = cond[0] if cond else value[0] if i > 0: - tlist.insert_before(stmt, self.nl( - offset_ - len(text_type(stmt)))) + tlist.insert_before(stmt, self.nl(offset_ - len(str(stmt)))) if cond: ws = sql.Token(T.Whitespace, self.char * ( max_cond_width - condition_width[i])) @@ -110,7 +107,7 @@ class AlignedIndentFilter(object): ): token_indent = token.value.split()[0] else: - token_indent = text_type(token) + token_indent = str(token) tlist.insert_before(token, self.nl(token_indent)) tidx += 1 tidx, token = self._next_token(tlist, tidx) diff --git a/sqlparse/filters/others.py b/sqlparse/filters/others.py index 52b8617..ff04b0e 100644 --- a/sqlparse/filters/others.py +++ b/sqlparse/filters/others.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2009-2018 the sqlparse authors and contributors # <see AUTHORS file> @@ -10,7 +9,7 @@ from sqlparse import sql, tokens as T from sqlparse.utils import split_unquoted_newlines -class StripCommentsFilter(object): +class StripCommentsFilter: @staticmethod def _process(tlist): def get_next_comment(): @@ -45,7 +44,7 @@ class StripCommentsFilter(object): return stmt -class StripWhitespaceFilter(object): +class StripWhitespaceFilter: def _stripws(self, tlist): func_name = '_stripws_{cls}'.format(cls=type(tlist).__name__) func = getattr(self, func_name.lower(), self._stripws_default) @@ -90,7 +89,7 @@ class StripWhitespaceFilter(object): return stmt -class SpacesAroundOperatorsFilter(object): +class SpacesAroundOperatorsFilter: @staticmethod def _process(tlist): @@ -118,7 +117,7 @@ class SpacesAroundOperatorsFilter(object): # --------------------------- # postprocess -class SerializerUnicode(object): +class SerializerUnicode: @staticmethod def process(stmt): lines = split_unquoted_newlines(stmt) diff --git a/sqlparse/filters/output.py b/sqlparse/filters/output.py index 3fbc46d..f34b29f 100644 --- a/sqlparse/filters/output.py +++ b/sqlparse/filters/output.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2009-2018 the sqlparse authors and contributors # <see AUTHORS file> @@ -7,10 +6,9 @@ # the BSD License: https://opensource.org/licenses/BSD-3-Clause from sqlparse import sql, tokens as T -from sqlparse.compat import text_type -class OutputFilter(object): +class OutputFilter: varname_prefix = '' def __init__(self, varname='sql'): @@ -23,11 +21,11 @@ class OutputFilter(object): def process(self, stmt): self.count += 1 if self.count > 1: - varname = u'{f.varname}{f.count}'.format(f=self) + varname = '{f.varname}{f.count}'.format(f=self) else: varname = self.varname - has_nl = len(text_type(stmt).strip().splitlines()) > 1 + has_nl = len(str(stmt).strip().splitlines()) > 1 stmt.tokens = self._process(stmt.tokens, varname, has_nl) return stmt diff --git a/sqlparse/filters/reindent.py b/sqlparse/filters/reindent.py index acec8ca..2b0f106 100644 --- a/sqlparse/filters/reindent.py +++ b/sqlparse/filters/reindent.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2009-2018 the sqlparse authors and contributors # <see AUTHORS file> @@ -7,11 +6,10 @@ # the BSD License: https://opensource.org/licenses/BSD-3-Clause from sqlparse import sql, tokens as T -from sqlparse.compat import text_type from sqlparse.utils import offset, indent -class ReindentFilter(object): +class ReindentFilter: def __init__(self, width=2, char=' ', wrap_after=0, n='\n', comma_first=False, indent_after_first=False, indent_columns=False): @@ -42,7 +40,7 @@ class ReindentFilter(object): return self.offset + self.indent * self.width def _get_offset(self, token): - raw = u''.join(map(text_type, self._flatten_up_to_token(token))) + raw = ''.join(map(str, self._flatten_up_to_token(token))) line = (raw or '\n').splitlines()[-1] # Now take current offset into account and return relative offset. return len(line) - len(self.char * self.leading_ws) @@ -71,7 +69,7 @@ class ReindentFilter(object): tidx, token = self._next_token(tlist) while token: pidx, prev_ = tlist.token_prev(tidx, skip_ws=False) - uprev = text_type(prev_) + uprev = str(prev_) if prev_ and prev_.is_whitespace: del tlist.tokens[pidx] @@ -234,7 +232,7 @@ class ReindentFilter(object): self._process(stmt) if self._last_stmt is not None: - nl = '\n' if text_type(self._last_stmt).endswith('\n') else '\n\n' + nl = '\n' if str(self._last_stmt).endswith('\n') else '\n\n' stmt.tokens.insert(0, sql.Token(T.Whitespace, nl)) self._last_stmt = stmt diff --git a/sqlparse/filters/right_margin.py b/sqlparse/filters/right_margin.py index 1658138..e263718 100644 --- a/sqlparse/filters/right_margin.py +++ b/sqlparse/filters/right_margin.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2009-2018 the sqlparse authors and contributors # <see AUTHORS file> @@ -9,11 +8,10 @@ import re from sqlparse import sql, tokens as T -from sqlparse.compat import text_type # FIXME: Doesn't work -class RightMarginFilter(object): +class RightMarginFilter: keep_together = ( # sql.TypeCast, sql.Identifier, sql.Alias, ) @@ -32,14 +30,14 @@ class RightMarginFilter(object): elif token.is_group and type(token) not in self.keep_together: token.tokens = self._process(token, token.tokens) else: - val = text_type(token) + val = str(token) if len(self.line) + len(val) > self.width: match = re.search(r'^ +', self.line) if match is not None: indent = match.group() else: indent = '' - yield sql.Token(T.Whitespace, '\n{0}'.format(indent)) + yield sql.Token(T.Whitespace, '\n{}'.format(indent)) self.line = indent self.line += val yield token diff --git a/sqlparse/filters/tokens.py b/sqlparse/filters/tokens.py index 93182b1..a40f4b0 100644 --- a/sqlparse/filters/tokens.py +++ b/sqlparse/filters/tokens.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2009-2018 the sqlparse authors and contributors # <see AUTHORS file> @@ -7,15 +6,14 @@ # the BSD License: https://opensource.org/licenses/BSD-3-Clause from sqlparse import tokens as T -from sqlparse.compat import text_type -class _CaseFilter(object): +class _CaseFilter: ttype = None def __init__(self, case=None): case = case or 'upper' - self.convert = getattr(text_type, case) + self.convert = getattr(str, case) def process(self, stream): for ttype, value in stream: @@ -38,7 +36,7 @@ class IdentifierCaseFilter(_CaseFilter): yield ttype, value -class TruncateStringFilter(object): +class TruncateStringFilter: def __init__(self, width, char): self.width = width self.char = char diff --git a/sqlparse/formatter.py b/sqlparse/formatter.py index 8962759..8016f87 100644 --- a/sqlparse/formatter.py +++ b/sqlparse/formatter.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2009-2018 the sqlparse authors and contributors # <see AUTHORS file> @@ -17,32 +16,32 @@ def validate_options(options): kwcase = options.get('keyword_case') if kwcase not in [None, 'upper', 'lower', 'capitalize']: raise SQLParseError('Invalid value for keyword_case: ' - '{0!r}'.format(kwcase)) + '{!r}'.format(kwcase)) idcase = options.get('identifier_case') if idcase not in [None, 'upper', 'lower', 'capitalize']: raise SQLParseError('Invalid value for identifier_case: ' - '{0!r}'.format(idcase)) + '{!r}'.format(idcase)) ofrmt = options.get('output_format') if ofrmt not in [None, 'sql', 'python', 'php']: raise SQLParseError('Unknown output format: ' - '{0!r}'.format(ofrmt)) + '{!r}'.format(ofrmt)) strip_comments = options.get('strip_comments', False) if strip_comments not in [True, False]: raise SQLParseError('Invalid value for strip_comments: ' - '{0!r}'.format(strip_comments)) + '{!r}'.format(strip_comments)) space_around_operators = options.get('use_space_around_operators', False) if space_around_operators not in [True, False]: raise SQLParseError('Invalid value for use_space_around_operators: ' - '{0!r}'.format(space_around_operators)) + '{!r}'.format(space_around_operators)) strip_ws = options.get('strip_whitespace', False) if strip_ws not in [True, False]: raise SQLParseError('Invalid value for strip_whitespace: ' - '{0!r}'.format(strip_ws)) + '{!r}'.format(strip_ws)) truncate_strings = options.get('truncate_strings') if truncate_strings is not None: @@ -50,17 +49,17 @@ def validate_options(options): truncate_strings = int(truncate_strings) except (ValueError, TypeError): raise SQLParseError('Invalid value for truncate_strings: ' - '{0!r}'.format(truncate_strings)) + '{!r}'.format(truncate_strings)) if truncate_strings <= 1: raise SQLParseError('Invalid value for truncate_strings: ' - '{0!r}'.format(truncate_strings)) + '{!r}'.format(truncate_strings)) options['truncate_strings'] = truncate_strings options['truncate_char'] = options.get('truncate_char', '[...]') indent_columns = options.get('indent_columns', False) if indent_columns not in [True, False]: raise SQLParseError('Invalid value for indent_columns: ' - '{0!r}'.format(indent_columns)) + '{!r}'.format(indent_columns)) elif indent_columns: options['reindent'] = True # enforce reindent options['indent_columns'] = indent_columns @@ -68,27 +67,27 @@ def validate_options(options): reindent = options.get('reindent', False) if reindent not in [True, False]: raise SQLParseError('Invalid value for reindent: ' - '{0!r}'.format(reindent)) + '{!r}'.format(reindent)) elif reindent: options['strip_whitespace'] = True reindent_aligned = options.get('reindent_aligned', False) if reindent_aligned not in [True, False]: raise SQLParseError('Invalid value for reindent_aligned: ' - '{0!r}'.format(reindent)) + '{!r}'.format(reindent)) elif reindent_aligned: options['strip_whitespace'] = True indent_after_first = options.get('indent_after_first', False) if indent_after_first not in [True, False]: raise SQLParseError('Invalid value for indent_after_first: ' - '{0!r}'.format(indent_after_first)) + '{!r}'.format(indent_after_first)) options['indent_after_first'] = indent_after_first indent_tabs = options.get('indent_tabs', False) if indent_tabs not in [True, False]: raise SQLParseError('Invalid value for indent_tabs: ' - '{0!r}'.format(indent_tabs)) + '{!r}'.format(indent_tabs)) elif indent_tabs: options['indent_char'] = '\t' else: diff --git a/sqlparse/keywords.py b/sqlparse/keywords.py index 9c37e50..3123531 100644 --- a/sqlparse/keywords.py +++ b/sqlparse/keywords.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2009-2018 the sqlparse authors and contributors # <see AUTHORS file> diff --git a/sqlparse/lexer.py b/sqlparse/lexer.py index fd007a4..836d53d 100644 --- a/sqlparse/lexer.py +++ b/sqlparse/lexer.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2009-2018 the sqlparse authors and contributors # <see AUTHORS file> @@ -13,13 +12,14 @@ # It's separated from the rest of pygments to increase performance # and to allow some customizations. +from io import TextIOBase + from sqlparse import tokens from sqlparse.keywords import SQL_REGEX -from sqlparse.compat import text_type, file_types from sqlparse.utils import consume -class Lexer(object): +class Lexer: """Lexer Empty class. Leaving for backwards-compatibility """ @@ -38,10 +38,10 @@ class Lexer(object): ``stack`` is the initial stack (default: ``['root']``) """ - if isinstance(text, file_types): + if isinstance(text, TextIOBase): text = text.read() - if isinstance(text, text_type): + if isinstance(text, str): pass elif isinstance(text, bytes): if encoding: @@ -52,7 +52,7 @@ class Lexer(object): except UnicodeDecodeError: text = text.decode('unicode-escape') else: - raise TypeError(u"Expected text or file-like object, got {!r}". + raise TypeError("Expected text or file-like object, got {!r}". format(type(text))) iterable = enumerate(text) diff --git a/sqlparse/sql.py b/sqlparse/sql.py index a942bcd..a9e11da 100644 --- a/sqlparse/sql.py +++ b/sqlparse/sql.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2009-2018 the sqlparse authors and contributors # <see AUTHORS file> @@ -7,12 +6,10 @@ # the BSD License: https://opensource.org/licenses/BSD-3-Clause """This module contains classes representing syntactical elements of SQL.""" -from __future__ import print_function import re from sqlparse import tokens as T -from sqlparse.compat import string_types, text_type, unicode_compatible from sqlparse.utils import imt, remove_quotes @@ -39,8 +36,7 @@ class NameAliasMixin: return self._get_first_name(reverse=True) -@unicode_compatible -class Token(object): +class Token: """Base class for all other classes in this module. It represents a single token and has two instance attributes: @@ -52,7 +48,7 @@ class Token(object): 'is_group', 'is_whitespace') def __init__(self, ttype, value): - value = text_type(value) + value = str(value) self.value = value self.ttype = ttype self.parent = None @@ -72,15 +68,15 @@ class Token(object): cls = self._get_repr_name() value = self._get_repr_value() - q = u'"' if value.startswith("'") and value.endswith("'") else u"'" - return u"<{cls} {q}{value}{q} at 0x{id:2X}>".format( + q = '"' if value.startswith("'") and value.endswith("'") else "'" + return "<{cls} {q}{value}{q} at 0x{id:2X}>".format( id=id(self), **locals()) def _get_repr_name(self): return str(self.ttype).split('.')[-1] def _get_repr_value(self): - raw = text_type(self) + raw = str(self) if len(raw) > 7: raw = raw[:6] + '...' return re.sub(r'\s+', ' ', raw) @@ -105,7 +101,7 @@ class Token(object): if not type_matched or values is None: return type_matched - if isinstance(values, string_types): + if isinstance(values, str): values = (values,) if regex: @@ -150,7 +146,6 @@ class Token(object): return False -@unicode_compatible class TokenList(Token): """A group of tokens. @@ -163,11 +158,11 @@ class TokenList(Token): def __init__(self, tokens=None): self.tokens = tokens or [] [setattr(token, 'parent', self) for token in self.tokens] - super(TokenList, self).__init__(None, text_type(self)) + super().__init__(None, str(self)) self.is_group = True def __str__(self): - return u''.join(token.value for token in self.flatten()) + return ''.join(token.value for token in self.flatten()) # weird bug # def __len__(self): @@ -190,14 +185,14 @@ class TokenList(Token): value = token._get_repr_value() last = idx == (token_count - 1) - pre = u'`- ' if last else u'|- ' + pre = '`- ' if last else '|- ' - q = u'"' if value.startswith("'") and value.endswith("'") else u"'" - print(u"{_pre}{pre}{idx} {cls} {q}{value}{q}" + q = '"' if value.startswith("'") and value.endswith("'") else "'" + print("{_pre}{pre}{idx} {cls} {q}{value}{q}" .format(**locals()), file=f) if token.is_group and (max_depth is None or depth < max_depth): - parent_pre = u' ' if last else u'| ' + parent_pre = ' ' if last else '| ' token._pprint_tree(max_depth, depth + 1, f, _pre + parent_pre) def get_token_at_offset(self, offset): @@ -216,8 +211,7 @@ class TokenList(Token): """ for token in self.tokens: if token.is_group: - for item in token.flatten(): - yield item + yield from token.flatten() else: yield token @@ -328,7 +322,7 @@ class TokenList(Token): grp = start grp.tokens.extend(subtokens) del self.tokens[start_idx + 1:end_idx] - grp.value = text_type(start) + grp.value = str(start) else: subtokens = self.tokens[start_idx:end_idx] grp = grp_cls(subtokens) diff --git a/sqlparse/tokens.py b/sqlparse/tokens.py index eefc0b4..193d100 100644 --- a/sqlparse/tokens.py +++ b/sqlparse/tokens.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2009-2018 the sqlparse authors and contributors # <see AUTHORS file> diff --git a/sqlparse/utils.py b/sqlparse/utils.py index 3283274..265378b 100644 --- a/sqlparse/utils.py +++ b/sqlparse/utils.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2009-2018 the sqlparse authors and contributors # <see AUTHORS file> @@ -10,7 +9,6 @@ import itertools import re from collections import deque from contextlib import contextmanager -from sqlparse.compat import text_type # This regular expression replaces the home-cooked parser that was here before. # It is much faster, but requires an extra post-processing step to get the @@ -40,7 +38,7 @@ def split_unquoted_newlines(stmt): Unlike str.splitlines(), this will ignore CR/LF/CR+LF if the requisite character is inside of a string.""" - text = text_type(stmt) + text = str(stmt) lines = SPLIT_REGEX.split(text) outputlines = [''] for line in lines: diff --git a/tests/conftest.py b/tests/conftest.py index f2473a4..939c481 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """Helpers for testing.""" import io @@ -35,7 +33,7 @@ def load_file(filepath): # https://stackoverflow.com/questions/18011902/py-test-pass-a-parameter-to-a-fixture-function/33879151#33879151 # Syntax is noisy and requires specific variable names # And seems to be limited to only 1 argument. - with io.open(filepath(filename), encoding=encoding) as f: + with open(filepath(filename), encoding=encoding) as f: return f.read().strip() return make_load_file @@ -44,6 +42,6 @@ def load_file(filepath): @pytest.fixture() def get_stream(filepath): def make_stream(filename, encoding='utf-8'): - return io.open(filepath(filename), encoding=encoding) + return open(filepath(filename), encoding=encoding) return make_stream diff --git a/tests/test_cli.py b/tests/test_cli.py index 5f1ea0e..746b632 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import subprocess import sys @@ -71,7 +69,7 @@ def test_stdout(filepath, load_file, capsys): def test_script(): # Call with the --help option as a basic sanity check. - cmd = "{0:s} -m sqlparse.cli --help".format(sys.executable) + cmd = "{:s} -m sqlparse.cli --help".format(sys.executable) assert subprocess.call(cmd.split()) == 0 @@ -115,7 +113,7 @@ def test_encoding_stdin_utf8(filepath, load_file, capfd): path = filepath('encoding_utf8.sql') expected = load_file('encoding_utf8.sql', 'utf-8') old_stdin = sys.stdin - with open(path, 'r') as f: + with open(path) as f: sys.stdin = f sys.stdout.encoding = 'utf-8' sqlparse.cli.main(['-']) @@ -128,7 +126,7 @@ def test_encoding_stdin_gbk(filepath, load_file, capfd): path = filepath('encoding_gbk.sql') expected = load_file('encoding_gbk.sql', 'gbk') old_stdin = sys.stdin - with open(path, 'r') as stream: + with open(path) as stream: sys.stdin = stream sys.stdout.encoding = 'gbk' sqlparse.cli.main(['-', '--encoding', 'gbk']) @@ -139,7 +137,7 @@ def test_encoding_stdin_gbk(filepath, load_file, capfd): def test_encoding(filepath, capsys): path = filepath('test_cp1251.sql') - expected = u'insert into foo values (1); -- Песня про надежду\n' + expected = 'insert into foo values (1); -- Песня про надежду\n' sqlparse.cli.main([path, '--encoding=cp1251']) out, _ = capsys.readouterr() assert out == expected diff --git a/tests/test_format.py b/tests/test_format.py index 811e083..20390ff 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -1,12 +1,10 @@ -# -*- coding: utf-8 -*- - import pytest import sqlparse from sqlparse.exceptions import SQLParseError -class TestFormat(object): +class TestFormat: def test_keywordcase(self): sql = 'select * from bar; -- select foo\n' res = sqlparse.format(sql, keyword_case='upper') @@ -123,7 +121,7 @@ class TestFormat(object): == "SELECT some_column LIKE 'value\\\\\\'\r' WHERE id = 1\n") -class TestFormatReindentAligned(object): +class TestFormatReindentAligned: @staticmethod def formatter(sql): return sqlparse.format(sql, reindent_aligned=True) @@ -294,7 +292,7 @@ class TestFormatReindentAligned(object): ' from table']) -class TestSpacesAroundOperators(object): +class TestSpacesAroundOperators: @staticmethod def formatter(sql): return sqlparse.format(sql, use_space_around_operators=True) @@ -321,7 +319,7 @@ class TestSpacesAroundOperators(object): assert self.formatter(sql) == 'select a * b - c from table' -class TestFormatReindent(object): +class TestFormatReindent: def test_option(self): with pytest.raises(SQLParseError): sqlparse.format('foo', reindent=2) @@ -598,7 +596,7 @@ class TestFormatReindent(object): ' , (5, 6)']) -class TestOutputFormat(object): +class TestOutputFormat: def test_python(self): sql = 'select * from foo;' f = lambda sql: sqlparse.format(sql, output_format='python') @@ -663,7 +661,7 @@ def test_format_column_ordering(): def test_truncate_strings(): - sql = "update foo set value = '{0}';".format('x' * 1000) + sql = "update foo set value = '{}';".format('x' * 1000) formatted = sqlparse.format(sql, truncate_strings=10) assert formatted == "update foo set value = 'xxxxxxxxxx[...]';" formatted = sqlparse.format(sql, truncate_strings=3, truncate_char='YYY') diff --git a/tests/test_grouping.py b/tests/test_grouping.py index 87dcf11..683954e 100644 --- a/tests/test_grouping.py +++ b/tests/test_grouping.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import pytest import sqlparse @@ -484,7 +482,7 @@ def test_comparison_with_parenthesis(): )) def test_comparison_with_strings(operator): # issue148 - p = sqlparse.parse("foo {0} 'bar'".format(operator))[0] + p = sqlparse.parse("foo {} 'bar'".format(operator))[0] assert len(p.tokens) == 1 assert isinstance(p.tokens[0], sql.Comparison) assert p.tokens[0].right.value == "'bar'" @@ -563,7 +561,7 @@ def test_comparison_with_typed_literal(): @pytest.mark.parametrize('start', ['FOR', 'FOREACH']) def test_forloops(start): - p = sqlparse.parse('{0} foo in bar LOOP foobar END LOOP'.format(start))[0] + p = sqlparse.parse('{} foo in bar LOOP foobar END LOOP'.format(start))[0] assert (len(p.tokens)) == 1 assert isinstance(p.tokens[0], sql.For) diff --git a/tests/test_keywords.py b/tests/test_keywords.py index c197f36..d4ded4b 100644 --- a/tests/test_keywords.py +++ b/tests/test_keywords.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import pytest from sqlparse import tokens diff --git a/tests/test_parse.py b/tests/test_parse.py index c28cb06..6cce909 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -1,12 +1,10 @@ -# -*- coding: utf-8 -*- - """Tests sqlparse.parse().""" +from io import StringIO import pytest import sqlparse from sqlparse import sql, tokens as T -from sqlparse.compat import StringIO, text_type def test_parse_tokenize(): @@ -409,26 +407,26 @@ def test_dbldollar_as_literal(sql, is_literal): def test_non_ascii(): - _test_non_ascii = u"insert into test (id, name) values (1, 'тест');" + _test_non_ascii = "insert into test (id, name) values (1, 'тест');" s = _test_non_ascii stmts = sqlparse.parse(s) assert len(stmts) == 1 statement = stmts[0] - assert text_type(statement) == s + assert str(statement) == s assert statement._pprint_tree() is None s = _test_non_ascii.encode('utf-8') stmts = sqlparse.parse(s, 'utf-8') assert len(stmts) == 1 statement = stmts[0] - assert text_type(statement) == _test_non_ascii + assert str(statement) == _test_non_ascii assert statement._pprint_tree() is None def test_get_real_name(): # issue 369 - s = u"update a t set t.b=1" + s = "update a t set t.b=1" stmts = sqlparse.parse(s) assert len(stmts) == 1 assert 'a' == stmts[0].tokens[2].get_real_name() @@ -437,14 +435,14 @@ def test_get_real_name(): def test_from_subquery(): # issue 446 - s = u'from(select 1)' + s = 'from(select 1)' stmts = sqlparse.parse(s) assert len(stmts) == 1 assert len(stmts[0].tokens) == 2 assert stmts[0].tokens[0].value == 'from' assert stmts[0].tokens[0].ttype == T.Keyword - s = u'from (select 1)' + s = 'from (select 1)' stmts = sqlparse.parse(s) assert len(stmts) == 1 assert len(stmts[0].tokens) == 3 diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 2ed0ff3..648cc4a 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -1,10 +1,7 @@ -# -*- coding: utf-8 -*- - import pytest import sqlparse from sqlparse import sql, tokens as T -from sqlparse.compat import PY2 def test_issue9(): @@ -20,9 +17,9 @@ def test_issue9(): def test_issue13(): - parsed = sqlparse.parse(("select 'one';\n" - "select 'two\\'';\n" - "select 'three';")) + parsed = sqlparse.parse("select 'one';\n" + "select 'two\\'';\n" + "select 'three';") assert len(parsed) == 3 assert str(parsed[1]).strip() == "select 'two\\'';" @@ -73,8 +70,8 @@ def test_issue39(): def test_issue40(): # make sure identifier lists in subselects are grouped - p = sqlparse.parse(('SELECT id, name FROM ' - '(SELECT id, name FROM bar) as foo'))[0] + p = sqlparse.parse('SELECT id, name FROM ' + '(SELECT id, name FROM bar) as foo')[0] assert len(p.tokens) == 7 assert p.tokens[2].__class__ == sql.IdentifierList assert p.tokens[-1].__class__ == sql.Identifier @@ -149,7 +146,7 @@ def test_issue83(): def test_comment_encoding_when_reindent(): # There was an UnicodeEncodeError in the reindent filter that # casted every comment followed by a keyword to str. - sql = u'select foo -- Comment containing Ümläuts\nfrom bar' + sql = 'select foo -- Comment containing Ümläuts\nfrom bar' formatted = sqlparse.format(sql, reindent=True) assert formatted == sql @@ -158,11 +155,9 @@ def test_parse_sql_with_binary(): # See https://github.com/andialbrecht/sqlparse/pull/88 # digest = '|ËêplL4¡høN{' digest = '\x82|\xcb\x0e\xea\x8aplL4\xa1h\x91\xf8N{' - sql = "select * from foo where bar = '{0}'".format(digest) + sql = "select * from foo where bar = '{}'".format(digest) formatted = sqlparse.format(sql, reindent=True) - tformatted = "select *\nfrom foo\nwhere bar = '{0}'".format(digest) - if PY2: - tformatted = tformatted.decode('unicode-escape') + tformatted = "select *\nfrom foo\nwhere bar = '{}'".format(digest) assert formatted == tformatted @@ -180,7 +175,7 @@ def test_format_accepts_encoding(load_file): # issue20 sql = load_file('test_cp1251.sql', 'cp1251') formatted = sqlparse.format(sql, reindent=True, encoding='cp1251') - tformatted = u'insert into foo\nvalues (1); -- Песня про надежду' + tformatted = 'insert into foo\nvalues (1); -- Песня про надежду' assert formatted == tformatted @@ -275,7 +270,7 @@ def test_issue186_get_type(): def test_issue212_py2unicode(): - t1 = sql.Token(T.String, u'schöner ') + t1 = sql.Token(T.String, 'schöner ') t2 = sql.Token(T.String, 'bug') token_list = sql.TokenList([t1, t2]) assert str(token_list) == 'schöner bug' @@ -337,11 +332,9 @@ def test_issue315_utf8_by_default(): '\x9b\xb2.' '\xec\x82\xac\xeb\x9e\x91\xed\x95\xb4\xec\x9a\x94' ) - sql = "select * from foo where bar = '{0}'".format(digest) + sql = "select * from foo where bar = '{}'".format(digest) formatted = sqlparse.format(sql, reindent=True) - tformatted = "select *\nfrom foo\nwhere bar = '{0}'".format(digest) - if PY2: - tformatted = tformatted.decode('utf-8') + tformatted = "select *\nfrom foo\nwhere bar = '{}'".format(digest) assert formatted == tformatted diff --git a/tests/test_split.py b/tests/test_split.py index a93e3d4..8214756 100644 --- a/tests/test_split.py +++ b/tests/test_split.py @@ -1,13 +1,11 @@ -# -*- coding: utf-8 -*- - # Tests splitting functions. import types +from io import StringIO import pytest import sqlparse -from sqlparse.compat import StringIO, text_type def test_split_semicolon(): @@ -33,7 +31,7 @@ def test_split_create_function(load_file, fn): sql = load_file(fn) stmts = sqlparse.parse(sql) assert len(stmts) == 1 - assert text_type(stmts[0]) == sql + assert str(stmts[0]) == sql def test_split_dashcomments(load_file): @@ -74,12 +72,12 @@ def test_split_dropif(): def test_split_comment_with_umlaut(): - sql = (u'select * from foo;\n' - u'-- Testing an umlaut: ä\n' - u'select * from bar;') + sql = ('select * from foo;\n' + '-- Testing an umlaut: ä\n' + 'select * from bar;') stmts = sqlparse.parse(sql) assert len(stmts) == 2 - assert ''.join(text_type(q) for q in stmts) == sql + assert ''.join(str(q) for q in stmts) == sql def test_split_comment_end_of_line(): @@ -125,11 +123,11 @@ def test_split_stream(): def test_split_encoding_parsestream(): stream = StringIO("SELECT 1; SELECT 2;") stmts = list(sqlparse.parsestream(stream)) - assert isinstance(stmts[0].tokens[0].value, text_type) + assert isinstance(stmts[0].tokens[0].value, str) def test_split_unicode_parsestream(): - stream = StringIO(u'SELECT ö') + stream = StringIO('SELECT ö') stmts = list(sqlparse.parsestream(stream)) assert str(stmts[0]) == 'SELECT ö' diff --git a/tests/test_tokenize.py b/tests/test_tokenize.py index 3e8831b..0d7f878 100644 --- a/tests/test_tokenize.py +++ b/tests/test_tokenize.py @@ -1,13 +1,11 @@ -# -*- coding: utf-8 -*- - import types +from io import StringIO import pytest import sqlparse from sqlparse import lexer from sqlparse import sql, tokens as T -from sqlparse.compat import StringIO def test_tokenize_simple(): @@ -152,7 +150,7 @@ def test_stream_error(): 'INNER JOIN', 'LEFT INNER JOIN']) def test_parse_join(expr): - p = sqlparse.parse('{0} foo'.format(expr))[0] + p = sqlparse.parse('{} foo'.format(expr))[0] assert len(p.tokens) == 3 assert p.tokens[0].ttype is T.Keyword @@ -1,13 +1,11 @@ [tox] skip_missing_interpreters = True envlist = - py27 - py34 py35 py36 py37 py38 - pypy_54 + pypy3 flake8 [testenv] |
