Remove vestigal concrete Project database, keeping trait (#198)
Some checks failed
lint / pre-commit (push) Has been cancelled
lint / rustfmt (push) Has been cancelled
lint / clippy (push) Has been cancelled
lint / cargo-check (push) Has been cancelled
release / build (push) Has been cancelled
release / test (push) Has been cancelled
test / generate-matrix (push) Has been cancelled
zizmor 🌈 / zizmor latest via PyPI (push) Has been cancelled
release / release (push) Has been cancelled
test / Python , Django () (push) Has been cancelled
test / tests (push) Has been cancelled

This commit is contained in:
Josh Thomas 2025-09-06 12:23:22 -05:00 committed by GitHub
parent 318a395d6f
commit 5974c51383
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 142 additions and 68 deletions

2
Cargo.lock generated
View file

@ -459,6 +459,7 @@ name = "djls-project"
version = "0.0.0" version = "0.0.0"
dependencies = [ dependencies = [
"djls-dev", "djls-dev",
"djls-workspace",
"pyo3", "pyo3",
"salsa", "salsa",
"tempfile", "tempfile",
@ -513,7 +514,6 @@ dependencies = [
"anyhow", "anyhow",
"camino", "camino",
"dashmap", "dashmap",
"djls-project",
"notify", "notify",
"percent-encoding", "percent-encoding",
"salsa", "salsa",

View file

@ -8,6 +8,8 @@ extension-module = []
default = [] default = []
[dependencies] [dependencies]
djls-workspace = { workspace = true }
pyo3 = { workspace = true } pyo3 = { workspace = true }
salsa = { workspace = true } salsa = { workspace = true }
which = { workspace = true} which = { workspace = true}

View file

@ -1,31 +1,31 @@
use crate::meta::ProjectMetadata; //! Project-specific database trait and queries.
//!
//! This module extends the workspace database trait with project-specific
//! functionality including metadata access and Python environment discovery.
use djls_workspace::Db as WorkspaceDb;
use crate::meta::ProjectMetadata;
use crate::python::PythonEnvironment;
/// Project-specific database trait extending the workspace database
#[salsa::db] #[salsa::db]
pub trait Db: salsa::Database { pub trait Db: WorkspaceDb {
/// Get the project metadata containing root path and venv configuration
fn metadata(&self) -> &ProjectMetadata; fn metadata(&self) -> &ProjectMetadata;
} }
#[salsa::db] /// Find the Python environment for the project.
#[derive(Clone)] ///
pub struct ProjectDatabase { /// This Salsa tracked function discovers the Python environment based on:
storage: salsa::Storage<ProjectDatabase>, /// 1. Explicit venv path from metadata
metadata: ProjectMetadata, /// 2. VIRTUAL_ENV environment variable
/// 3. Common venv directories in project root (.venv, venv, env, .env)
/// 4. System Python as fallback
#[salsa::tracked]
pub fn find_python_environment(db: &dyn Db) -> Option<PythonEnvironment> {
let project_path = db.metadata().root().as_path();
let venv_path = db.metadata().venv().and_then(|p| p.to_str());
PythonEnvironment::new(project_path, venv_path)
} }
impl ProjectDatabase {
pub fn new(metadata: ProjectMetadata) -> Self {
let storage = salsa::Storage::new(None);
Self { storage, metadata }
}
}
#[salsa::db]
impl Db for ProjectDatabase {
fn metadata(&self) -> &ProjectMetadata {
&self.metadata
}
}
#[salsa::db]
impl salsa::Database for ProjectDatabase {}

View file

@ -8,11 +8,11 @@ use std::fmt;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use db::ProjectDatabase; pub use db::find_python_environment;
use meta::ProjectMetadata; pub use db::Db;
pub use meta::ProjectMetadata;
use pyo3::prelude::*; use pyo3::prelude::*;
use python::find_python_environment; pub use python::PythonEnvironment;
use python::PythonEnvironment;
pub use templatetags::TemplateTags; pub use templatetags::TemplateTags;
#[derive(Debug)] #[derive(Debug)]
@ -32,11 +32,9 @@ impl DjangoProject {
} }
} }
pub fn initialize(&mut self, venv_path: Option<&str>) -> PyResult<()> { pub fn initialize(&mut self, db: &dyn Db) -> PyResult<()> {
let venv_pathbuf = venv_path.map(PathBuf::from); // Use the database to find the Python environment
let metadata = ProjectMetadata::new(self.path.clone(), venv_pathbuf); self.env = find_python_environment(db);
let db = ProjectDatabase::new(metadata);
self.env = find_python_environment(&db);
if self.env.is_none() { if self.env.is_none() {
return Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>( return Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(
"Could not find Python environment", "Could not find Python environment",

View file

@ -7,14 +7,17 @@ pub struct ProjectMetadata {
} }
impl ProjectMetadata { impl ProjectMetadata {
#[must_use]
pub fn new(root: PathBuf, venv: Option<PathBuf>) -> Self { pub fn new(root: PathBuf, venv: Option<PathBuf>) -> Self {
ProjectMetadata { root, venv } ProjectMetadata { root, venv }
} }
#[must_use]
pub fn root(&self) -> &PathBuf { pub fn root(&self) -> &PathBuf {
&self.root &self.root
} }
#[must_use]
pub fn venv(&self) -> Option<&PathBuf> { pub fn venv(&self) -> Option<&PathBuf> {
self.venv.as_ref() self.venv.as_ref()
} }

View file

@ -4,17 +4,8 @@ use std::path::PathBuf;
use pyo3::prelude::*; use pyo3::prelude::*;
use crate::db::Db;
use crate::system; use crate::system;
#[salsa::tracked]
pub fn find_python_environment(db: &dyn Db) -> Option<PythonEnvironment> {
let project_path = db.metadata().root().as_path();
let venv_path = db.metadata().venv().and_then(|p| p.to_str());
PythonEnvironment::new(project_path, venv_path)
}
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct PythonEnvironment { pub struct PythonEnvironment {
python_path: PathBuf, python_path: PathBuf,
@ -23,7 +14,8 @@ pub struct PythonEnvironment {
} }
impl PythonEnvironment { impl PythonEnvironment {
fn new(project_path: &Path, venv_path: Option<&str>) -> Option<Self> { #[must_use]
pub fn new(project_path: &Path, venv_path: Option<&str>) -> Option<Self> {
if let Some(path) = venv_path { if let Some(path) = venv_path {
let prefix = PathBuf::from(path); let prefix = PathBuf::from(path);
if let Some(env) = Self::from_venv_prefix(&prefix) { if let Some(env) = Self::from_venv_prefix(&prefix) {
@ -703,12 +695,57 @@ mod tests {
} }
} }
// Add tests for the salsa tracked function
mod salsa_integration { mod salsa_integration {
use std::sync::Arc;
use djls_workspace::FileSystem;
use djls_workspace::InMemoryFileSystem;
use super::*; use super::*;
use crate::db::ProjectDatabase; use crate::db::find_python_environment;
use crate::db::Db as ProjectDb;
use crate::meta::ProjectMetadata; use crate::meta::ProjectMetadata;
/// Test implementation of ProjectDb for unit tests
#[salsa::db]
#[derive(Clone)]
struct TestDatabase {
storage: salsa::Storage<TestDatabase>,
metadata: ProjectMetadata,
fs: Arc<dyn FileSystem>,
}
impl TestDatabase {
fn new(metadata: ProjectMetadata) -> Self {
Self {
storage: salsa::Storage::new(None),
metadata,
fs: Arc::new(InMemoryFileSystem::new()),
}
}
}
#[salsa::db]
impl salsa::Database for TestDatabase {}
#[salsa::db]
impl djls_workspace::Db for TestDatabase {
fn fs(&self) -> Arc<dyn FileSystem> {
self.fs.clone()
}
fn read_file_content(&self, path: &std::path::Path) -> std::io::Result<String> {
self.fs.read_to_string(path)
}
}
#[salsa::db]
impl ProjectDb for TestDatabase {
fn metadata(&self) -> &ProjectMetadata {
&self.metadata
}
}
#[test] #[test]
fn test_find_python_environment_with_salsa_db() { fn test_find_python_environment_with_salsa_db() {
let project_dir = tempdir().unwrap(); let project_dir = tempdir().unwrap();
@ -721,8 +758,8 @@ mod tests {
let metadata = let metadata =
ProjectMetadata::new(project_dir.path().to_path_buf(), Some(venv_prefix.clone())); ProjectMetadata::new(project_dir.path().to_path_buf(), Some(venv_prefix.clone()));
// Create a ProjectDatabase with the metadata // Create a TestDatabase with the metadata
let db = ProjectDatabase::new(metadata); let db = TestDatabase::new(metadata);
// Call the tracked function // Call the tracked function
let env = find_python_environment(&db); let env = find_python_environment(&db);
@ -756,8 +793,8 @@ mod tests {
// Create a metadata instance with project path but no explicit venv path // Create a metadata instance with project path but no explicit venv path
let metadata = ProjectMetadata::new(project_dir.path().to_path_buf(), None); let metadata = ProjectMetadata::new(project_dir.path().to_path_buf(), None);
// Create a ProjectDatabase with the metadata // Create a TestDatabase with the metadata
let db = ProjectDatabase::new(metadata); let db = TestDatabase::new(metadata);
// Mock to ensure VIRTUAL_ENV is not set // Mock to ensure VIRTUAL_ENV is not set
let _guard = system::mock::MockGuard; let _guard = system::mock::MockGuard;

View file

@ -1,8 +1,8 @@
//! Concrete Salsa database implementation for the Django Language Server. //! Concrete Salsa database implementation for the Django Language Server.
//! //!
//! This module provides the concrete [`DjangoDatabase`] that implements all //! This module provides the concrete [`DjangoDatabase`] that implements all
//! the database traits from workspace and template crates. This follows Ruff's //! the database traits from workspace, template, and project crates. This follows
//! architecture pattern where the concrete database lives at the top level. //! Ruff's architecture pattern where the concrete database lives at the top level.
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
@ -11,6 +11,8 @@ use std::sync::Arc;
use std::sync::Mutex; use std::sync::Mutex;
use dashmap::DashMap; use dashmap::DashMap;
use djls_project::Db as ProjectDb;
use djls_project::ProjectMetadata;
use djls_templates::db::Db as TemplateDb; use djls_templates::db::Db as TemplateDb;
use djls_workspace::db::Db as WorkspaceDb; use djls_workspace::db::Db as WorkspaceDb;
use djls_workspace::db::SourceFile; use djls_workspace::db::SourceFile;
@ -23,6 +25,7 @@ use salsa::Setter;
/// This database implements all the traits from various crates: /// This database implements all the traits from various crates:
/// - [`WorkspaceDb`] for file system access and core operations /// - [`WorkspaceDb`] for file system access and core operations
/// - [`TemplateDb`] for template parsing and diagnostics /// - [`TemplateDb`] for template parsing and diagnostics
/// - [`ProjectDb`] for project metadata and Python environment
#[salsa::db] #[salsa::db]
#[derive(Clone)] #[derive(Clone)]
pub struct DjangoDatabase { pub struct DjangoDatabase {
@ -32,6 +35,9 @@ pub struct DjangoDatabase {
/// Maps paths to [`SourceFile`] entities for O(1) lookup. /// Maps paths to [`SourceFile`] entities for O(1) lookup.
files: Arc<DashMap<PathBuf, SourceFile>>, files: Arc<DashMap<PathBuf, SourceFile>>,
/// Project metadata containing root path and venv configuration.
metadata: ProjectMetadata,
storage: salsa::Storage<Self>, storage: salsa::Storage<Self>,
// The logs are only used for testing and demonstrating reuse: // The logs are only used for testing and demonstrating reuse:
@ -49,6 +55,7 @@ impl Default for DjangoDatabase {
Self { Self {
fs: Arc::new(InMemoryFileSystem::new()), fs: Arc::new(InMemoryFileSystem::new()),
files: Arc::new(DashMap::new()), files: Arc::new(DashMap::new()),
metadata: ProjectMetadata::new(PathBuf::from("/test"), None),
storage: salsa::Storage::new(Some(Box::new({ storage: salsa::Storage::new(Some(Box::new({
let logs = logs.clone(); let logs = logs.clone();
move |event| { move |event| {
@ -68,11 +75,16 @@ impl Default for DjangoDatabase {
} }
impl DjangoDatabase { impl DjangoDatabase {
/// Create a new [`DjangoDatabase`] with the given file system and file map. /// Create a new [`DjangoDatabase`] with the given file system, file map, and project metadata.
pub fn new(file_system: Arc<dyn FileSystem>, files: Arc<DashMap<PathBuf, SourceFile>>) -> Self { pub fn new(
file_system: Arc<dyn FileSystem>,
files: Arc<DashMap<PathBuf, SourceFile>>,
metadata: ProjectMetadata,
) -> Self {
Self { Self {
fs: file_system, fs: file_system,
files, files,
metadata,
storage: salsa::Storage::new(None), storage: salsa::Storage::new(None),
#[cfg(test)] #[cfg(test)]
logs: Arc::new(Mutex::new(None)), logs: Arc::new(Mutex::new(None)),
@ -149,3 +161,10 @@ impl WorkspaceDb for DjangoDatabase {
#[salsa::db] #[salsa::db]
impl TemplateDb for DjangoDatabase {} impl TemplateDb for DjangoDatabase {}
#[salsa::db]
impl ProjectDb for DjangoDatabase {
fn metadata(&self) -> &ProjectMetadata {
&self.metadata
}
}

View file

@ -149,12 +149,7 @@ impl LanguageServer for DjangoLanguageServer {
let init_result = { let init_result = {
let mut session_lock = session_arc.lock().await; let mut session_lock = session_arc.lock().await;
if let Some(project) = session_lock.project_mut().as_mut() { session_lock.initialize_project()
project.initialize(venv_path.as_deref())
} else {
// Project was removed between read and write locks
Ok(())
}
}; };
match init_result { match init_result {

View file

@ -9,11 +9,13 @@ use std::sync::Arc;
use dashmap::DashMap; use dashmap::DashMap;
use djls_conf::Settings; use djls_conf::Settings;
use djls_project::DjangoProject; use djls_project::DjangoProject;
use djls_project::ProjectMetadata;
use djls_workspace::db::SourceFile; use djls_workspace::db::SourceFile;
use djls_workspace::paths; use djls_workspace::paths;
use djls_workspace::PositionEncoding; use djls_workspace::PositionEncoding;
use djls_workspace::TextDocument; use djls_workspace::TextDocument;
use djls_workspace::Workspace; use djls_workspace::Workspace;
use pyo3::PyResult;
use tower_lsp_server::lsp_types; use tower_lsp_server::lsp_types;
use url::Url; use url::Url;
@ -63,23 +65,29 @@ impl Session {
std::env::current_dir().ok() std::env::current_dir().ok()
}); });
let (project, settings) = if let Some(path) = &project_path { let (project, settings, metadata) = if let Some(path) = &project_path {
let settings = let settings =
djls_conf::Settings::new(path).unwrap_or_else(|_| djls_conf::Settings::default()); djls_conf::Settings::new(path).unwrap_or_else(|_| djls_conf::Settings::default());
let project = Some(djls_project::DjangoProject::new(path.clone())); let project = Some(djls_project::DjangoProject::new(path.clone()));
(project, settings) // Create metadata for the project with venv path from settings
let venv_path = settings.venv_path().map(PathBuf::from);
let metadata = ProjectMetadata::new(path.clone(), venv_path);
(project, settings, metadata)
} else { } else {
(None, Settings::default()) // Default metadata for when there's no project path
let metadata = ProjectMetadata::new(PathBuf::from("."), None);
(None, Settings::default(), metadata)
}; };
// Create workspace for buffer management // Create workspace for buffer management
let workspace = Workspace::new(); let workspace = Workspace::new();
// Create the concrete database with the workspace's file system // Create the concrete database with the workspace's file system and metadata
let files = Arc::new(DashMap::new()); let files = Arc::new(DashMap::new());
let db = DjangoDatabase::new(workspace.file_system(), files); let db = DjangoDatabase::new(workspace.file_system(), files, metadata);
Self { Self {
db, db,
@ -130,6 +138,20 @@ impl Session {
f(&mut self.db) f(&mut self.db)
} }
/// Get a reference to the database for project operations.
pub fn database(&self) -> &DjangoDatabase {
&self.db
}
/// Initialize the project with the database.
pub fn initialize_project(&mut self) -> PyResult<()> {
if let Some(project) = self.project.as_mut() {
project.initialize(&self.db)
} else {
Ok(())
}
}
/// Open a document in the session. /// Open a document in the session.
/// ///
/// Updates both the workspace buffers and database. Creates the file in /// Updates both the workspace buffers and database. Creates the file in

View file

@ -4,8 +4,6 @@ version = "0.0.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
djls-project = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
camino = { workspace = true } camino = { workspace = true }
dashmap = { workspace = true } dashmap = { workspace = true }