-
-
Notifications
You must be signed in to change notification settings - Fork 493
Add a test for missing generics in stubs #2659
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
sobolevn
merged 6 commits into
typeddjango:master
from
UnknownPlatypus:test-missing-generic
May 8, 2025
+137
−7
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
6917e21
Add a test for missing generics in stubs
UnknownPlatypus fd3b17e
Fix missing generic patches
UnknownPlatypus 0839929
Fix monkeypatching of `SetPasswordMixin`
UnknownPlatypus c1e605f
Changes after cr
UnknownPlatypus 1edbd3c
Handle conditional patch based on django version and document workaround
UnknownPlatypus 864eb55
Fix link and check test duration
UnknownPlatypus 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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import ast | ||
import glob | ||
import importlib | ||
import os | ||
from typing import final | ||
from unittest import mock | ||
|
||
import django | ||
|
||
# The root directory of the django-stubs package | ||
STUBS_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "django-stubs")) | ||
|
||
|
||
@final | ||
class GenericInheritanceVisitor(ast.NodeVisitor): | ||
UnknownPlatypus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"""AST visitor to find classes inheriting from `typing.Generic` in stubs.""" | ||
|
||
def __init__(self) -> None: | ||
self.generic_classes: set[str] = set() | ||
|
||
def visit_ClassDef(self, node: ast.ClassDef) -> None: | ||
for base in node.bases: | ||
if ( | ||
isinstance(base, ast.Subscript) | ||
and isinstance(base.value, ast.Name) | ||
and base.value.id == "Generic" | ||
and not any(dec.id == "type_check_only" for dec in node.decorator_list if isinstance(dec, ast.Name)) | ||
): | ||
self.generic_classes.add(node.name) | ||
break | ||
self.generic_visit(node) | ||
|
||
|
||
def test_find_classes_inheriting_from_generic() -> None: | ||
""" | ||
This test ensures that the `ext/django_stubs_ext/patch.py` stays up-to-date with the stubs. | ||
It works as follows: | ||
1. Parse the ast of each .pyi file, and collects classes inheriting from Generic. | ||
2. For each Generic in the stubs, import the associated module and capture every class in the MRO | ||
3. Ensure that at least one class in the mro is patched in `ext/django_stubs_ext/patch.py`. | ||
""" | ||
with mock.patch.dict(os.environ, {"DJANGO_SETTINGS_MODULE": "scripts.django_tests_settings"}): | ||
# We need this to be able to do django import | ||
django.setup() | ||
|
||
# A dict of class_name -> [subclasses names] for each Generic in the stubs. | ||
all_generic_classes: dict[str, list[str]] = {} | ||
|
||
print(f"Searching for classes inheriting from Generic in: {STUBS_ROOT}") | ||
pyi_files = glob.glob("**/*.pyi", root_dir=STUBS_ROOT, recursive=True) | ||
for file_path in pyi_files: | ||
with open(os.path.join(STUBS_ROOT, file_path)) as f: | ||
source = f.read() | ||
|
||
tree = ast.parse(source) | ||
generic_visitor = GenericInheritanceVisitor() | ||
generic_visitor.visit(tree) | ||
|
||
# For each Generic in the stubs, import the associated module and capture every class in the MRO | ||
if generic_visitor.generic_classes: | ||
module_name = _get_module_from_pyi(file_path) | ||
django_module = importlib.import_module(module_name) | ||
all_generic_classes.update( | ||
{ | ||
cls: [subcls.__name__ for subcls in getattr(django_module, cls).mro()[1:-1]] | ||
for cls in generic_visitor.generic_classes | ||
} | ||
) | ||
|
||
print(f"Processed {len(pyi_files)} .pyi files.") | ||
print(f"Found {len(all_generic_classes)} unique classes inheriting from Generic in stubs") | ||
|
||
# Class patched in `ext/django_stubs_ext/patch.py` | ||
import django_stubs_ext | ||
|
||
patched_classes = {mp_generic.cls.__name__ for mp_generic in django_stubs_ext.patch._get_need_generic()} | ||
|
||
# Pretty-print missing patch in `ext/django_stubs_ext/patch.py` | ||
errors = [] | ||
for cls_name, subcls_names in all_generic_classes.items(): | ||
if not any(name in patched_classes for name in [*subcls_names, cls_name]): | ||
bases = f"({', '.join(subcls_names)})" if subcls_names else "" | ||
errors.append(f"{cls_name}{bases} is not patched in `ext/django_stubs_ext/patch.py`") | ||
|
||
assert not errors, "\n".join(errors) | ||
|
||
|
||
def _get_module_from_pyi(pyi_path: str) -> str: | ||
py_module = "django." + pyi_path.replace(".pyi", "").replace("/", ".") | ||
return py_module.removesuffix(".__init__") |
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.