diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 47d7912..3a92138 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,6 +27,6 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip setuptools - pip install -e . flake8${{ matrix.flake }} six + pip install -e . flake8${{ matrix.flake }} - name: Run tests run: python -m unittest -v diff --git a/README.rst b/README.rst index 27deda8..1bf8fa6 100644 --- a/README.rst +++ b/README.rst @@ -14,9 +14,7 @@ String format parameter checker :target: https://pypi.python.org/pypi/flake8-string-format An extension for `Flake8 `_ to check the -strings and parameters using ``str.format``. It checks all strings whether they -use numbered parameters with an implicit index which isn't support in -Python 2.6. +strings and parameters using ``str.format``. In all instances of ``'…'.format(…)`` it will also check whether there are enough parameters given. If the format call uses variable arguments, it'll just @@ -32,16 +30,13 @@ is available in ``flake8``:: $ flake8 --version 3.0.2 (flake8-string-format: 0.2.3, […] -This plugin supports Flake8 2.6 as well as Flake8 3.0. Older or newer versions -may be supported too but they weren't tested. - Via ``--ignore`` it's possible to ignore unindexed parameters:: $ flake8 some_file.py ... - some_file.py:1:1: FMT101 format string does contain unindexed parameters + some_file.py:1:1: FMT201 format call index too large (5 - $ flake8 --ignore FMT101 some_file.py + $ flake8 --ignore FMT201 some_file.py ... @@ -56,14 +51,6 @@ Error codes This plugin is using the following error codes: -+--------+---------------------------------------------------------------------+ -| Presence of implicit parameters | -+--------+---------------------------------------------------------------------+ -| FMT101 | format string contains unindexed parameters | -+--------+---------------------------------------------------------------------+ -| FMT102 | docstring contains unindexed parameters | -+--------+---------------------------------------------------------------------+ -| FMT103 | other string contains unindexed parameters | +--------+---------------------------------------------------------------------+ | Missing values in the parameters | +--------+---------------------------------------------------------------------+ @@ -88,19 +75,10 @@ This plugin is using the following error codes: Operation --------- -The plugin will go through all ``bytes``, ``str`` and ``unicode`` instances. If +The plugin will go through all ``bytes``, ``str``. If it encounters ``bytes`` instances on Python 3, it'll decode them using ASCII and if that fails it'll skip that entry. -Depending on the usage the string is handled differently. When it is not being -formatted, it can only cause ``FMT102`` and ``FMT103``. For this plugin all -strings which are the first expression of the module or after a function or -class definition are considered docstrings. - -Both ``FMT102`` and ``FMT103`` issue many false positives and should only be -used with Python 2.6 which does not support `unindexed parameters -`_. - Format strings `````````````` Every string where either the ``format`` method is called or where it is the @@ -113,18 +91,6 @@ FMT301 and FMT302 can still be checked for any argument which is defined statically. -Python 2.6 support -`````````````````` - -Python 2.6 is only partially supported as it's using Python's capability to -format a string. So if a string contains implicit parameters, it won't be -detected as a parameter on Python 2.6 and thus it won't cause any FMT1XX errors. -But it might still cause an error FMT301 when variable arguments aren't used. - -So if Python 2.6 compatibility is wished and thus implicit parameters aren't -allowed, this plugin won't cause false positives. - - Changes ------- 0.4.0 - 2026-06-09 diff --git a/flake8_string_format.py b/flake8_string_format.py index b4ec45c..9dc1a27 100755 --- a/flake8_string_format.py +++ b/flake8_string_format.py @@ -1,12 +1,9 @@ #!/usr/bin/python -# -*- coding: utf-8 -*- """Extension for flake8 to test string format usage.""" -from __future__ import print_function, unicode_literals import ast import itertools import re -import sys from string import Formatter @@ -21,7 +18,7 @@ class TextVisitor(ast.NodeVisitor): """ def __init__(self): - super(TextVisitor, self).__init__() + super().__init__() self.nodes = [] self.calls = {} @@ -31,29 +28,7 @@ def _add_node(self, node): self.nodes += [node] def is_base_string(self, node): - # Python 3.14 removed ast.Str/ast.Bytes, but older versions still use - # or expose them, so accept modern Constant nodes first and then fall - # back to whichever legacy node types still exist on this interpreter. - if isinstance(node, ast.Constant): - return isinstance(node.value, (str, bytes)) - - types = [] - if hasattr(ast, 'Str'): - types.append(ast.Str) - if sys.version_info[0] > 2 and hasattr(ast, 'Bytes'): - types.append(ast.Bytes) - - return isinstance(node, tuple(types)) - - def visit_Str(self, node): - # Constant with Python 3.8 uses the value-property - node.value = node.s - self._add_node(node) - - def visit_Bytes(self, node): - # Constant with Python 3.8 uses the value-property - node.value = node.s - self._add_node(node) + return isinstance(node, ast.Constant) and isinstance(node.value, (str, bytes)) def visit_Constant(self, node): if type(node.value) in (str, bytes): @@ -109,10 +84,10 @@ def visit_Call(self, node): node.func.value.id == 'str' and node.args and self.is_base_string(node.args[0])): self.calls[node.args[0]] = (node, True) - super(TextVisitor, self).generic_visit(node) + super().generic_visit(node) -class StringFormatChecker(object): +class StringFormatChecker: _FORMATTER = Formatter() FIELD_REGEX = re.compile(r'^((?:\s|.)*?)(\..*|\[.*\])?$') @@ -120,9 +95,6 @@ class StringFormatChecker(object): name = 'flake8-string-format' ERRORS = { - 101: 'format string contains unindexed parameters', - 102: 'docstring contains unindexed parameters', - 103: 'other string contains unindexed parameters', 201: 'format call index too large ({idx})', 202: 'format call uses missing keyword ({kw})', 203: 'format call uses keyword arguments but there are no keyword entries', @@ -135,15 +107,7 @@ class StringFormatChecker(object): def __init__(self, tree, filename): self.tree = tree - def _generate_unindexed(self, node): - return self._generate_error( - node, 102 if node.is_docstring else 103) - def _generate_error(self, node, code, **params): - if sys.version_info[:3] == (3, 4, 2) and isinstance(node, ast.Call): - # Due to https://bugs.python.org/issue21295 we cannot use the - # Call object - node = node.func.value msg = 'FMT{0} {1}'.format(code, self.ERRORS[code]) msg = msg.format(**params) return node.lineno, node.col_offset, msg, type(self) @@ -176,19 +140,13 @@ def run(self): assert not (set(visitor.calls) - set(visitor.nodes)) for node in visitor.nodes: text = node.value - if sys.version_info[0] > 2 and isinstance(text, bytes): + if isinstance(text, bytes): try: # TODO: Maybe decode using file encoding? text = text.decode('ascii') except UnicodeDecodeError as e: continue fields, implicit, explicit = self.get_fields(text) - if implicit: - if node in visitor.calls: - assert not node.is_docstring - yield self._generate_error(node, 101) - else: - yield self._generate_unindexed(node) if node in visitor.calls: call, str_args = visitor.calls[node] @@ -212,20 +170,14 @@ def run(self): num_args = len(call.args) if str_args: num_args -= 1 - if sys.version_info < (3, 5): - has_kwargs = bool(call.kwargs) - has_starargs = bool(call.starargs) - else: - # With Python version 3.5 the location and number of - # kwargs/starargs has been relaxed - has_kwargs = None in keywords - has_starargs = sum(1 for arg in call.args - if isinstance(arg, ast.Starred)) - - if has_kwargs: - keywords.discard(None) - if has_starargs: - num_args -= has_starargs + has_kwargs = None in keywords + has_starargs = sum(1 for arg in call.args + if isinstance(arg, ast.Starred)) + + if has_kwargs: + keywords.discard(None) + if has_starargs: + num_args -= has_starargs # if starargs or kwargs is not None, it can't count the # parameters but at least check if the args are used diff --git a/pyproject.toml b/pyproject.toml index 610d46b..d702945 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,8 +30,6 @@ classifiers = [ [project.urls] Homepage = "https://github.com/xZise/flake8-string-format" -[project.optional-dependencies] -test = ["six"] [project.entry-points."flake8.extension"] FMT = "flake8_string_format:StringFormatChecker" diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index ffe2fce..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1 +0,0 @@ -six diff --git a/test_flake8_string_format.py b/test_flake8_string_format.py index 6742b69..04b4e07 100644 --- a/test_flake8_string_format.py +++ b/test_flake8_string_format.py @@ -1,38 +1,22 @@ -from __future__ import print_function - import ast -import codecs import itertools -import optparse import os import re import sys -import tempfile - -PY26 = sys.version_info[:2] == (2, 6) - -if PY26: - import unittest2 as unittest -else: - import unittest +import unittest from collections import defaultdict from subprocess import Popen, PIPE -import six - import flake8_string_format def generate_code(): - if PY26: - working_formats = [2, 3] - else: - working_formats = [1, 2, 3] + working_formats = [1, 2, 3] code = ['#!/usr/bin/python', '# -*- coding: utf-8 -*-', 'dummy = "line"'] positions = [] for variant in itertools.product( - ['', '#', ' '], ['', 'u', 'b'], ['', '0', 'param'], ['', ':03'], + ['', '#', ' '], ['', 'b'], ['', '0', 'param'], ['', ':03'], ['', 'Before'], ['', 'After']): variant = list(variant) indented = variant[0].startswith(' ') @@ -61,12 +45,7 @@ def generate_code(): code += ['{0}{1}"{4}{{{2}{3}}}{5}"{fmt}'.format(*variant, fmt=fmt_code)] if not variant[2] and not variant[0].strip().startswith('#') and use_format in working_formats: column = len(variant[0]) - if PY26: - expected_code = 'FMT301' - if use_format == 3: - column -= len('str.format(') - else: - expected_code = 'FMT101' if use_format > 1 else 'FMT103' + expected_code = 'FMT101' if use_format > 1 else 'FMT103' positions += [(len(code), column, expected_code)] return '\n'.join(code), positions @@ -177,7 +156,7 @@ def __new__(cls, name, bases, dct): assert test.__name__ not in dct dct[test.__name__] = test - return super(ManualFileMetaClass, cls).__new__(cls, name, bases, dct) + return super().__new__(cls, name, bases, dct) @classmethod def _create_tests(cls, directory, filename): @@ -192,7 +171,7 @@ def first_find(string, searched): only_filename = filename filename = os.path.join(directory, filename) - with codecs.open(filename, 'r', 'utf8') as f: + with open(filename, 'r', encoding='utf8') as f: content = f.read() all_positions = [] lines = content.splitlines() @@ -248,8 +227,7 @@ def defaults(self): return defaults -@six.add_metaclass(ManualFileMetaClass) -class TestManualFiles(SimpleImportTestCase): +class TestManualFiles(SimpleImportTestCase, metaclass=ManualFileMetaClass): """Test the manually created files in tests/files/.""" @@ -294,12 +272,11 @@ def run_test(self, positions, filename, content): self.iterator(stdout_lines, expected_filename), positions) -@six.add_metaclass(ManualFileMetaClass) -class TestFlake8Files(Flake8CaseBase): +class TestFlake8Files(Flake8CaseBase, metaclass=ManualFileMetaClass): def run_test(self, positions, tree, filename): """Test using stdin.""" - super(TestFlake8Files, self).run_test(positions, filename, None) + super().run_test(positions, filename, None) class TestFlake8StdinDynamic(Flake8CaseBase): @@ -308,14 +285,13 @@ def test_dynamic(self): self.run_test(dynamic_positions, None, dynamic_code.encode('utf8')) -@six.add_metaclass(ManualFileMetaClass) -class TestFlake8Stdin(Flake8CaseBase): +class TestFlake8Stdin(Flake8CaseBase, metaclass=ManualFileMetaClass): def run_test(self, positions, tree, filename): """Test using stdin.""" with open(filename, 'rb') as f: content = f.read() - super(TestFlake8Stdin, self).run_test(positions, None, content) + super().run_test(positions, None, content) if __name__ == '__main__':