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"
dependencies = [
"djls-dev",
"djls-workspace",
"pyo3",
"salsa",
"tempfile",
@ -513,7 +514,6 @@ dependencies = [
"anyhow",
"camino",
"dashmap",
"djls-project",
"notify",
"percent-encoding",
"salsa",

View file

@ -8,6 +8,8 @@ extension-module = []
default = []
[dependencies]
djls-workspace = { workspace = true }
pyo3 = { workspace = true }
salsa = { 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]
pub trait Db: salsa::Database {
pub trait Db: WorkspaceDb {
/// Get the project metadata containing root path and venv configuration
fn metadata(&self) -> &ProjectMetadata;
}
#[salsa::db]
#[derive(Clone)]
pub struct ProjectDatabase {
storage: salsa::Storage<ProjectDatabase>,
metadata: ProjectMetadata,
}
/// Find the Python environment for the project.
///
/// This Salsa tracked function discovers the Python environment based on:
/// 1. Explicit venv path from metadata
/// 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());
impl ProjectDatabase {
pub fn new(metadata: ProjectMetadata) -> Self {
let storage = salsa::Storage::new(None);
Self { storage, metadata }
PythonEnvironment::new(project_path, venv_path)
}
}
#[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::PathBuf;
use db::ProjectDatabase;
use meta::ProjectMetadata;
pub use db::find_python_environment;
pub use db::Db;
pub use meta::ProjectMetadata;
use pyo3::prelude::*;
use python::find_python_environment;
use python::PythonEnvironment;
pub use python::PythonEnvironment;
pub use templatetags::TemplateTags;
#[derive(Debug)]
@ -32,11 +32,9 @@ impl DjangoProject {
}
}
pub fn initialize(&mut self, venv_path: Option<&str>) -> PyResult<()> {
let venv_pathbuf = venv_path.map(PathBuf::from);
let metadata = ProjectMetadata::new(self.path.clone(), venv_pathbuf);
let db = ProjectDatabase::new(metadata);
self.env = find_python_environment(&db);
pub fn initialize(&mut self, db: &dyn Db) -> PyResult<()> {
// 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",

View file

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

View file

@ -4,17 +4,8 @@ use std::path::PathBuf;
use pyo3::prelude::*;
use crate::db::Db;
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)]
pub struct PythonEnvironment {
python_path: PathBuf,
@ -23,7 +14,8 @@ pub struct 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 {
let prefix = PathBuf::from(path);
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 {
use std::sync::Arc;
use djls_workspace::FileSystem;
use djls_workspace::InMemoryFileSystem;
use super::*;
use crate::db::ProjectDatabase;
use crate::db::find_python_environment;
use crate::db::Db as ProjectDb;
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]
fn test_find_python_environment_with_salsa_db() {
let project_dir = tempdir().unwrap();
@ -721,8 +758,8 @@ mod tests {
let metadata =
ProjectMetadata::new(project_dir.path().to_path_buf(), Some(venv_prefix.clone()));
// Create a ProjectDatabase with the metadata
let db = ProjectDatabase::new(metadata);
// Create a TestDatabase with the metadata
let db = TestDatabase::new(metadata);
// Call the tracked function
let env = find_python_environment(&db);
@ -756,8 +793,8 @@ mod tests {
// Create a metadata instance with project path but no explicit venv path
let metadata = ProjectMetadata::new(project_dir.path().to_path_buf(), None);
// Create a ProjectDatabase with the metadata
let db = ProjectDatabase::new(metadata);
// Create a TestDatabase with the metadata
let db = TestDatabase::new(metadata);
// Mock to ensure VIRTUAL_ENV is not set
let _guard = system::mock::MockGuard;

View file

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

View file

@ -9,11 +9,13 @@ use std::sync::Arc;
use dashmap::DashMap;
use djls_conf::Settings;
use djls_project::DjangoProject;
use djls_project::ProjectMetadata;
use djls_workspace::db::SourceFile;
use djls_workspace::paths;
use djls_workspace::PositionEncoding;
use djls_workspace::TextDocument;
use djls_workspace::Workspace;
use pyo3::PyResult;
use tower_lsp_server::lsp_types;
use url::Url;
@ -63,23 +65,29 @@ impl Session {
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 =
djls_conf::Settings::new(path).unwrap_or_else(|_| djls_conf::Settings::default());
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 {
(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
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 db = DjangoDatabase::new(workspace.file_system(), files);
let db = DjangoDatabase::new(workspace.file_system(), files, metadata);
Self {
db,
@ -130,6 +138,20 @@ impl Session {
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.
///
/// Updates both the workspace buffers and database. Creates the file in

View file

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