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

This commit is contained in:
Josh Thomas 2025-09-09 19:08:42 -05:00 committed by GitHub
parent 31b0308a40
commit d99c96d6b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 903 additions and 696 deletions

View file

@ -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

View file

@ -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()
);
}

View 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>,
}

View 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();
}
}

View 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());
}
}

View 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>,
}

View file

@ -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]

View file

@ -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;

View file

@ -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))
}
}