Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: PyCQA/flake8-bugbear
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 23.6.5
Choose a base ref
...
head repository: PyCQA/flake8-bugbear
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 23.7.10
Choose a head ref
  • 7 commits
  • 10 files changed
  • 6 contributors

Commits on Jun 26, 2023

  1. Remove use of the deprecated ast.Constant.s attribute (#392)

    The `.s` attribute on `ast.Constant` nodes is an alias to the `.value` attribute. The `.s` alias has been deprecated since Python 3.8, and causes a DeprecationWarning to be emitted on Python 3.12
    AlexWaygood authored Jun 26, 2023

    Verified

    This commit was signed with the committer’s verified signature.
    targos Michaël Zasso
    Copy the full SHA
    2763a13 View commit details
  2. Verified

    This commit was signed with the committer’s verified signature. The key has expired.
    MylesBorins Myles Borins
    Copy the full SHA
    f820ba8 View commit details

Commits on Jul 1, 2023

  1. Verified

    This commit was signed with the committer’s verified signature.
    BethGriggs Bethany Griggs
    Copy the full SHA
    c722166 View commit details
  2. Verified

    This commit was signed with the committer’s verified signature.
    BethGriggs Bethany Griggs
    Copy the full SHA
    a224d62 View commit details

Commits on Jul 3, 2023

  1. Add exclusions for B905 (#388)

    Co-authored-by: Alexey Nikitin <alexeynikitin@swatmobility.com>
    NewGlad and Alexey Nikitin authored Jul 3, 2023

    Verified

    This commit was signed with the committer’s verified signature.
    BethGriggs Bethany Griggs
    Copy the full SHA
    f1c391a View commit details

Commits on Jul 10, 2023

  1. Add B034: re.sub/subn/split must pass flags/count/maxsplit as keyword…

    … arguments (#398)
    
    * Add B034: re.sub/subn/split must pass flags/count/maxsplit as keyword arguments.
    
    * remove <3.8 check accidentally added back
    
    * Apply suggestions from code review
    
    Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
    
    * update column in testcase
    
    * improved wording
    
    ---------
    
    Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
    jakkdl and JelleZijlstra authored Jul 10, 2023

    Verified

    This commit was signed with the committer’s verified signature.
    BethGriggs Bethany Griggs
    Copy the full SHA
    dea2e00 View commit details
  2. Copy the full SHA
    b62f181 View commit details
Showing with 257 additions and 97 deletions.
  1. +2 −1 .github/workflows/ci.yml
  2. +1 −1 DEVELOPMENT.md
  3. +15 −2 README.rst
  4. +110 −43 bugbear.py
  5. +1 −0 pyproject.toml
  6. +30 −0 tests/b034.py
  7. +12 −0 tests/b905_py310.py
  8. +2 −2 tests/b907.py
  9. +76 −46 tests/test_bugbear.py
  10. +8 −2 tox.ini
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]

os: [ubuntu-latest]

@@ -20,6 +20,7 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true

- name: Update pip and setuptools
run: |
2 changes: 1 addition & 1 deletion DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -55,7 +55,7 @@ You can also use [tox](https://tox.wiki/en/latest/index.html) to test with multi
```console
/path/to/venv/bin/tox
```
will by default run all tests on python versions 3.8 through 3.11. If you only want to test a specific version you can specify the environment with `-e`
will by default run all tests on python versions 3.8 through 3.12. If you only want to test a specific version you can specify the environment with `-e`

```console
/path/to/venv/bin/tox -e py38
17 changes: 15 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
@@ -188,6 +188,8 @@ second usage. Save the result to a list if the result is needed multiple times.

**B033**: Sets should not contain duplicate items. Duplicate items will be replaced with a single item at runtime.

**B034**: Calls to `re.sub`, `re.subn` or `re.split` should pass `flags` or `count`/`maxsplit` as keyword arguments. It is commonly assumed that `flags` is the third positional parameter, forgetting about `count`/`maxsplit`, since many other `re` module functions are of the form `f(pattern, string, flags)`.

Opinionated warnings
~~~~~~~~~~~~~~~~~~~~

@@ -220,8 +222,11 @@ See `the exception chaining tutorial <https://docs.python.org/3/tutorial/errors.
for details.

**B905**: ``zip()`` without an explicit `strict=` parameter set. ``strict=True`` causes the resulting iterator
to raise a ``ValueError`` if the arguments are exhausted at differing lengths. The ``strict=`` argument
was added in Python 3.10, so don't enable this flag for code that should work on <3.10.
to raise a ``ValueError`` if the arguments are exhausted at differing lengths.

Exclusions are `itertools.count <https://docs.python.org/3/library/itertools.html#itertools.count>`_, `itertools.cycle <https://docs.python.org/3/library/itertools.html#itertools.cycle>`_ and `itertools.repeat <https://docs.python.org/3/library/itertools.html#itertools.repeat>`_ (with times=None) since they are infinite iterators.

The ``strict=`` argument was added in Python 3.10, so don't enable this flag for code that should work on <3.10.
For more information: https://peps.python.org/pep-0618/

**B906**: ``visit_`` function with no further call to a ``visit`` function. This is often an error, and will stop the visitor from recursing into the subnodes of a visited node. Consider adding a call ``self.generic_visit(node)`` at the end of the function.
@@ -329,6 +334,14 @@ MIT
Change Log
----------

23.7.10
~~~~~~~~~~

* Add B034: re.sub/subn/split must pass flags/count/maxsplit as keyword arguments.
* Fix a crash and several test failures on Python 3.12, all relating to the B907
check.
* Declare support for Python 3.12.

23.6.5
~~~~~~

153 changes: 110 additions & 43 deletions bugbear.py
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@
import math
import re
import sys
import warnings
from collections import namedtuple
from contextlib import suppress
from functools import lru_cache, partial
@@ -13,7 +14,7 @@
import attr
import pycodestyle

__version__ = "23.6.5"
__version__ = "23.7.10"

LOG = logging.getLogger("flake8.bugbear")
CONTEXTFUL_NODES = (
@@ -200,7 +201,7 @@ def _is_identifier(arg):
if not isinstance(arg, ast.Constant) or not isinstance(arg.value, str):
return False

return re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", arg.s) is not None
return re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", arg.value) is not None


def _flatten_excepthandler(node):
@@ -401,29 +402,30 @@ def visit_Call(self, node):
with suppress(AttributeError, IndexError):
if (
node.func.id in ("getattr", "hasattr")
and node.args[1].s == "__call__"
and node.args[1].value == "__call__"
):
self.errors.append(B004(node.lineno, node.col_offset))
if (
node.func.id == "getattr"
and len(node.args) == 2
and _is_identifier(node.args[1])
and not iskeyword(node.args[1].s)
and not iskeyword(node.args[1].value)
):
self.errors.append(B009(node.lineno, node.col_offset))
elif (
not any(isinstance(n, ast.Lambda) for n in self.node_stack)
and node.func.id == "setattr"
and len(node.args) == 3
and _is_identifier(node.args[1])
and not iskeyword(node.args[1].s)
and not iskeyword(node.args[1].value)
):
self.errors.append(B010(node.lineno, node.col_offset))

self.check_for_b026(node)

self.check_for_b905(node)
self.check_for_b028(node)
self.check_for_b034(node)
self.check_for_b905(node)
self.generic_visit(node)

def visit_Module(self, node):
@@ -556,11 +558,11 @@ def check_for_b005(self, node):
if call_path in B005.valid_paths:
return # path is exempt

s = node.args[0].s
if len(s) == 1:
value = node.args[0].value
if len(value) == 1:
return # stripping just one character

if len(s) == len(set(s)):
if len(value) == len(set(value)):
return # no characters appear more than once

self.errors.append(B005(node.lineno, node.col_offset))
@@ -1168,12 +1170,46 @@ def check_for_b025(self, node):
for duplicate in duplicates:
self.errors.append(B025(node.lineno, node.col_offset, vars=(duplicate,)))

def check_for_b905(self, node):
if (
isinstance(node.func, ast.Name)
and node.func.id == "zip"
and not any(kw.arg == "strict" for kw in node.keywords)
@staticmethod
def _is_infinite_iterator(node: ast.expr) -> bool:
if not (
isinstance(node, ast.Call)
and isinstance(node.func, ast.Attribute)
and isinstance(node.func.value, ast.Name)
and node.func.value.id == "itertools"
):
return False
if node.func.attr in {"cycle", "count"}:
return True
elif node.func.attr == "repeat":
if len(node.args) == 1 and len(node.keywords) == 0:
# itertools.repeat(iterable)
return True
if (
len(node.args) == 2
and isinstance(node.args[1], ast.Constant)
and node.args[1].value is None
):
# itertools.repeat(iterable, None)
return True
for kw in node.keywords:
# itertools.repeat(iterable, times=None)
if (
kw.arg == "times"
and isinstance(kw.value, ast.Constant)
and kw.value.value is None
):
return True

return False

def check_for_b905(self, node):
if not (isinstance(node.func, ast.Name) and node.func.id == "zip"):
return
for arg in node.args:
if self._is_infinite_iterator(arg):
return
if not any(kw.arg == "strict" for kw in node.keywords):
self.errors.append(B905(node.lineno, node.col_offset))

def check_for_b906(self, node: ast.FunctionDef):
@@ -1182,7 +1218,12 @@ def check_for_b906(self, node: ast.FunctionDef):

# extract what's visited
class_name = node.name[len("visit_") :]
class_type = getattr(ast, class_name, None)

# silence any DeprecationWarnings
# that might come from accessing a deprecated AST node
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=DeprecationWarning)
class_type = getattr(ast, class_name, None)

if (
# not a valid ast subclass
@@ -1239,36 +1280,34 @@ def myunparse(node: ast.AST) -> str: # pragma: no cover
current_mark = None
variable = None
for value in node.values:
# check for quote mark after pre-marked variable
if (
current_mark is not None
and variable is not None
and isinstance(value, ast.Constant)
and isinstance(value.value, str)
and value.value[0] == current_mark
):
self.errors.append(
B907(
variable.lineno,
variable.col_offset,
vars=(myunparse(variable.value),),
)
)
current_mark = variable = None
# don't continue with length>1, so we can detect a new pre-mark
# in the same string as a post-mark, e.g. `"{foo}" "{bar}"`
if len(value.value) == 1:
if isinstance(value, ast.Constant) and isinstance(value.value, str):
if not value.value:
continue

# detect pre-mark
if (
isinstance(value, ast.Constant)
and isinstance(value.value, str)
and value.value[-1] in quote_marks
):
current_mark = value.value[-1]
variable = None
continue
# check for quote mark after pre-marked variable
if (
current_mark is not None
and variable is not None
and value.value[0] == current_mark
):
self.errors.append(
B907(
variable.lineno,
variable.col_offset,
vars=(myunparse(variable.value),),
)
)
current_mark = variable = None
# don't continue with length>1, so we can detect a new pre-mark
# in the same string as a post-mark, e.g. `"{foo}" "{bar}"`
if len(value.value) == 1:
continue

# detect pre-mark
if value.value[-1] in quote_marks:
current_mark = value.value[-1]
variable = None
continue

# detect variable, if there's a pre-mark
if (
@@ -1362,6 +1401,27 @@ def check_for_b033(self, node):
else:
seen.add(elt.value)

def check_for_b034(self, node: ast.Call):
if not isinstance(node.func, ast.Attribute):
return
if not isinstance(node.func.value, ast.Name) or node.func.value.id != "re":
return

def check(num_args, param_name):
if len(node.args) > num_args:
self.errors.append(
B034(
node.args[num_args].lineno,
node.args[num_args].col_offset,
vars=(node.func.attr, param_name),
)
)

if node.func.attr in ("sub", "subn"):
check(3, "count")
elif node.func.attr == "split":
check(2, "maxsplit")


def compose_call_path(node):
if isinstance(node, ast.Attribute):
@@ -1766,6 +1826,13 @@ def visit_Lambda(self, node):
)
)

B034 = Error(
message=(
"B034 {} should pass `{}` and `flags` as keyword arguments to avoid confusion"
" due to unintuitive argument positions."
)
)

# Warnings disabled by default.
B901 = Error(
message=(
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -30,6 +30,7 @@ classifiers = [
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3 :: Only",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Software Development :: Quality Assurance",
30 changes: 30 additions & 0 deletions tests/b034.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import re
from re import sub

# error
re.sub("a", "b", "aaa", re.IGNORECASE)
re.sub("a", "b", "aaa", 5)
re.sub("a", "b", "aaa", 5, re.IGNORECASE)
re.subn("a", "b", "aaa", re.IGNORECASE)
re.subn("a", "b", "aaa", 5)
re.subn("a", "b", "aaa", 5, re.IGNORECASE)
re.split(" ", "a a a a", re.I)
re.split(" ", "a a a a", 2)
re.split(" ", "a a a a", 2, re.I)

# okay
re.sub("a", "b", "aaa")
re.sub("a", "b", "aaa", flags=re.IGNORECASE)
re.sub("a", "b", "aaa", count=5)
re.sub("a", "b", "aaa", count=5, flags=re.IGNORECASE)
re.subn("a", "b", "aaa")
re.subn("a", "b", "aaa", flags=re.IGNORECASE)
re.subn("a", "b", "aaa", count=5)
re.subn("a", "b", "aaa", count=5, flags=re.IGNORECASE)
re.split(" ", "a a a a", flags=re.I)
re.split(" ", "a a a a", maxsplit=2)
re.split(" ", "a a a a", maxsplit=2, flags=re.I)


# not covered
sub("a", "b", "aaa", re.IGNORECASE)
12 changes: 12 additions & 0 deletions tests/b905_py310.py
Original file line number Diff line number Diff line change
@@ -8,3 +8,15 @@
zip(range(3), strict=True)
zip("a", "b", strict=False)
zip("a", "b", "c", strict=True)

# infinite iterators from itertools module should not raise errors
import itertools

zip([1, 2, 3], itertools.cycle("ABCDEF"))
zip([1, 2, 3], itertools.count())
zip([1, 2, 3], itertools.repeat(1))
zip([1, 2, 3], itertools.repeat(1, None))
zip([1, 2, 3], itertools.repeat(1, times=None))

zip([1, 2, 3], itertools.repeat(1, 1))
zip([1, 2, 3], itertools.repeat(1, times=4))
4 changes: 2 additions & 2 deletions tests/b907.py
Original file line number Diff line number Diff line change
@@ -17,12 +17,12 @@ def foo():
f'a "{foo()}" b'

# fmt: off
k = (f'"' # error emitted on this line since all values are assigned the same lineno
k = (f'"' # Error emitted here on <py312 (all values assigned the same lineno)
f'{var}'
f'"'
f'"')

k = (f'"' # error emitted on this line
k = (f'"' # error emitted on this line on <py312
f'{var}'
'"'
f'"')
Loading