mirror of
https://github.com/huggingface/diffusers.git
synced 2026-10-23 09:09:01 +03:00
* add return types * add return blocks * style * modular pipelines * add a test to validate * feedback - skip deprecated stuff
181 lines
6.8 KiB
Python
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())
|