1
0
mirror of https://github.com/huggingface/diffusers.git synced 2026-10-24 20:59:27 +03:00
Files
diffusers/utils/check_return_annotations.py
Steven Liu c60830ee36 [fix] Add return types (#14874)
* add return types

* add return blocks

* style

* modular pipelines

* add a test to validate

* feedback - skip deprecated stuff
2026-09-30 08:59:23 -07:00

181 lines
6.8 KiB
Python

# coding=utf-8
# Copyright 2026 The HuggingFace Inc. team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Check that these methods have a return type annotation:
* `forward()` on every class in `src/diffusers/models`
* `__call__()` on every pipeline in `src/diffusers/pipelines`
* `__call__()` on every modular pipeline block in `src/diffusers/modular_pipelines`
A class counts as a pipeline if it inherits from `DiffusionPipeline`, either directly or through another class. A class
counts as a modular pipeline block if it inherits from `ModularPipelineBlocks` in the same way.
Deprecated code is skipped:
* anything in a folder named `deprecated`, such as `pipelines/deprecated`
* pipelines that inherit from `DeprecatedPipelineMixin`
* classes and methods whose `# Copied from` comment points to deprecated code, because they can't change unless the
deprecated code changes too
A method is only checked on the class where it's written, not on classes that inherit it. Any annotation passes,
including `-> None`.
Run from the repository root:
python utils/check_return_annotations.py
"""
from __future__ import annotations
import ast
import sys
from collections import defaultdict
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SRC_DIR = REPO_ROOT / "src" / "diffusers"
MODELS_DIR = SRC_DIR / "models"
PIPELINES_DIR = SRC_DIR / "pipelines"
MODULAR_DIR = SRC_DIR / "modular_pipelines"
PIPELINE_BASE = "DiffusionPipeline"
DEPRECATED_PIPELINE_BASE = "DeprecatedPipelineMixin"
BLOCKS_BASE = "ModularPipelineBlocks"
def _base_names(class_def: ast.ClassDef) -> list[str]:
"""Return the names of the classes this class inherits from. For a name like `nn.Module`, keep only `Module`."""
names = []
for base in class_def.bases:
if isinstance(base, ast.Name):
names.append(base.id)
elif isinstance(base, ast.Attribute):
names.append(base.attr)
return names
def _find_method(class_def: ast.ClassDef, method_name: str) -> ast.FunctionDef | ast.AsyncFunctionDef | None:
for node in class_def.body:
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == method_name:
return node
return None
def _parse_classes(paths: list[Path]) -> list[tuple[Path, ast.ClassDef, list[str]]]:
"""Return every class in `paths`, along with its file and the lines of that file."""
classes = []
for path in paths:
try:
source = path.read_text(encoding="utf-8")
tree = ast.parse(source)
except (SyntaxError, UnicodeDecodeError):
continue
lines = source.splitlines()
classes.extend((path, node, lines) for node in ast.walk(tree) if isinstance(node, ast.ClassDef))
return classes
def _is_deprecated_path(path: Path) -> bool:
"""Return whether the file is inside a folder named `deprecated`."""
return "deprecated" in path.relative_to(SRC_DIR).parts[:-1]
def _copied_from_deprecated(lines: list[str], node: ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
"""Return whether the `# Copied from` comment right above a class or method points to deprecated code."""
first_line = min([decorator.lineno for decorator in node.decorator_list] + [node.lineno])
if first_line < 2:
return False
comment = lines[first_line - 2].strip()
return comment.startswith("# Copied from") and ".deprecated." in comment
def _subclass_checker(classes: list[tuple[Path, ast.ClassDef, list[str]]]):
"""
Return a function `is_subclass(name, base)` that tells whether the class `name` inherits from the class `base`,
either directly or through other classes.
Classes are matched by name only, so two classes with the same name in different files are treated as one class.
"""
bases_by_name: dict[str, set[str]] = defaultdict(set)
for _, class_def, _ in classes:
bases_by_name[class_def.name].update(_base_names(class_def))
cache: dict[tuple[str, str], bool] = {}
def is_subclass(name: str, base: str, _seen: frozenset[str] = frozenset()) -> bool:
if name == base:
return True
if (name, base) in cache:
return cache[(name, base)]
if name in _seen: # stop if this class was already visited, so a loop in the class names can't run forever
return False
result = any(is_subclass(parent, base, _seen | {name}) for parent in bases_by_name.get(name, ()))
cache[(name, base)] = result
return result
return is_subclass
def _is_under(path: Path, directory: Path) -> bool:
return directory in path.parents
def main() -> int:
classes = _parse_classes(sorted(SRC_DIR.rglob("*.py")))
is_subclass = _subclass_checker(classes)
errors = []
for path, class_def, lines in classes:
if _is_deprecated_path(path):
continue
if _is_under(path, MODELS_DIR):
method_name = "forward"
elif _is_under(path, PIPELINES_DIR):
if not is_subclass(class_def.name, PIPELINE_BASE) or is_subclass(class_def.name, DEPRECATED_PIPELINE_BASE):
continue
method_name = "__call__"
elif _is_under(path, MODULAR_DIR):
if not is_subclass(class_def.name, BLOCKS_BASE):
continue
method_name = "__call__"
else:
continue
method = _find_method(class_def, method_name)
if method is None or method.returns is not None:
continue
if _copied_from_deprecated(lines, class_def) or _copied_from_deprecated(lines, method):
continue
rel = path.relative_to(REPO_ROOT).as_posix()
errors.append(f"{rel}:{method.lineno}: {class_def.name}.{method_name} has no return type annotation")
if errors:
print("\n".join(errors))
sys.stdout.flush() # print the list before the summary, even when both end up in the same log
if len(errors) == 1:
summary = "Found 1 method without a return type annotation. Add one to the method above."
else:
summary = f"Found {len(errors)} methods without a return type annotation. Add one to each method above."
print(f"\n{summary}", file=sys.stderr)
return 1
print("All forward/__call__ methods have return type annotations.")
return 0
if __name__ == "__main__":
sys.exit(main())