mirror of
https://github.com/joshuadavidthomas/django-language-server.git
synced 2025-09-13 13:56:25 +00:00
Replace PyO3 with IPC approach for Python/project information (#214)
Some checks are pending
lint / pre-commit (push) Waiting to run
lint / rustfmt (push) Waiting to run
lint / clippy (push) Waiting to run
lint / cargo-check (push) Waiting to run
release / release (push) Blocked by required conditions
test / generate-matrix (push) Waiting to run
release / build (push) Waiting to run
release / test (push) Waiting to run
test / Python , Django () (push) Blocked by required conditions
test / tests (push) Blocked by required conditions
zizmor 🌈 / zizmor latest via PyPI (push) Waiting to run
Some checks are pending
lint / pre-commit (push) Waiting to run
lint / rustfmt (push) Waiting to run
lint / clippy (push) Waiting to run
lint / cargo-check (push) Waiting to run
release / release (push) Blocked by required conditions
test / generate-matrix (push) Waiting to run
release / build (push) Waiting to run
release / test (push) Waiting to run
test / Python , Django () (push) Blocked by required conditions
test / tests (push) Blocked by required conditions
zizmor 🌈 / zizmor latest via PyPI (push) Waiting to run
This commit is contained in:
parent
31b0308a40
commit
d99c96d6b6
39 changed files with 903 additions and 696 deletions
12
python/Justfile
Normal file
12
python/Justfile
Normal file
|
@ -0,0 +1,12 @@
|
|||
set unstable := true
|
||||
|
||||
[private]
|
||||
default:
|
||||
@just --list
|
||||
|
||||
[private]
|
||||
fmt:
|
||||
@just --fmt
|
||||
|
||||
build:
|
||||
uv run build.py
|
24
python/build.py
Normal file
24
python/build.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import zipapp
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main():
|
||||
source_dir = Path(__file__).parent / "src" / "djls_inspector"
|
||||
output_file = Path(__file__).parent / "dist" / "djls_inspector.pyz"
|
||||
output_file.parent.mkdir(exist_ok=True)
|
||||
|
||||
zipapp.create_archive(
|
||||
source_dir,
|
||||
target=output_file,
|
||||
interpreter=None, # No shebang - will be invoked explicitly
|
||||
compressed=True,
|
||||
)
|
||||
|
||||
print(f"Successfully created {output_file}")
|
||||
print(f"Size: {output_file.stat().st_size} bytes")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
12
python/pyproject.toml
Normal file
12
python/pyproject.toml
Normal file
|
@ -0,0 +1,12 @@
|
|||
[build-system]
|
||||
requires = ["uv_build>=0.7.21,<0.8"]
|
||||
build-backend = "uv_build"
|
||||
|
||||
[project]
|
||||
authors = [
|
||||
{ name = "Josh Thomas", email = "josh@joshthomas.dev" }
|
||||
]
|
||||
dependencies = []
|
||||
name = "djls-inspector"
|
||||
requires-python = ">=3.9"
|
||||
version = "0.1.0"
|
0
python/src/djls_inspector/__init__.py
Normal file
0
python/src/djls_inspector/__init__.py
Normal file
44
python/src/djls_inspector/__main__.py
Normal file
44
python/src/djls_inspector/__main__.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
|
||||
# When running from zipapp, we need to import without the package prefix
|
||||
# since the zipapp root is already the package
|
||||
try:
|
||||
# Try direct import (when running as zipapp)
|
||||
from inspector import DjlsResponse
|
||||
from inspector import handle_request
|
||||
except ImportError:
|
||||
# Fall back to package import (when running with python -m)
|
||||
from djls_inspector.inspector import DjlsResponse
|
||||
from djls_inspector.inspector import handle_request
|
||||
|
||||
|
||||
def main() -> None:
|
||||
try:
|
||||
for line in sys.stdin:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
try:
|
||||
request = json.loads(line)
|
||||
response = handle_request(request)
|
||||
except json.JSONDecodeError as e:
|
||||
response = DjlsResponse(ok=False, error=f"Invalid JSON: {e}")
|
||||
except Exception as e:
|
||||
response = DjlsResponse(ok=False, error=f"Unexpected error: {e}")
|
||||
|
||||
response_json = json.dumps(response.to_dict())
|
||||
print(response_json, flush=True)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"Fatal error in inspector: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
77
python/src/djls_inspector/inspector.py
Normal file
77
python/src/djls_inspector/inspector.py
Normal file
|
@ -0,0 +1,77 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
# Try direct import (when running as zipapp)
|
||||
from queries import Query
|
||||
from queries import QueryData
|
||||
from queries import get_installed_templatetags
|
||||
from queries import get_python_environment_info
|
||||
from queries import initialize_django
|
||||
except ImportError:
|
||||
# Fall back to relative import (when running with python -m)
|
||||
from .queries import Query
|
||||
from .queries import QueryData
|
||||
from .queries import get_installed_templatetags
|
||||
from .queries import get_python_environment_info
|
||||
from .queries import initialize_django
|
||||
|
||||
|
||||
@dataclass
|
||||
class DjlsRequest:
|
||||
query: Query
|
||||
args: list[str] | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class DjlsResponse:
|
||||
ok: bool
|
||||
data: QueryData | None = None
|
||||
error: str | None = None
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
d = asdict(self)
|
||||
# Convert Path objects to strings for JSON serialization
|
||||
if self.data:
|
||||
if hasattr(self.data, "__dataclass_fields__"):
|
||||
data_dict = asdict(self.data)
|
||||
# Convert Path objects to strings
|
||||
for key, value in data_dict.items():
|
||||
if key in ["sys_base_prefix", "sys_executable", "sys_prefix"]:
|
||||
if value:
|
||||
data_dict[key] = str(value)
|
||||
elif key == "sys_path":
|
||||
data_dict[key] = [str(p) for p in value]
|
||||
d["data"] = data_dict
|
||||
return d
|
||||
|
||||
|
||||
def handle_request(request: dict[str, Any]) -> DjlsResponse:
|
||||
try:
|
||||
query_str = request.get("query")
|
||||
if not query_str:
|
||||
return DjlsResponse(ok=False, error="Missing 'query' field in request")
|
||||
|
||||
try:
|
||||
query = Query(query_str)
|
||||
except ValueError:
|
||||
return DjlsResponse(ok=False, error=f"Unknown query type: {query_str}")
|
||||
|
||||
args = request.get("args")
|
||||
|
||||
if query == Query.PYTHON_ENV:
|
||||
return DjlsResponse(ok=True, data=get_python_environment_info())
|
||||
|
||||
elif query == Query.TEMPLATETAGS:
|
||||
return DjlsResponse(ok=True, data=get_installed_templatetags())
|
||||
|
||||
elif query == Query.DJANGO_INIT:
|
||||
return DjlsResponse(ok=True, data=initialize_django())
|
||||
|
||||
return DjlsResponse(ok=False, error=f"Unhandled query type: {query}")
|
||||
|
||||
except Exception as e:
|
||||
return DjlsResponse(ok=False, error=str(e))
|
174
python/src/djls_inspector/queries.py
Normal file
174
python/src/djls_inspector/queries.py
Normal file
|
@ -0,0 +1,174 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
|
||||
class Query(str, Enum):
|
||||
PYTHON_ENV = "python_env"
|
||||
TEMPLATETAGS = "templatetags"
|
||||
DJANGO_INIT = "django_init"
|
||||
|
||||
|
||||
@dataclass
|
||||
class PythonEnvironmentQueryData:
|
||||
sys_base_prefix: Path
|
||||
sys_executable: Path
|
||||
sys_path: list[Path]
|
||||
sys_platform: str
|
||||
sys_prefix: Path
|
||||
sys_version_info: tuple[
|
||||
int, int, int, Literal["alpha", "beta", "candidate", "final"], int
|
||||
]
|
||||
|
||||
|
||||
def get_python_environment_info():
|
||||
return PythonEnvironmentQueryData(
|
||||
sys_base_prefix=Path(sys.base_prefix),
|
||||
sys_executable=Path(sys.executable),
|
||||
sys_path=[Path(p) for p in sys.path],
|
||||
sys_platform=sys.platform,
|
||||
sys_prefix=Path(sys.prefix),
|
||||
sys_version_info=(
|
||||
sys.version_info.major,
|
||||
sys.version_info.minor,
|
||||
sys.version_info.micro,
|
||||
sys.version_info.releaselevel,
|
||||
sys.version_info.serial,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TemplateTagQueryData:
|
||||
templatetags: list[TemplateTag]
|
||||
|
||||
|
||||
@dataclass
|
||||
class TemplateTag:
|
||||
name: str
|
||||
module: str
|
||||
doc: str | None
|
||||
|
||||
|
||||
def get_installed_templatetags() -> TemplateTagQueryData:
|
||||
import django
|
||||
from django.apps import apps
|
||||
from django.template.engine import Engine
|
||||
from django.template.library import import_library
|
||||
|
||||
# Ensure Django is set up
|
||||
if not apps.ready:
|
||||
django.setup()
|
||||
|
||||
templatetags: list[TemplateTag] = []
|
||||
|
||||
engine = Engine.get_default()
|
||||
|
||||
for library in engine.template_builtins:
|
||||
if library.tags:
|
||||
for tag_name, tag_func in library.tags.items():
|
||||
templatetags.append(
|
||||
TemplateTag(
|
||||
name=tag_name, module=tag_func.__module__, doc=tag_func.__doc__
|
||||
)
|
||||
)
|
||||
|
||||
for lib_module in engine.libraries.values():
|
||||
library = import_library(lib_module)
|
||||
if library and library.tags:
|
||||
for tag_name, tag_func in library.tags.items():
|
||||
templatetags.append(
|
||||
TemplateTag(
|
||||
name=tag_name, module=tag_func.__module__, doc=tag_func.__doc__
|
||||
)
|
||||
)
|
||||
|
||||
return TemplateTagQueryData(templatetags=templatetags)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DjangoInitQueryData:
|
||||
success: bool
|
||||
message: str | None = None
|
||||
|
||||
|
||||
def initialize_django() -> DjangoInitQueryData:
|
||||
import os
|
||||
import django
|
||||
from django.apps import apps
|
||||
|
||||
try:
|
||||
# Check if Django settings are configured
|
||||
if not os.environ.get("DJANGO_SETTINGS_MODULE"):
|
||||
# Try to find and set settings module
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Look for manage.py to determine project structure
|
||||
current_path = Path.cwd()
|
||||
manage_py = None
|
||||
|
||||
# Search up to 3 levels for manage.py
|
||||
for _ in range(3):
|
||||
if (current_path / "manage.py").exists():
|
||||
manage_py = current_path / "manage.py"
|
||||
break
|
||||
if current_path.parent == current_path:
|
||||
break
|
||||
current_path = current_path.parent
|
||||
|
||||
if not manage_py:
|
||||
return DjangoInitQueryData(
|
||||
success=False,
|
||||
message="Could not find manage.py or DJANGO_SETTINGS_MODULE not set",
|
||||
)
|
||||
|
||||
# Add project directory to sys.path
|
||||
project_dir = manage_py.parent
|
||||
if str(project_dir) not in sys.path:
|
||||
sys.path.insert(0, str(project_dir))
|
||||
|
||||
# Try to find settings module - look for common patterns
|
||||
# First check if there's a directory with the same name as the parent
|
||||
project_name = project_dir.name
|
||||
settings_candidates = [
|
||||
f"{project_name}.settings", # e.g., myproject.settings
|
||||
"settings", # Just settings.py in root
|
||||
"config.settings", # Common pattern
|
||||
"project.settings", # Another common pattern
|
||||
]
|
||||
|
||||
# Also check for any directory containing settings.py
|
||||
for item in project_dir.iterdir():
|
||||
if item.is_dir() and (item / "settings.py").exists():
|
||||
candidate = f"{item.name}.settings"
|
||||
if candidate not in settings_candidates:
|
||||
settings_candidates.insert(
|
||||
0, candidate
|
||||
) # Prioritize found settings
|
||||
|
||||
for settings_candidate in settings_candidates:
|
||||
try:
|
||||
__import__(settings_candidate)
|
||||
os.environ["DJANGO_SETTINGS_MODULE"] = settings_candidate
|
||||
break
|
||||
except ImportError:
|
||||
continue
|
||||
|
||||
# Set up Django
|
||||
if not apps.ready:
|
||||
django.setup()
|
||||
|
||||
return DjangoInitQueryData(
|
||||
success=True, message="Django initialized successfully"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return DjangoInitQueryData(success=False, message=str(e))
|
||||
|
||||
|
||||
QueryData = PythonEnvironmentQueryData | TemplateTagQueryData | DjangoInitQueryData
|
8
python/uv.lock
generated
Normal file
8
python/uv.lock
generated
Normal file
|
@ -0,0 +1,8 @@
|
|||
version = 1
|
||||
revision = 2
|
||||
requires-python = ">=3.9"
|
||||
|
||||
[[package]]
|
||||
name = "djls-inspector"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
Loading…
Add table
Add a link
Reference in a new issue