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
|
@ -3,22 +3,20 @@ name = "djls-project"
|
|||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
extension-module = []
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
djls-workspace = { workspace = true }
|
||||
|
||||
pyo3 = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
which = { workspace = true}
|
||||
|
||||
[build-dependencies]
|
||||
djls-dev = { workspace = true }
|
||||
which = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
@ -1,3 +1,56 @@
|
|||
fn main() {
|
||||
djls_dev::setup_python_linking();
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
fn get_python_executable_path() -> PathBuf {
|
||||
let python = which::which("python3")
|
||||
.or_else(|_| which::which("python"))
|
||||
.expect("Python not found. Please install Python to build this project.");
|
||||
println!(
|
||||
"cargo:warning=Building Python inspector with: {}",
|
||||
python.display()
|
||||
);
|
||||
python
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=../../python/src/djls_inspector");
|
||||
println!("cargo:rerun-if-changed=../../python/build.py");
|
||||
|
||||
let workspace_dir = env::var("CARGO_WORKSPACE_DIR").expect("CARGO_WORKSPACE_DIR not set");
|
||||
let workspace_path = PathBuf::from(workspace_dir);
|
||||
let python_dir = workspace_path.join("python");
|
||||
let dist_dir = python_dir.join("dist");
|
||||
let pyz_path = dist_dir.join("djls_inspector.pyz");
|
||||
|
||||
std::fs::create_dir_all(&dist_dir).expect("Failed to create python/dist directory");
|
||||
|
||||
let python = get_python_executable_path();
|
||||
|
||||
let output = Command::new(&python)
|
||||
.arg("build.py")
|
||||
.current_dir(&python_dir)
|
||||
.output()
|
||||
.expect("Failed to run Python build script");
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
panic!("Failed to build Python inspector:\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}");
|
||||
}
|
||||
|
||||
assert!(
|
||||
pyz_path.exists(),
|
||||
"Python inspector zipapp was not created at expected location: {}",
|
||||
pyz_path.display()
|
||||
);
|
||||
|
||||
let metadata =
|
||||
std::fs::metadata(&pyz_path).expect("Failed to get metadata for inspector zipapp");
|
||||
|
||||
println!(
|
||||
"cargo:warning=Successfully built Python inspector: {} ({} bytes)",
|
||||
pyz_path.display(),
|
||||
metadata.len()
|
||||
);
|
||||
}
|
||||
|
|
20
crates/djls-project/src/inspector.rs
Normal file
20
crates/djls-project/src/inspector.rs
Normal file
|
@ -0,0 +1,20 @@
|
|||
pub mod ipc;
|
||||
pub mod pool;
|
||||
pub mod queries;
|
||||
|
||||
pub use queries::Query;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct DjlsRequest {
|
||||
#[serde(flatten)]
|
||||
pub query: Query,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DjlsResponse {
|
||||
pub ok: bool,
|
||||
pub data: Option<serde_json::Value>,
|
||||
pub error: Option<String>,
|
||||
}
|
128
crates/djls-project/src/inspector/ipc.rs
Normal file
128
crates/djls-project/src/inspector/ipc.rs
Normal file
|
@ -0,0 +1,128 @@
|
|||
use std::io::BufRead;
|
||||
use std::io::BufReader;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::process::Child;
|
||||
use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use serde_json;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use super::DjlsRequest;
|
||||
use super::DjlsResponse;
|
||||
use crate::python::PythonEnvironment;
|
||||
|
||||
const INSPECTOR_PYZ: &[u8] = include_bytes!(concat!(
|
||||
env!("CARGO_WORKSPACE_DIR"),
|
||||
"/python/dist/djls_inspector.pyz"
|
||||
));
|
||||
|
||||
pub struct InspectorProcess {
|
||||
child: Child,
|
||||
stdin: std::process::ChildStdin,
|
||||
stdout: BufReader<std::process::ChildStdout>,
|
||||
_zipapp_file: NamedTempFile,
|
||||
}
|
||||
|
||||
impl InspectorProcess {
|
||||
pub fn new(python_env: &PythonEnvironment, project_path: &Path) -> Result<Self> {
|
||||
let mut zipapp_file = tempfile::Builder::new()
|
||||
.prefix("djls_inspector_")
|
||||
.suffix(".pyz")
|
||||
.tempfile()
|
||||
.context("Failed to create temp file for inspector")?;
|
||||
|
||||
zipapp_file
|
||||
.write_all(INSPECTOR_PYZ)
|
||||
.context("Failed to write inspector zipapp to temp file")?;
|
||||
zipapp_file
|
||||
.flush()
|
||||
.context("Failed to flush inspector zipapp")?;
|
||||
|
||||
let zipapp_path = zipapp_file.path();
|
||||
|
||||
let mut cmd = Command::new(&python_env.python_path);
|
||||
cmd.arg(zipapp_path)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::inherit())
|
||||
.current_dir(project_path);
|
||||
|
||||
if let Ok(pythonpath) = std::env::var("PYTHONPATH") {
|
||||
let mut paths = vec![project_path.to_string_lossy().to_string()];
|
||||
paths.push(pythonpath);
|
||||
cmd.env("PYTHONPATH", paths.join(":"));
|
||||
} else {
|
||||
cmd.env("PYTHONPATH", project_path);
|
||||
}
|
||||
|
||||
if let Ok(settings) = std::env::var("DJANGO_SETTINGS_MODULE") {
|
||||
cmd.env("DJANGO_SETTINGS_MODULE", settings);
|
||||
} else {
|
||||
// Try to detect settings module
|
||||
if project_path.join("manage.py").exists() {
|
||||
// Look for common settings modules
|
||||
for candidate in &["settings", "config.settings", "project.settings"] {
|
||||
let parts: Vec<&str> = candidate.split('.').collect();
|
||||
let mut path = project_path.to_path_buf();
|
||||
for part in &parts[..parts.len() - 1] {
|
||||
path = path.join(part);
|
||||
}
|
||||
if let Some(last) = parts.last() {
|
||||
path = path.join(format!("{last}.py"));
|
||||
}
|
||||
|
||||
if path.exists() {
|
||||
cmd.env("DJANGO_SETTINGS_MODULE", candidate);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut child = cmd.spawn().context("Failed to spawn inspector process")?;
|
||||
|
||||
let stdin = child.stdin.take().context("Failed to get stdin handle")?;
|
||||
let stdout = BufReader::new(child.stdout.take().context("Failed to get stdout handle")?);
|
||||
|
||||
Ok(Self {
|
||||
child,
|
||||
stdin,
|
||||
stdout,
|
||||
_zipapp_file: zipapp_file,
|
||||
})
|
||||
}
|
||||
|
||||
/// Send a request and receive a response
|
||||
pub fn query(&mut self, request: &DjlsRequest) -> Result<DjlsResponse> {
|
||||
let request_json = serde_json::to_string(request)?;
|
||||
|
||||
writeln!(self.stdin, "{request_json}")?;
|
||||
self.stdin.flush()?;
|
||||
|
||||
let mut response_line = String::new();
|
||||
self.stdout
|
||||
.read_line(&mut response_line)
|
||||
.context("Failed to read response from inspector")?;
|
||||
|
||||
let response: DjlsResponse =
|
||||
serde_json::from_str(&response_line).context("Failed to parse inspector response")?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub fn is_running(&mut self) -> bool {
|
||||
matches!(self.child.try_wait(), Ok(None))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for InspectorProcess {
|
||||
fn drop(&mut self) {
|
||||
// Try to terminate the child process gracefully
|
||||
let _ = self.child.kill();
|
||||
let _ = self.child.wait();
|
||||
}
|
||||
}
|
206
crates/djls-project/src/inspector/pool.rs
Normal file
206
crates/djls-project/src/inspector/pool.rs
Normal file
|
@ -0,0 +1,206 @@
|
|||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use super::ipc::InspectorProcess;
|
||||
use super::DjlsRequest;
|
||||
use super::DjlsResponse;
|
||||
use crate::python::PythonEnvironment;
|
||||
|
||||
/// Global singleton pool for convenience
|
||||
static GLOBAL_POOL: std::sync::OnceLock<InspectorPool> = std::sync::OnceLock::new();
|
||||
|
||||
pub fn global_pool() -> &'static InspectorPool {
|
||||
GLOBAL_POOL.get_or_init(InspectorPool::new)
|
||||
}
|
||||
const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
/// Manages a pool of inspector processes with automatic cleanup
|
||||
#[derive(Clone)]
|
||||
pub struct InspectorPool {
|
||||
inner: Arc<Mutex<InspectorPoolInner>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for InspectorPool {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("InspectorPool")
|
||||
.field("has_active_process", &self.has_active_process())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
struct InspectorPoolInner {
|
||||
process: Option<InspectorProcessHandle>,
|
||||
idle_timeout: Duration,
|
||||
}
|
||||
|
||||
struct InspectorProcessHandle {
|
||||
process: InspectorProcess,
|
||||
last_used: Instant,
|
||||
python_env: PythonEnvironment,
|
||||
project_path: std::path::PathBuf,
|
||||
}
|
||||
|
||||
impl InspectorPool {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::with_timeout(DEFAULT_IDLE_TIMEOUT)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_timeout(idle_timeout: Duration) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(Mutex::new(InspectorPoolInner {
|
||||
process: None,
|
||||
idle_timeout,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a query, reusing existing process if available and not idle
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the inspector pool mutex is poisoned (another thread panicked while holding the lock)
|
||||
pub fn query(
|
||||
&self,
|
||||
python_env: &PythonEnvironment,
|
||||
project_path: &Path,
|
||||
request: &DjlsRequest,
|
||||
) -> Result<DjlsResponse> {
|
||||
let mut inner = self.inner.lock().expect("Inspector pool mutex poisoned");
|
||||
let idle_timeout = inner.idle_timeout;
|
||||
|
||||
// Check if we need to drop the existing process
|
||||
let need_new_process = if let Some(handle) = &mut inner.process {
|
||||
let idle_too_long = handle.last_used.elapsed() > idle_timeout;
|
||||
let not_running = !handle.process.is_running();
|
||||
let different_env =
|
||||
handle.python_env != *python_env || handle.project_path != project_path;
|
||||
|
||||
idle_too_long || not_running || different_env
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if need_new_process {
|
||||
inner.process = None;
|
||||
}
|
||||
|
||||
// Get or create process
|
||||
if inner.process.is_none() {
|
||||
let process = InspectorProcess::new(python_env, project_path)?;
|
||||
inner.process = Some(InspectorProcessHandle {
|
||||
process,
|
||||
last_used: Instant::now(),
|
||||
python_env: python_env.clone(),
|
||||
project_path: project_path.to_path_buf(),
|
||||
});
|
||||
}
|
||||
|
||||
// Now we can safely get a mutable reference
|
||||
let handle = inner
|
||||
.process
|
||||
.as_mut()
|
||||
.expect("Process should exist after creation");
|
||||
|
||||
// Execute query
|
||||
let response = handle.process.query(request)?;
|
||||
handle.last_used = Instant::now();
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// Manually close the inspector process
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the inspector pool mutex is poisoned
|
||||
pub fn close(&self) {
|
||||
let mut inner = self.inner.lock().expect("Inspector pool mutex poisoned");
|
||||
inner.process = None;
|
||||
}
|
||||
|
||||
/// Check if there's an active process
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the inspector pool mutex is poisoned
|
||||
#[must_use]
|
||||
pub fn has_active_process(&self) -> bool {
|
||||
let mut inner = self.inner.lock().expect("Inspector pool mutex poisoned");
|
||||
if let Some(handle) = &mut inner.process {
|
||||
handle.process.is_running() && handle.last_used.elapsed() <= inner.idle_timeout
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the configured idle timeout
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the inspector pool mutex is poisoned
|
||||
#[must_use]
|
||||
pub fn idle_timeout(&self) -> Duration {
|
||||
let inner = self.inner.lock().expect("Inspector pool mutex poisoned");
|
||||
inner.idle_timeout
|
||||
}
|
||||
|
||||
/// Start a background cleanup task that periodically checks for idle processes
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// The spawned thread will panic if the inspector pool mutex is poisoned
|
||||
pub fn start_cleanup_task(self: Arc<Self>) {
|
||||
std::thread::spawn(move || {
|
||||
loop {
|
||||
std::thread::sleep(Duration::from_secs(30)); // Check every 30 seconds
|
||||
|
||||
let mut inner = self.inner.lock().expect("Inspector pool mutex poisoned");
|
||||
if let Some(handle) = &inner.process {
|
||||
if handle.last_used.elapsed() > inner.idle_timeout {
|
||||
// Process is idle, drop it
|
||||
inner.process = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InspectorPool {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_pool_creation() {
|
||||
let pool = InspectorPool::new();
|
||||
assert_eq!(pool.idle_timeout(), DEFAULT_IDLE_TIMEOUT);
|
||||
assert!(!pool.has_active_process());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pool_with_custom_timeout() {
|
||||
let timeout = Duration::from_secs(120);
|
||||
let pool = InspectorPool::with_timeout(timeout);
|
||||
assert_eq!(pool.idle_timeout(), timeout);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pool_close() {
|
||||
let pool = InspectorPool::new();
|
||||
pool.close();
|
||||
assert!(!pool.has_active_process());
|
||||
}
|
||||
}
|
50
crates/djls-project/src/inspector/queries.rs
Normal file
50
crates/djls-project/src/inspector/queries.rs
Normal file
|
@ -0,0 +1,50 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(tag = "query", content = "args")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Query {
|
||||
PythonEnv,
|
||||
Templatetags,
|
||||
DjangoInit,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum VersionReleaseLevel {
|
||||
Alpha,
|
||||
Beta,
|
||||
Candidate,
|
||||
Final,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct PythonEnvironmentQueryData {
|
||||
pub sys_base_prefix: PathBuf,
|
||||
pub sys_executable: PathBuf,
|
||||
pub sys_path: Vec<PathBuf>,
|
||||
pub sys_platform: String,
|
||||
pub sys_prefix: PathBuf,
|
||||
pub sys_version_info: (u32, u32, u32, VersionReleaseLevel, u32),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct TemplateTagQueryData {
|
||||
pub templatetags: Vec<TemplateTag>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct TemplateTag {
|
||||
pub name: String,
|
||||
pub module: String,
|
||||
pub doc: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct DjangoInitQueryData {
|
||||
pub success: bool,
|
||||
pub message: Option<String>,
|
||||
}
|
|
@ -1,17 +1,23 @@
|
|||
mod db;
|
||||
pub mod inspector;
|
||||
mod meta;
|
||||
mod python;
|
||||
pub mod python;
|
||||
mod system;
|
||||
mod templatetags;
|
||||
|
||||
use std::fmt;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
pub use db::find_python_environment;
|
||||
pub use db::Db;
|
||||
use inspector::pool::InspectorPool;
|
||||
use inspector::DjlsRequest;
|
||||
use inspector::Query;
|
||||
pub use meta::ProjectMetadata;
|
||||
use pyo3::prelude::*;
|
||||
pub use python::PythonEnvironment;
|
||||
pub use templatetags::TemplateTags;
|
||||
|
||||
|
@ -20,6 +26,7 @@ pub struct DjangoProject {
|
|||
path: PathBuf,
|
||||
env: Option<PythonEnvironment>,
|
||||
template_tags: Option<TemplateTags>,
|
||||
inspector_pool: Arc<InspectorPool>,
|
||||
}
|
||||
|
||||
impl DjangoProject {
|
||||
|
@ -29,45 +36,39 @@ impl DjangoProject {
|
|||
path,
|
||||
env: None,
|
||||
template_tags: None,
|
||||
inspector_pool: Arc::new(InspectorPool::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initialize(&mut self, db: &dyn Db) -> PyResult<()> {
|
||||
pub fn initialize(&mut self, db: &dyn Db) -> Result<()> {
|
||||
// Use the database to find the Python environment
|
||||
self.env = find_python_environment(db);
|
||||
if self.env.is_none() {
|
||||
return Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(
|
||||
"Could not find Python environment",
|
||||
));
|
||||
let env = self
|
||||
.env
|
||||
.as_ref()
|
||||
.context("Could not find Python environment")?;
|
||||
|
||||
// Initialize Django
|
||||
let request = DjlsRequest {
|
||||
query: Query::DjangoInit,
|
||||
};
|
||||
let response = self.inspector_pool.query(env, &self.path, &request)?;
|
||||
|
||||
if !response.ok {
|
||||
anyhow::bail!("Failed to initialize Django: {:?}", response.error);
|
||||
}
|
||||
|
||||
Python::with_gil(|py| {
|
||||
let sys = py.import("sys")?;
|
||||
let py_path = sys.getattr("path")?;
|
||||
// Get template tags
|
||||
let request = DjlsRequest {
|
||||
query: Query::Templatetags,
|
||||
};
|
||||
let response = self.inspector_pool.query(env, &self.path, &request)?;
|
||||
|
||||
if let Some(path_str) = self.path.to_str() {
|
||||
py_path.call_method1("insert", (0, path_str))?;
|
||||
}
|
||||
if let Some(data) = response.data {
|
||||
self.template_tags = Some(TemplateTags::from_json(&data)?);
|
||||
}
|
||||
|
||||
let env = self.env.as_ref().ok_or_else(|| {
|
||||
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(
|
||||
"Internal error: Python environment missing after initialization",
|
||||
)
|
||||
})?;
|
||||
env.activate(py)?;
|
||||
|
||||
match py.import("django") {
|
||||
Ok(django) => {
|
||||
django.call_method0("setup")?;
|
||||
self.template_tags = Some(TemplateTags::from_python(py)?);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to import Django: {e}");
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
|
|
|
@ -2,15 +2,13 @@ use std::fmt;
|
|||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use crate::system;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PythonEnvironment {
|
||||
python_path: PathBuf,
|
||||
sys_path: Vec<PathBuf>,
|
||||
sys_prefix: PathBuf,
|
||||
pub python_path: PathBuf,
|
||||
pub sys_path: Vec<PathBuf>,
|
||||
pub sys_prefix: PathBuf,
|
||||
}
|
||||
|
||||
impl PythonEnvironment {
|
||||
|
@ -74,19 +72,6 @@ impl PythonEnvironment {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn activate(&self, py: Python) -> PyResult<()> {
|
||||
let sys = py.import("sys")?;
|
||||
let py_path = sys.getattr("path")?;
|
||||
|
||||
for path in &self.sys_path {
|
||||
if let Some(path_str) = path.to_str() {
|
||||
py_path.call_method1("append", (path_str,))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn from_system_python() -> Option<Self> {
|
||||
let Ok(python_path) = system::find_executable("python") else {
|
||||
return None;
|
||||
|
@ -181,20 +166,6 @@ mod tests {
|
|||
prefix
|
||||
}
|
||||
|
||||
fn get_sys_path(py: Python) -> PyResult<Vec<String>> {
|
||||
let sys = py.import("sys")?;
|
||||
let py_path = sys.getattr("path")?;
|
||||
py_path.extract::<Vec<String>>()
|
||||
}
|
||||
|
||||
fn create_test_env(sys_paths: Vec<PathBuf>) -> PythonEnvironment {
|
||||
PythonEnvironment {
|
||||
python_path: PathBuf::from("dummy/bin/python"),
|
||||
sys_prefix: PathBuf::from("dummy"),
|
||||
sys_path: sys_paths,
|
||||
}
|
||||
}
|
||||
|
||||
mod env_discovery {
|
||||
use which::Error as WhichError;
|
||||
|
||||
|
@ -493,213 +464,6 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
mod env_activation {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[ignore = "Requires Python runtime - run with --ignored flag"]
|
||||
fn test_activate_appends_paths() -> PyResult<()> {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let path1 = temp_dir.path().join("scripts");
|
||||
let path2 = temp_dir.path().join("libs");
|
||||
fs::create_dir_all(&path1).unwrap();
|
||||
fs::create_dir_all(&path2).unwrap();
|
||||
|
||||
let test_env = create_test_env(vec![path1.clone(), path2.clone()]);
|
||||
|
||||
pyo3::prepare_freethreaded_python();
|
||||
|
||||
Python::with_gil(|py| {
|
||||
let initial_sys_path = get_sys_path(py)?;
|
||||
let initial_len = initial_sys_path.len();
|
||||
|
||||
test_env.activate(py)?;
|
||||
|
||||
let final_sys_path = get_sys_path(py)?;
|
||||
assert_eq!(
|
||||
final_sys_path.len(),
|
||||
initial_len + 2,
|
||||
"Should have added 2 paths"
|
||||
);
|
||||
assert_eq!(
|
||||
final_sys_path.get(initial_len).unwrap(),
|
||||
path1.to_str().expect("Path 1 should be valid UTF-8")
|
||||
);
|
||||
assert_eq!(
|
||||
final_sys_path.get(initial_len + 1).unwrap(),
|
||||
path2.to_str().expect("Path 2 should be valid UTF-8")
|
||||
);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "Requires Python runtime - run with --ignored flag"]
|
||||
fn test_activate_empty_sys_path() -> PyResult<()> {
|
||||
let test_env = create_test_env(vec![]);
|
||||
|
||||
pyo3::prepare_freethreaded_python();
|
||||
|
||||
Python::with_gil(|py| {
|
||||
let initial_sys_path = get_sys_path(py)?;
|
||||
|
||||
test_env.activate(py)?;
|
||||
|
||||
let final_sys_path = get_sys_path(py)?;
|
||||
assert_eq!(
|
||||
final_sys_path, initial_sys_path,
|
||||
"sys.path should remain unchanged for empty env.sys_path"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "Requires Python runtime - run with --ignored flag"]
|
||||
fn test_activate_with_non_existent_paths() -> PyResult<()> {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let path1 = temp_dir.path().join("non_existent_dir");
|
||||
let path2 = temp_dir.path().join("another_missing/path");
|
||||
|
||||
let test_env = create_test_env(vec![path1.clone(), path2.clone()]);
|
||||
|
||||
pyo3::prepare_freethreaded_python();
|
||||
|
||||
Python::with_gil(|py| {
|
||||
let initial_sys_path = get_sys_path(py)?;
|
||||
let initial_len = initial_sys_path.len();
|
||||
|
||||
test_env.activate(py)?;
|
||||
|
||||
let final_sys_path = get_sys_path(py)?;
|
||||
assert_eq!(
|
||||
final_sys_path.len(),
|
||||
initial_len + 2,
|
||||
"Should still add 2 paths even if they don't exist"
|
||||
);
|
||||
assert_eq!(
|
||||
final_sys_path.get(initial_len).unwrap(),
|
||||
path1.to_str().unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
final_sys_path.get(initial_len + 1).unwrap(),
|
||||
path2.to_str().unwrap()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
#[ignore = "Requires Python runtime - run with --ignored flag"]
|
||||
fn test_activate_skips_non_utf8_paths_unix() -> PyResult<()> {
|
||||
use std::ffi::OsStr;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let valid_path = temp_dir.path().join("valid_dir");
|
||||
fs::create_dir(&valid_path).unwrap();
|
||||
|
||||
let invalid_bytes = b"invalid_\xff_utf8";
|
||||
let os_str = OsStr::from_bytes(invalid_bytes);
|
||||
let non_utf8_path = PathBuf::from(os_str);
|
||||
assert!(
|
||||
non_utf8_path.to_str().is_none(),
|
||||
"Path should not be convertible to UTF-8 str"
|
||||
);
|
||||
|
||||
let test_env = create_test_env(vec![valid_path.clone(), non_utf8_path.clone()]);
|
||||
|
||||
pyo3::prepare_freethreaded_python();
|
||||
|
||||
Python::with_gil(|py| {
|
||||
let initial_sys_path = get_sys_path(py)?;
|
||||
let initial_len = initial_sys_path.len();
|
||||
|
||||
test_env.activate(py)?;
|
||||
|
||||
let final_sys_path = get_sys_path(py)?;
|
||||
assert_eq!(
|
||||
final_sys_path.len(),
|
||||
initial_len + 1,
|
||||
"Should only add valid UTF-8 paths"
|
||||
);
|
||||
assert_eq!(
|
||||
final_sys_path.get(initial_len).unwrap(),
|
||||
valid_path.to_str().unwrap()
|
||||
);
|
||||
|
||||
let invalid_path_lossy = non_utf8_path.to_string_lossy();
|
||||
assert!(
|
||||
!final_sys_path
|
||||
.iter()
|
||||
.any(|p| p.contains(&*invalid_path_lossy)),
|
||||
"Non-UTF8 path should not be present in sys.path"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
#[ignore = "Requires Python runtime - run with --ignored flag"]
|
||||
fn test_activate_skips_non_utf8_paths_windows() -> PyResult<()> {
|
||||
use std::ffi::OsString;
|
||||
use std::os::windows::ffi::OsStringExt;
|
||||
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let valid_path = temp_dir.path().join("valid_dir");
|
||||
|
||||
let invalid_wide: Vec<u16> = vec![
|
||||
'i' as u16, 'n' as u16, 'v' as u16, 'a' as u16, 'l' as u16, 'i' as u16, 'd' as u16,
|
||||
'_' as u16, 0xD800, '_' as u16, 'w' as u16, 'i' as u16, 'd' as u16, 'e' as u16,
|
||||
];
|
||||
let os_string = OsString::from_wide(&invalid_wide);
|
||||
let non_utf8_path = PathBuf::from(os_string);
|
||||
|
||||
assert!(
|
||||
non_utf8_path.to_str().is_none(),
|
||||
"Path with lone surrogate should not be convertible to UTF-8 str"
|
||||
);
|
||||
|
||||
let test_env = create_test_env(vec![valid_path.clone(), non_utf8_path.clone()]);
|
||||
|
||||
pyo3::prepare_freethreaded_python();
|
||||
|
||||
Python::with_gil(|py| {
|
||||
let initial_sys_path = get_sys_path(py)?;
|
||||
let initial_len = initial_sys_path.len();
|
||||
|
||||
test_env.activate(py)?;
|
||||
|
||||
let final_sys_path = get_sys_path(py)?;
|
||||
assert_eq!(
|
||||
final_sys_path.len(),
|
||||
initial_len + 1,
|
||||
"Should only add paths convertible to valid UTF-8"
|
||||
);
|
||||
assert_eq!(
|
||||
final_sys_path.get(initial_len).unwrap(),
|
||||
valid_path.to_str().unwrap()
|
||||
);
|
||||
|
||||
let invalid_path_lossy = non_utf8_path.to_string_lossy();
|
||||
assert!(
|
||||
!final_sys_path
|
||||
.iter()
|
||||
.any(|p| p.contains(&*invalid_path_lossy)),
|
||||
"Non-UTF8 path (from invalid wide chars) should not be present in sys.path"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
mod salsa_integration {
|
||||
use std::sync::Arc;
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use std::ops::Deref;
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::PyDict;
|
||||
use pyo3::types::PyList;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct TemplateTags(Vec<TemplateTag>);
|
||||
|
@ -16,57 +16,46 @@ impl Deref for TemplateTags {
|
|||
}
|
||||
|
||||
impl TemplateTags {
|
||||
fn new() -> Self {
|
||||
Self(Vec::new())
|
||||
}
|
||||
pub fn from_json(data: &Value) -> Result<TemplateTags> {
|
||||
let mut tags = Vec::new();
|
||||
|
||||
fn process_library(
|
||||
module_name: &str,
|
||||
library: &Bound<'_, PyAny>,
|
||||
tags: &mut Vec<TemplateTag>,
|
||||
) -> PyResult<()> {
|
||||
let tags_dict = library.getattr("tags")?;
|
||||
let dict = tags_dict.downcast::<PyDict>()?;
|
||||
// Parse the JSON response from the inspector
|
||||
let templatetags = data
|
||||
.get("templatetags")
|
||||
.context("Missing 'templatetags' field in response")?
|
||||
.as_array()
|
||||
.context("'templatetags' field is not an array")?;
|
||||
|
||||
for (key, value) in dict.iter() {
|
||||
let tag_name = key.extract::<String>()?;
|
||||
let doc = value.getattr("__doc__")?.extract().ok();
|
||||
for tag_data in templatetags {
|
||||
let name = tag_data
|
||||
.get("name")
|
||||
.and_then(|v| v.as_str())
|
||||
.context("Missing or invalid 'name' field")?
|
||||
.to_string();
|
||||
|
||||
let library_name = if module_name.is_empty() {
|
||||
"builtins".to_string()
|
||||
} else {
|
||||
module_name.split('.').next_back().unwrap_or("").to_string()
|
||||
};
|
||||
let module = tag_data
|
||||
.get("module")
|
||||
.and_then(|v| v.as_str())
|
||||
.context("Missing or invalid 'module' field")?;
|
||||
|
||||
tags.push(TemplateTag::new(tag_name, library_name, doc));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
// Extract library name from module (e.g., "django.templatetags.static" -> "static")
|
||||
let library = module
|
||||
.split('.')
|
||||
.filter(|part| part.contains("templatetags"))
|
||||
.nth(1)
|
||||
.or_else(|| module.split('.').next_back())
|
||||
.unwrap_or("builtins")
|
||||
.to_string();
|
||||
|
||||
pub fn from_python(py: Python) -> PyResult<TemplateTags> {
|
||||
let mut template_tags = TemplateTags::new();
|
||||
let doc = tag_data
|
||||
.get("doc")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from);
|
||||
|
||||
let engine = py
|
||||
.import("django.template.engine")?
|
||||
.getattr("Engine")?
|
||||
.call_method0("get_default")?;
|
||||
|
||||
// Built-in template tags
|
||||
let builtins_attr = engine.getattr("template_builtins")?;
|
||||
let builtins = builtins_attr.downcast::<PyList>()?;
|
||||
for builtin in builtins {
|
||||
Self::process_library("", &builtin, &mut template_tags.0)?;
|
||||
tags.push(TemplateTag::new(name, library, doc));
|
||||
}
|
||||
|
||||
// Custom template libraries
|
||||
let libraries_attr = engine.getattr("template_libraries")?;
|
||||
let libraries = libraries_attr.downcast::<PyDict>()?;
|
||||
for (module_name, library) in libraries.iter() {
|
||||
let module_name = module_name.extract::<String>()?;
|
||||
Self::process_library(&module_name, &library, &mut template_tags.0)?;
|
||||
}
|
||||
|
||||
Ok(template_tags)
|
||||
Ok(TemplateTags(tags))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue