-
-
Notifications
You must be signed in to change notification settings - Fork 11.3k
[Perf] Validate @config in pre-commit instead of dynamically #20200
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
96e8187
Validate @config in pre-commit
lionelvillard 98fa907
gemini comments
lionelvillard 57a1146
fix precommit errors
lionelvillard 3c078c8
check config classes are also dataclass.
lionelvillard 7ff12f6
only validate 2 files
lionelvillard 0a32fee
Add link to original docstring code
lionelvillard e3db3bd
add tests
lionelvillard File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # SPDX-FileCopyrightText: Copyright contributors to the vLLM project | ||
|
|
||
| import ast | ||
|
|
||
| import pytest | ||
|
|
||
| from tools.validate_config import validate_ast | ||
|
|
||
| _TestConfig1 = ''' | ||
| @config | ||
| class _TestConfig1: | ||
| pass | ||
| ''' | ||
|
|
||
| _TestConfig2 = ''' | ||
| @config | ||
| @dataclass | ||
| class _TestConfig2: | ||
| a: int | ||
| """docstring""" | ||
| ''' | ||
|
|
||
| _TestConfig3 = ''' | ||
| @config | ||
| @dataclass | ||
| class _TestConfig3: | ||
| a: int = 1 | ||
| ''' | ||
|
|
||
| _TestConfig4 = ''' | ||
| @config | ||
| @dataclass | ||
| class _TestConfig4: | ||
| a: Union[Literal[1], Literal[2]] = 1 | ||
| """docstring""" | ||
| ''' | ||
|
|
||
|
|
||
| @pytest.mark.parametrize(("test_config", "expected_error"), [ | ||
| (_TestConfig1, "must be a dataclass"), | ||
| (_TestConfig2, "must have a default"), | ||
| (_TestConfig3, "must have a docstring"), | ||
| (_TestConfig4, "must use a single Literal"), | ||
| ]) | ||
| def test_config(test_config, expected_error): | ||
| tree = ast.parse(test_config) | ||
| with pytest.raises(Exception, match=expected_error): | ||
| validate_ast(tree) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # SPDX-FileCopyrightText: Copyright contributors to the vLLM project | ||
| """ | ||
| Ensures all fields in a config dataclass have default values | ||
| and that each field has a docstring. | ||
| """ | ||
|
|
||
| import ast | ||
| import inspect | ||
| import sys | ||
|
|
||
|
|
||
| def get_attr_docs(cls_node: ast.ClassDef) -> dict[str, str]: | ||
| """ | ||
| Get any docstrings placed after attribute assignments in a class body. | ||
|
|
||
| Adapted from https://davidism.com/attribute-docstrings/ | ||
| https://davidism.com/mit-license/ | ||
| """ | ||
|
|
||
| def pairwise(iterable): | ||
| """ | ||
| Manually implement https://docs.python.org/3/library/itertools.html#itertools.pairwise | ||
|
|
||
| Can be removed when Python 3.9 support is dropped. | ||
| """ | ||
| iterator = iter(iterable) | ||
| a = next(iterator, None) | ||
|
|
||
| for b in iterator: | ||
| yield a, b | ||
| a = b | ||
|
|
||
| out = {} | ||
|
|
||
| # Consider each pair of nodes. | ||
| for a, b in pairwise(cls_node.body): | ||
| # Must be an assignment then a constant string. | ||
| if (not isinstance(a, (ast.Assign, ast.AnnAssign)) | ||
| or not isinstance(b, ast.Expr) | ||
| or not isinstance(b.value, ast.Constant) | ||
| or not isinstance(b.value.value, str)): | ||
| continue | ||
|
|
||
| doc = inspect.cleandoc(b.value.value) | ||
|
|
||
| # An assignment can have multiple targets (a = b = v), but an | ||
| # annotated assignment only has one target. | ||
| targets = a.targets if isinstance(a, ast.Assign) else [a.target] | ||
|
|
||
| for target in targets: | ||
| # Must be assigning to a plain name. | ||
| if not isinstance(target, ast.Name): | ||
| continue | ||
|
|
||
| out[target.id] = doc | ||
|
|
||
| return out | ||
|
|
||
|
|
||
| class ConfigValidator(ast.NodeVisitor): | ||
|
|
||
| def __init__(self): | ||
| ... | ||
|
|
||
| def visit_ClassDef(self, node): | ||
| # Validate class with both @config and @dataclass decorators | ||
| decorators = [ | ||
| id for d in node.decorator_list if (isinstance(d, ast.Name) and ( | ||
| (id := d.id) == 'config' or id == 'dataclass')) or | ||
| (isinstance(d, ast.Call) and (isinstance(d.func, ast.Name) and | ||
| (id := d.func.id) == 'dataclass')) | ||
| ] | ||
|
|
||
| if set(decorators) == {'config', 'dataclass'}: | ||
| validate_class(node) | ||
lionelvillard marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| elif set(decorators) == {'config'}: | ||
| fail( | ||
| f"Class {node.name} with config decorator must be a dataclass.", | ||
| node) | ||
|
|
||
| self.generic_visit(node) | ||
|
|
||
|
|
||
| def validate_class(class_node: ast.ClassDef): | ||
| attr_docs = get_attr_docs(class_node) | ||
|
|
||
| for stmt in class_node.body: | ||
| # A field is defined as a class variable that has a type annotation. | ||
| if isinstance(stmt, ast.AnnAssign): | ||
| # Skip ClassVar | ||
| # see https://docs.python.org/3/library/dataclasses.html#class-variables | ||
| if isinstance(stmt.annotation, ast.Subscript) and isinstance( | ||
| stmt.annotation.value, | ||
| ast.Name) and stmt.annotation.value.id == "ClassVar": | ||
| continue | ||
|
|
||
| if isinstance(stmt.target, ast.Name): | ||
| field_name = stmt.target.id | ||
| if stmt.value is None: | ||
| fail( | ||
| f"Field '{field_name}' in {class_node.name} must have " | ||
| "a default value.", stmt) | ||
|
|
||
| if field_name not in attr_docs: | ||
| fail( | ||
| f"Field '{field_name}' in {class_node.name} must have " | ||
| "a docstring.", stmt) | ||
|
|
||
| if isinstance(stmt.annotation, ast.Subscript) and \ | ||
| isinstance(stmt.annotation.value, ast.Name) \ | ||
| and stmt.annotation.value.id == "Union" and \ | ||
| isinstance(stmt.annotation.slice, ast.Tuple): | ||
| args = stmt.annotation.slice.elts | ||
| literal_args = [ | ||
| arg for arg in args | ||
| if isinstance(arg, ast.Subscript) and isinstance( | ||
| arg.value, ast.Name) and arg.value.id == "Literal" | ||
| ] | ||
| if len(literal_args) > 1: | ||
| fail( | ||
| f"Field '{field_name}' in {class_node.name} must " | ||
| "use a single " | ||
| "Literal type. Please use 'Literal[Literal1, " | ||
| "Literal2]' instead of 'Union[Literal1, Literal2]'" | ||
| ".", stmt) | ||
|
|
||
|
|
||
| def validate_ast(tree: ast.stmt): | ||
| ConfigValidator().visit(tree) | ||
|
|
||
|
|
||
| def validate_file(file_path: str): | ||
| try: | ||
| print(f"validating {file_path} config dataclasses ", end="") | ||
| with open(file_path, encoding="utf-8") as f: | ||
| source = f.read() | ||
|
|
||
| tree = ast.parse(source, filename=file_path) | ||
| validate_ast(tree) | ||
| except ValueError as e: | ||
| print(e) | ||
| SystemExit(2) | ||
| else: | ||
| print("✅") | ||
|
|
||
|
|
||
| def fail(message: str, node: ast.stmt): | ||
| raise ValueError(f"❌ line({node.lineno}): {message}") | ||
|
|
||
|
|
||
| def main(): | ||
| for filename in sys.argv[1:]: | ||
| validate_file(filename) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this file need a
"Adapted from https://github.com/..."message? Not sure what the intent of linking this URL is.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure. The link is needed because of the MIT license: