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.7.10
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.9.16
Choose a head ref
  • 8 commits
  • 16 files changed
  • 8 contributors

Commits on Jul 11, 2023

  1. [pre-commit.ci] pre-commit autoupdate (#401)

    updates:
    - [github.com/psf/black: 23.3.0 → 23.7.0](psf/black@23.3.0...23.7.0)
    pre-commit-ci[bot] authored Jul 11, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    45f8fc5 View commit details

Commits on Jul 26, 2023

  1. Copy the full SHA
    bf48278 View commit details

Commits on Sep 2, 2023

  1. Copy the full SHA
    f2aaeb9 View commit details
  2. Merge pull request #406 from jakkdl/fix_python312

    fix name collision for node_stack on python 3.12
    Zac-HD authored Sep 2, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    954e51e View commit details

Commits on Sep 7, 2023

  1. Bump actions/checkout from 3 to 4 (#407)

    Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
    - [Release notes](https://github.com/actions/checkout/releases)
    - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
    - [Commits](actions/checkout@v3...v4)
    
    ---
    updated-dependencies:
    - dependency-name: actions/checkout
      dependency-type: direct:production
      update-type: version-update:semver-major
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Sep 7, 2023
    Copy the full SHA
    8bfbaae View commit details

Commits on Sep 13, 2023

  1. Upgrade black (#410)

    hauntsaninja authored Sep 13, 2023
    Copy the full SHA
    d1a8f2b View commit details

Commits on Sep 16, 2023

  1. add --classmethod-decorators (#405)

    * add --classmethod-decorators
    
    * reproduce attribute handling like pep8-naming
    
    * fix pep8-naming coexistence, add tox environment
    
    * fix CI
    
    * update readme.rst, also check metaclass logic
    
    * broke out logic to helper function
    
    * set->Set due to py38
    
    * ignore C901
    jakkdl authored Sep 16, 2023
    Copy the full SHA
    f75417c View commit details
  2. Copy the full SHA
    f8fe3c4 View commit details
Showing with 384 additions and 297 deletions.
  1. +1 −1 .github/workflows/ci.yml
  2. +5 −5 .github/workflows/pypi_upload.yml
  3. +1 −1 .pre-commit-config.yaml
  4. +13 −2 README.rst
  5. +55 −20 bugbear.py
  6. +11 −22 tests/b006_b008.py
  7. +3 −6 tests/b008_extended.py
  8. +21 −42 tests/b019.py
  9. +7 −14 tests/b027.py
  10. +23 −46 tests/b902.py
  11. +104 −0 tests/b902_extended.py
  12. +23 −46 tests/b902_py38.py
  13. +1 −2 tests/b903.py
  14. +17 −34 tests/b906.py
  15. +87 −54 tests/test_bugbear.py
  16. +12 −2 tox.ini
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ jobs:
os: [ubuntu-latest]

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
10 changes: 5 additions & 5 deletions .github/workflows/pypi_upload.yml
Original file line number Diff line number Diff line change
@@ -10,24 +10,24 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.x

- name: Install latest pip, setuptools, twine + wheel
- name: Install latest pip, build & twine
run: |
python -m pip install --upgrade pip setuptools twine wheel
python -m pip install --upgrade pip build twine
- name: Build sdist + wheel
run: |
python setup.py bdist_wheel
python setup.py sdist
python -m build
- name: Upload to PyPI via Twine
env:
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
run: |
twine check dist/*
twine upload --verbose -u '__token__' dist/*
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ repos:
- id: isort

- repo: https://github.com/psf/black
rev: 23.3.0
rev: 23.9.1
hooks:
- id: black
args:
15 changes: 13 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
@@ -297,20 +297,24 @@ and/or ignored codes.
Configuration
-------------

The plugin currently has one setting:
The plugin currently has the following settings:

``extend-immutable-calls``: Specify a list of additional immutable calls.
This could be useful, when using other libraries that provide more immutable calls,
beside those already handled by ``flake8-bugbear``. Calls to these method will no longer
raise a ``B008`` warning.

``classmethod-decorators``: Specify a list of decorators to additionally mark a method as a ``classmethod`` as used by B902. Default values are ``classmethod, validator, root_validator``, and when an ``@obj.name`` decorator is specified it will match against either ``name`` or ``obj.name``.
This functions similarly to how `pep8-naming <https://github.com/PyCQA/pep8-naming>` handles it, but with different defaults, and they don't support specifying attributes such that a decorator will never match against a specified value ``obj.name`` even if decorated with ``@obj.name``.

For example::

[flake8]
max-line-length = 80
max-complexity = 12
...
extend-immutable-calls = pathlib.Path, Path
classmethod-decorators = myclassmethod, mylibrary.otherclassmethod

Tests / Lints
---------------
@@ -334,8 +338,15 @@ MIT
Change Log
----------

23.9.16
~~~~~~~

* add --classmethod-decorators (#405)
* fix name collision for node_stack on python 3.12 (#406)
* Use pypa/build to build the package (#404)

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
75 changes: 55 additions & 20 deletions bugbear.py
Original file line number Diff line number Diff line change
@@ -10,11 +10,12 @@
from contextlib import suppress
from functools import lru_cache, partial
from keyword import iskeyword
from typing import Dict, List, Set, Union

import attr
import pycodestyle

__version__ = "23.7.10"
__version__ = "23.9.16"

LOG = logging.getLogger("flake8.bugbear")
CONTEXTFUL_NODES = (
@@ -38,6 +39,8 @@
"assertWarnsRegex",
}

B902_default_decorators = {"classmethod", "validator", "root_validator"}

Context = namedtuple("Context", ["node", "stack"])


@@ -62,10 +65,15 @@ def run(self):
else:
b008_extend_immutable_calls = set()

b902_classmethod_decorators: set[str] = B902_default_decorators
if self.options and hasattr(self.options, "classmethod_decorators"):
b902_classmethod_decorators = set(self.options.classmethod_decorators)

visitor = self.visitor(
filename=self.filename,
lines=self.lines,
b008_extend_immutable_calls=b008_extend_immutable_calls,
b902_classmethod_decorators=b902_classmethod_decorators,
)
visitor.visit(self.tree)
for e in itertools.chain(visitor.errors, self.gen_line_based_checks()):
@@ -143,6 +151,19 @@ def add_options(optmanager):
default=[],
help="Skip B008 test for additional immutable calls.",
)
# you cannot register the same option in two different plugins, so we
# only register --classmethod-decorators if pep8-naming is not installed
if "pep8ext_naming" not in sys.modules.keys():
optmanager.add_option(
"--classmethod-decorators",
comma_separated_list=True,
parse_from_config=True,
default=B902_default_decorators,
help=(
"List of method decorators that should be treated as classmethods"
" by B902"
),
)

@lru_cache # noqa: B019
def should_warn(self, code):
@@ -184,10 +205,8 @@ def should_warn(self, code):
return True

LOG.info(
(
"Optional warning %s not present in selected warnings: %r. Not "
"firing it at all."
),
"Optional warning %s not present in selected warnings: %r. Not "
"firing it at all.",
code,
self.options.select,
)
@@ -310,7 +329,7 @@ class BugBearVisitor(ast.NodeVisitor):
filename = attr.ib()
lines = attr.ib()
b008_extend_immutable_calls = attr.ib(default=attr.Factory(set))
node_stack = attr.ib(default=attr.Factory(list))
b902_classmethod_decorators = attr.ib(default=attr.Factory(set))
node_window = attr.ib(default=attr.Factory(list))
errors = attr.ib(default=attr.Factory(list))
futures = attr.ib(default=attr.Factory(set))
@@ -974,37 +993,51 @@ def check_for_b901(self, node):
self.errors.append(B901(return_node.lineno, return_node.col_offset))
break

def check_for_b902(self, node):
# taken from pep8-naming
@classmethod
def find_decorator_name(cls, d):
if isinstance(d, ast.Name):
return d.id
elif isinstance(d, ast.Attribute):
return d.attr
elif isinstance(d, ast.Call):
return cls.find_decorator_name(d.func)

def check_for_b902( # noqa: C901 (too complex)
self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef]
) -> None:
def is_classmethod(decorators: Set[str]) -> bool:
return (
any(name in decorators for name in self.b902_classmethod_decorators)
or node.name in B902.implicit_classmethods
)

if len(self.contexts) < 2 or not isinstance(
self.contexts[-2].node, ast.ClassDef
):
return

cls = self.contexts[-2].node
decorators = NameFinder()
decorators.visit(node.decorator_list)

if "staticmethod" in decorators.names:
decorators: set[str] = {
self.find_decorator_name(d) for d in node.decorator_list
}

if "staticmethod" in decorators:
# TODO: maybe warn if the first argument is surprisingly `self` or
# `cls`?
return

bases = {b.id for b in cls.bases if isinstance(b, ast.Name)}
if "type" in bases:
if (
"classmethod" in decorators.names
or node.name in B902.implicit_classmethods
):
if is_classmethod(decorators):
expected_first_args = B902.metacls
kind = "metaclass class"
else:
expected_first_args = B902.cls
kind = "metaclass instance"
else:
if (
"classmethod" in decorators.names
or node.name in B902.implicit_classmethods
):
if is_classmethod(decorators):
expected_first_args = B902.cls
kind = "class"
else:
@@ -1441,9 +1474,11 @@ class NameFinder(ast.NodeVisitor):
key is name string, value is the node (useful for location purposes).
"""

names = attr.ib(default=attr.Factory(dict))
names: Dict[str, List[ast.Name]] = attr.ib(default=attr.Factory(dict))

def visit_Name(self, node): # noqa: B906 # names don't contain other names
def visit_Name( # noqa: B906 # names don't contain other names
self, node: ast.Name
):
self.names.setdefault(node.id, []).append(node)

def visit(self, node):
33 changes: 11 additions & 22 deletions tests/b006_b008.py
Original file line number Diff line number Diff line change
@@ -12,8 +12,7 @@

# B006
# Allow immutable literals/calls/comprehensions
def this_is_okay(value=(1, 2, 3)):
...
def this_is_okay(value=(1, 2, 3)): ...


async def and_this_also(value=tuple()):
@@ -50,35 +49,28 @@ def operators_ok_unqualified(
pass


def kwonlyargs_immutable(*, value=()):
...
def kwonlyargs_immutable(*, value=()): ...


# Flag mutable literals/comprehensions


def this_is_wrong(value=[1, 2, 3]):
...
def this_is_wrong(value=[1, 2, 3]): ...


def this_is_also_wrong(value={}):
...
def this_is_also_wrong(value={}): ...


def and_this(value=set()):
...
def and_this(value=set()): ...


def this_too(value=collections.OrderedDict()):
...
def this_too(value=collections.OrderedDict()): ...


async def async_this_too(value=collections.defaultdict()):
...
async def async_this_too(value=collections.defaultdict()): ...


def dont_forget_me(value=collections.deque()):
...
def dont_forget_me(value=collections.deque()): ...


# N.B. we're also flagging the function call in the comprehension
@@ -94,8 +86,7 @@ def set_comprehension_also_not_okay(default={i**2 for i in range(3)}):
pass


def kwonlyargs_mutable(*, value=[]):
...
def kwonlyargs_mutable(*, value=[]): ...


# Recommended approach for mutable defaults
@@ -106,16 +97,14 @@ def do_this_instead(value=None):

# B008
# Flag function calls as default args (including if they are part of a sub-expression)
def in_fact_all_calls_are_wrong(value=time.time()):
...
def in_fact_all_calls_are_wrong(value=time.time()): ...


def f(when=dt.datetime.now() + dt.timedelta(days=7)):
pass


def can_even_catch_lambdas(a=(lambda x: x)()):
...
def can_even_catch_lambdas(a=(lambda x: x)()): ...


# Recommended approach for function calls as default args
9 changes: 3 additions & 6 deletions tests/b008_extended.py
Original file line number Diff line number Diff line change
@@ -4,13 +4,10 @@
from fastapi import Query


def this_is_okay_extended(db=fastapi.Depends(get_db)):
...
def this_is_okay_extended(db=fastapi.Depends(get_db)): ...


def this_is_okay_extended_second(data: List[str] = fastapi.Query(None)):
...
def this_is_okay_extended_second(data: List[str] = fastapi.Query(None)): ...


def this_is_not_okay_relative_import_not_listed(data: List[str] = Query(None)):
...
def this_is_not_okay_relative_import_not_listed(data: List[str] = Query(None)): ...
Loading