Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- id: no-redundant-dataclasses
name: No redundant dataclasses
description: Prevents using raw dataclasses on Strawberry GraphQL types
entry: pre-commit-no-redundant-dataclasses
language: python
types: [python]
15 changes: 15 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Release type: minor

Add a [pre-commit](https://pre-commit.com/) hook to prevent redundant `@dataclass` decorators on Strawberry types.

Since Strawberry types (`@strawberry.type`, `@strawberry.input`, `@strawberry.interface`) already provide dataclass functionality, decorating them with `@dataclass` is redundant. This hook detects and prevents this pattern, helping maintain cleaner code.

To use the hook, add it to your `.pre-commit-config.yaml`:

```yaml
repos:
- repo: https://github.com/strawberry-graphql/strawberry
rev: <version>
hooks:
- id: no-redundant-dataclasses
```
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ documentation = "https://strawberry.rocks/"

[project.scripts]
strawberry = "strawberry.cli:run"
pre-commit-no-redundant-dataclasses = "strawberry.hooks.no_redundant_dataclasses:main"

[project.optional-dependencies]
aiohttp = ["aiohttp>=3.7.4.post0,<4"]
Expand Down Expand Up @@ -342,6 +343,7 @@ ignore = [
"strawberry/extensions/tracing/__init__.py" = ["TCH004"]
"strawberry/fastapi/*" = ["B008"]
"strawberry/annotation.py" = ["RET505"]
"strawberry/hooks/*" = ["T201"]
"tests/*" = [
"ANN001",
"ANN201",
Expand Down
Empty file added strawberry/hooks/__init__.py
Empty file.
77 changes: 77 additions & 0 deletions strawberry/hooks/no_redundant_dataclasses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from __future__ import annotations

import argparse
import ast
from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from collections.abc import Sequence


class Visitor(ast.NodeVisitor):
"""A linter that finds issues and includes the source code."""

def __init__(self) -> None:
self.errors: list[str] = []

def visit_ClassDef(self, node: ast.ClassDef) -> None:
is_strawberry_class = False
is_raw_dataclass = False

for decorator in node.decorator_list:
# Check for strawberry decorators: type, input, interface
if (
isinstance(decorator, ast.Attribute)
and isinstance(decorator.value, ast.Name)
and decorator.value.id == "strawberry"
and decorator.attr in ("type", "input", "interface")
) or (
isinstance(decorator, ast.Call)
and isinstance(decorator.func, ast.Attribute)
and isinstance(decorator.func.value, ast.Name)
and decorator.func.value.id == "strawberry"
and decorator.func.attr in ("type", "input", "interface")
):
is_strawberry_class = True

if (isinstance(decorator, ast.Name) and decorator.id == "dataclass") or (
isinstance(decorator, ast.Attribute)
and isinstance(decorator.value, ast.Name)
and decorator.value.id == "dataclasses"
and decorator.attr == "dataclass"
):
is_raw_dataclass = True

if is_strawberry_class and is_raw_dataclass:
self.errors.append(f":{node.lineno}: {node.name}")

self.generic_visit(node)


def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument("filenames", nargs="*")
args = parser.parse_args(argv)
violations: list[str] = []

for filename in args.filenames:
with Path(filename).open("rb") as f:
tree = ast.parse(f.read(), filename=filename)
visitor = Visitor()
visitor.visit(tree)
violations.extend(f"- {filename}{error}" for error in visitor.errors)

if not violations:
return 0

msg = "\n".join(
(
"Decorating strawberry types with dataclasses.dataclass is redundant.",
"Remove the dataclass decorator from the following classes:",
*violations,
)
)

print(msg)
return 1
Empty file added tests/hooks/__init__.py
Empty file.
46 changes: 46 additions & 0 deletions tests/hooks/test_no_redundant_dataclasses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from textwrap import dedent

from strawberry.hooks.no_redundant_dataclasses import main


def test_check_namespaced_decorator(tmp_path):
code = """
import strawberry
import dataclasses

@dataclasses.dataclass
@strawberry.type
class Foo: ...
"""
file = tmp_path / "foo.py"
file.write_text(dedent(code), encoding="utf-8")
exit_code = main([str(file)])
assert exit_code == 1


def test_check_imported_decorator(tmp_path):
code = """
import strawberry
from dataclasses import dataclass

@dataclass
@strawberry.type
class Foo: ...
"""
file = tmp_path / "foo.py"
file.write_text(dedent(code), encoding="utf-8")
exit_code = main([str(file)])
assert exit_code == 1


def test_check_passing_file(tmp_path):
code = """
import strawberry

@strawberry.type
class Foo: ...
"""
file = tmp_path / "foo.py"
file.write_text(dedent(code), encoding="utf-8")
exit_code = main([str(file)])
assert exit_code == 0
Loading