-
-
Notifications
You must be signed in to change notification settings - Fork 646
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
The beginnings of a Django plugin. #18173
Merged
Merged
Changes from 1 commit
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
c82701e
The beginnings of a Django plugin.
benjyw 5408532
Detect first-party app labels
benjyw adad561
Lint and typo
benjyw 96731a1
Merge branch 'main' into django_plugin3
benjyw 207ec8c
Merge branch 'main' into django_plugin3
benjyw b0f6b3c
Test default label
benjyw 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 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 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 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,7 @@ | ||
# Copyright 2023 Pants project contributors (see CONTRIBUTORS.md). | ||
# Licensed under the Apache License, Version 2.0 (see LICENSE). | ||
|
||
|
||
python_sources(dependencies=["src/python/pants/backend/python/framework/django/scripts"]) | ||
|
||
python_tests(name="tests") |
Empty file.
This file contains 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 @@ | ||
# Copyright 2023 Pants project contributors (see CONTRIBUTORS.md). | ||
# Licensed under the Apache License, Version 2.0 (see LICENSE). | ||
|
||
from dataclasses import dataclass | ||
|
||
from pants.backend.python.dependency_inference.parse_python_dependencies import ( | ||
PythonDependencyVisitor, | ||
PythonDependencyVisitorRequest, | ||
_get_scripts_digest, | ||
) | ||
from pants.backend.python.target_types import PythonSourceField | ||
from pants.engine.rules import collect_rules, rule | ||
from pants.engine.target import FieldSet | ||
from pants.engine.unions import UnionRule | ||
from pants.util.frozendict import FrozenDict | ||
|
||
|
||
@dataclass(frozen=True) | ||
class DjangoDependencyVisitorRequest(PythonDependencyVisitorRequest): | ||
pass | ||
|
||
|
||
_scripts_package = "pants.backend.python.framework.django.scripts" | ||
|
||
|
||
@rule | ||
async def general_parser_script( | ||
_: DjangoDependencyVisitorRequest, | ||
) -> PythonDependencyVisitor: | ||
digest = await _get_scripts_digest(_scripts_package, ["django_dependency_visitor.py"]) | ||
return PythonDependencyVisitor( | ||
digest=digest, | ||
classname=f"{_scripts_package}.django_dependency_visitor.DjangoDependencyVisitor", | ||
env=FrozenDict({}), | ||
) | ||
|
||
|
||
@dataclass(frozen=True) | ||
class DjangoMigrationDependenciesInferenceFieldSet(FieldSet): | ||
required_fields = (PythonSourceField,) | ||
|
||
source: PythonSourceField | ||
|
||
|
||
def rules(): | ||
return [ | ||
UnionRule(PythonDependencyVisitorRequest, DjangoDependencyVisitorRequest), | ||
*collect_rules(), | ||
] |
125 changes: 125 additions & 0 deletions
125
src/python/pants/backend/python/framework/django/rules_test.py
This file contains 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,125 @@ | ||
# Copyright 2023 Pants project contributors (see CONTRIBUTORS.md). | ||
# Licensed under the Apache License, Version 2.0 (see LICENSE). | ||
|
||
from __future__ import annotations | ||
|
||
from textwrap import dedent | ||
|
||
import pytest | ||
|
||
from pants.backend.python.dependency_inference import parse_python_dependencies | ||
from pants.backend.python.dependency_inference.parse_python_dependencies import ( | ||
ParsedPythonDependencies, | ||
) | ||
from pants.backend.python.dependency_inference.parse_python_dependencies import ( | ||
ParsedPythonImportInfo as ImpInfo, | ||
) | ||
from pants.backend.python.dependency_inference.parse_python_dependencies import ( | ||
ParsePythonDependenciesRequest, | ||
) | ||
from pants.backend.python.framework.django import rules as django_rules | ||
from pants.backend.python.target_types import PythonSourceField, PythonSourceTarget | ||
from pants.backend.python.util_rules import pex | ||
from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints | ||
from pants.core.util_rules import stripped_source_files | ||
from pants.engine.addresses import Address | ||
from pants.testutil.python_interpreter_selection import ( | ||
skip_unless_python27_present, | ||
skip_unless_python37_present, | ||
skip_unless_python38_present, | ||
skip_unless_python39_present, | ||
) | ||
from pants.testutil.rule_runner import QueryRule, RuleRunner | ||
|
||
|
||
@pytest.fixture | ||
def rule_runner() -> RuleRunner: | ||
return RuleRunner( | ||
rules=[ | ||
*parse_python_dependencies.rules(), | ||
*stripped_source_files.rules(), | ||
*pex.rules(), | ||
*django_rules.rules(), | ||
QueryRule(ParsedPythonDependencies, [ParsePythonDependenciesRequest]), | ||
QueryRule(ParsedPythonDependencies, [ParsePythonDependenciesRequest]), | ||
], | ||
target_types=[PythonSourceTarget], | ||
) | ||
|
||
|
||
def assert_deps_parsed( | ||
rule_runner: RuleRunner, | ||
content: str, | ||
*, | ||
expected_imports: dict[str, ImpInfo] | None = None, | ||
expected_assets: list[str] | None = None, | ||
filename: str = "app0/migrations/0001_initial.py", | ||
constraints: str = ">=3.6", | ||
) -> None: | ||
expected_imports = expected_imports or {} | ||
expected_assets = expected_assets or [] | ||
rule_runner.set_options( | ||
[ | ||
"--no-python-infer-string-imports", | ||
"--no-python-infer-assets", | ||
], | ||
env_inherit={"PATH", "PYENV_ROOT", "HOME"}, | ||
) | ||
rule_runner.write_files( | ||
{ | ||
"BUILD": f"python_source(name='t', source={repr(filename)})", | ||
filename: content, | ||
} | ||
) | ||
tgt = rule_runner.get_target(Address("", target_name="t")) | ||
result = rule_runner.request( | ||
ParsedPythonDependencies, | ||
[ | ||
ParsePythonDependenciesRequest( | ||
tgt[PythonSourceField], | ||
InterpreterConstraints([constraints]), | ||
) | ||
], | ||
) | ||
assert dict(result.imports) == expected_imports | ||
assert list(result.assets) == sorted(expected_assets) | ||
|
||
|
||
def do_test_migration_dependencies(rule_runner: RuleRunner, constraints: str) -> None: | ||
content = dedent( | ||
"""\ | ||
class Migration(migrations.Migration): | ||
dependencies = [("app1", "0012_some_migration"), ("app2", "0042_some_other_migration")] | ||
|
||
operations = [] | ||
""" | ||
) | ||
assert_deps_parsed( | ||
rule_runner, | ||
content, | ||
expected_imports={ | ||
"app1.migrations.0012_some_migration": ImpInfo(lineno=2, weak=True), | ||
"app2.migrations.0042_some_other_migration": ImpInfo(lineno=2, weak=True), | ||
}, | ||
constraints=constraints, | ||
) | ||
|
||
|
||
@skip_unless_python27_present | ||
def test_works_with_python2(rule_runner: RuleRunner) -> None: | ||
do_test_migration_dependencies(rule_runner, constraints="CPython==2.7.*") | ||
|
||
|
||
@skip_unless_python37_present | ||
def test_works_with_python37(rule_runner: RuleRunner) -> None: | ||
do_test_migration_dependencies(rule_runner, constraints="CPython==3.7.*") | ||
|
||
|
||
@skip_unless_python38_present | ||
def test_works_with_python38(rule_runner: RuleRunner) -> None: | ||
do_test_migration_dependencies(rule_runner, constraints="CPython==3.8.*") | ||
|
||
|
||
@skip_unless_python39_present | ||
def test_works_with_python39(rule_runner: RuleRunner) -> None: | ||
do_test_migration_dependencies(rule_runner, constraints="CPython==3.9.*") |
4 changes: 4 additions & 0 deletions
4
src/python/pants/backend/python/framework/django/scripts/BUILD
This file contains 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,4 @@ | ||
# Copyright 2023 Pants project contributors (see CONTRIBUTORS.md). | ||
# Licensed under the Apache License, Version 2.0 (see LICENSE). | ||
|
||
python_sources() |
Empty file.
48 changes: 48 additions & 0 deletions
48
src/python/pants/backend/python/framework/django/scripts/django_dependency_visitor.py
This file contains 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,48 @@ | ||
# Copyright 2023 Pants project contributors (see CONTRIBUTORS.md). | ||
# Licensed under the Apache License, Version 2.0 (see LICENSE). | ||
# -*- coding: utf-8 -*- | ||
|
||
# NB: This must be compatible with Python 2.7 and 3.5+. | ||
# NB: An easy way to debug this is to invoke it directly on a file. | ||
# E.g. | ||
# $ PYTHONPATH=src/python VISITOR_CLASSNAMES=pants.backend.python.framework.django.scripts\ | ||
# .django_dependency_visitor.DjangoDependencyVisitor \ | ||
# python src/python/pants/backend/python/dependency_inference/scripts/main.py FILE | ||
|
||
from __future__ import print_function, unicode_literals | ||
|
||
import ast | ||
|
||
from pants.backend.python.dependency_inference.scripts.dependency_visitor_base import ( | ||
DependencyVisitorBase, | ||
) | ||
|
||
|
||
class DjangoDependencyVisitor(DependencyVisitorBase): | ||
def __init__(self, *args, **kwargs): | ||
super(DjangoDependencyVisitor, self).__init__(*args, **kwargs) | ||
self._in_migration = False | ||
|
||
def visit_ClassDef(self, node): | ||
# Detect `class Migration(migrations.Migration):` | ||
if ( | ||
node.name == "Migration" | ||
and len(node.bases) > 0 | ||
and node.bases[0].value.id == "migrations" | ||
and node.bases[0].attr == "Migration" | ||
): | ||
self._in_migration = True | ||
self.generic_visit(node) | ||
self._in_migration = False | ||
|
||
def visit_Assign(self, node): | ||
if self._in_migration and len(node.targets) > 0 and node.targets[0].id == "dependencies": | ||
if isinstance(node.value, (ast.Tuple, ast.List)): | ||
for elt in node.value.elts: | ||
if isinstance(elt, (ast.Tuple, ast.List)) and len(elt.elts) == 2: | ||
app = self.maybe_str(elt.elts[0]) | ||
migration = self.maybe_str(elt.elts[1]) | ||
if app is not None and migration is not None: | ||
module = "{}.migrations.{}".format(app, migration) | ||
self.add_weak_import(module, node.lineno) | ||
self.generic_visit(node) |
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.
This assumes that all apps are at a source root, which may not be the case.
Example (from a real project):
Roots are:
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.
I applied this diff to your PR in order to try it live:
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.
Hmm, there is a problem, but it's not with source roots. I was absent-mindedly assuming that the migration deps use the dotted module path (e.g.,
django.contrib.auth
), which is from the source root, so the source root doesn't matter.But actually it's the Django app "label", which is typically just the last component of the module path, e.g.,
auth
but I think can be overridden in theAppConfig
class (I will experiment).So it's not the source roots that are the issue, but that we need a map from app label to app module.
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.
Don't forget that this extraction phase returns dependency modules, not files, so it does not know or need to know about source roots.
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.
Ah, true.
Well, I guess it doesn't need to know about source roots or file paths, but the dependency module won't work if it isn't complete from a source root perspective.
With a app label to module mapping this will work just fine, so I think that is the correct way forward :)
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.
That's true in general, but the dep inference machinery takes care of this already. The responsibility of this extraction phase is just to say "this file depends on this module". Other code takes care of figuring out who provides the module, which is where source roots come in.
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.
Yea, that's what I mean. If the extraction phase says "we have a dependency on module
foo.bar
" but we only havesome.foo.bar
(due to how the source roots are set up) then the extraction phase needs to providesome.foo.bar
for it to work, right? (as there's no one providing justfoo.bar
)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.
The extraction has to get the module paths right, of course, but this should not require it to know about source roots, since
import
statements (or any other runtime loading/dependency mechanisms) don't know about them.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.
Agreed. That's exactly what I was trying to say with
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.
why is human language so hard to use for communication? :P