Add support for system-wide installation (#62)

This commit is contained in:
Josh Thomas 2025-01-03 11:39:18 -06:00 committed by GitHub
parent 84d87a8857
commit b33d3f890d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 122 additions and 17 deletions

View file

@ -20,6 +20,16 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/
### Added
- Support for system-wide installation using `uv tool` or `pipx` with automatic Python environment detection and virtualenv discovery
### Changed
- Server no longer requires installation in project virtualenv, including robust Python dependency resolution using `PATH` and `site-packages` detection
## [5.1.0a1]
### Added
- Basic Neovim plugin
## [5.1.0a0]
@ -44,5 +54,6 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/
- Josh Thomas <josh@joshthomas.dev> (maintainer)
[unreleased]: https://github.com/joshuadavidthomas/django-language-server/compare/v5.1.0a0...HEAD
[unreleased]: https://github.com/joshuadavidthomas/django-language-server/compare/v5.1.0a1...HEAD
[5.1.0a0]: https://github.com/joshuadavidthomas/django-language-server/releases/tag/v5.1.0a0
[5.1.0a1]: https://github.com/joshuadavidthomas/django-language-server/releases/tag/v5.1.0a1

View file

@ -53,7 +53,19 @@ See the [Versioning](#versioning) section for details on how this project's vers
## Installation
Install the Django Language Server in your project's environment:
The Django Language Server can be installed using your preferred Python package manager.
For system-wide availability using either `uv` or `pipx`:
```bash
uv tool install django-language-server
# or
pipx install django-language-server
```
Or to try it out in your current project:
```bash
uv add --dev django-language-server
@ -67,11 +79,9 @@ pip install django-language-server
The package provides pre-built wheels with the Rust-based LSP server compiled for common platforms. Installing it adds the `djls` command-line tool to your environment.
> [!NOTE]
> The server must currently be installed in each project's environment as it needs to run using the project's Python interpreter to access the correct Django installation and other dependencies.
> The server will automatically detect and use your project's Python environment when you open a Django project. It needs access to your project's Django installation and other dependencies, but should be able to find these regardless of where the server itself is installed.
>
> Global installation is not yet supported as it would run against a global Python environment rather than your project's virtualenv. The server uses [PyO3](https://pyo3.rs) to interact with Django, and we aim to support global installation in the future, allowing the server to detect and use project virtualenvs, but this is a tricky problem involving PyO3 and Python interpreter management.
>
> If you have experience with [PyO3](https://pyo3.rs) or [maturin](https://maturin.rs) and ideas on how to achieve this, please check the [Contributing](#contributing) section below.
> It's recommended to use `uv` or `pipx` to install it system-wide for convenience, but installing in your project's environment will work just as well to give it a test drive around the block.
## Editor Setup
@ -144,11 +154,9 @@ The project is written in Rust using PyO3 for Python integration. Here is a high
- Template parsing ([`crates/djls-template-ast/`](./crates/djls-template-ast/))
- Tokio-based background task management ([`crates/djls-worker/`](./crates/djls-worker/))
Code contributions are welcome from developers of all backgrounds. Rust expertise is especially valuable for the LSP server and core components.
Code contributions are welcome from developers of all backgrounds. Rust expertise is valuable for the LSP server and core components, but Python and Django developers should not be deterred by the Rust codebase - Django expertise is just as valuable. Understanding Django's internals and common development patterns helps inform what features would be most valuable.
One significant challenge we're trying to solve is supporting global installation of the language server while still allowing it to detect and use project virtualenvs for Django introspection. Currently, the server must be installed in each project's virtualenv to access the project's Django installation. If you have experience with PyO3 and ideas about how to achieve this, we'd love your help!
Python and Django developers should not be deterred by the Rust codebase - Django expertise is just as valuable. Understanding Django's internals and common development patterns helps inform what features would be most valuable. The Rust components were built by [a simple country CRUD web developer](https://youtu.be/7ij_1SQqbVo?si=hwwPyBjmaOGnvPPI&t=53) learning Rust along the way.
So far it's all been built by a [a simple country CRUD web developer](https://youtu.be/7ij_1SQqbVo?si=hwwPyBjmaOGnvPPI&t=53) learning Rust along the way - send help!
## License

View file

@ -6,3 +6,5 @@ edition = "2021"
[dependencies]
pyo3 = { workspace = true }
tower-lsp = { workspace = true }
which = "7.0.1"

View file

@ -3,12 +3,15 @@ mod templatetags;
pub use templatetags::TemplateTags;
use pyo3::prelude::*;
use std::fmt;
use std::path::{Path, PathBuf};
use tower_lsp::lsp_types::*;
use which::which;
#[derive(Debug)]
pub struct DjangoProject {
path: PathBuf,
env: Option<PythonEnvironment>,
template_tags: Option<TemplateTags>,
}
@ -16,6 +19,7 @@ impl DjangoProject {
pub fn new(path: PathBuf) -> Self {
Self {
path,
env: None,
template_tags: None,
}
}
@ -36,19 +40,37 @@ impl DjangoProject {
}
pub fn initialize(&mut self) -> PyResult<()> {
let python_env = PythonEnvironment::new().ok_or_else(|| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>("Could not find Python in PATH")
})?;
Python::with_gil(|py| {
// Add project to Python path
let sys = py.import("sys")?;
let py_path = sys.getattr("path")?;
py_path.call_method1("append", (self.path.to_str().unwrap(),))?;
// Setup Django
let django = py.import("django")?;
django.call_method0("setup")?;
if let Some(path_str) = self.path.to_str() {
py_path.call_method1("insert", (0, path_str))?;
}
self.template_tags = Some(TemplateTags::from_python(py)?);
for path in &python_env.sys_path {
if let Some(path_str) = path.to_str() {
py_path.call_method1("append", (path_str,))?;
}
}
Ok(())
self.env = Some(python_env);
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)
}
}
})
}
@ -60,3 +82,65 @@ impl DjangoProject {
&self.path
}
}
impl fmt::Display for DjangoProject {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Project path: {}", self.path.display())?;
if let Some(py_env) = &self.env {
write!(f, "{}", py_env)?;
}
Ok(())
}
}
#[derive(Debug)]
struct PythonEnvironment {
python_path: PathBuf,
sys_path: Vec<PathBuf>,
sys_prefix: PathBuf,
}
impl PythonEnvironment {
fn new() -> Option<Self> {
let python_path = which("python").ok()?;
let prefix = python_path.parent()?.parent()?;
let mut sys_path = Vec::new();
sys_path.push(prefix.join("bin"));
if let Some(site_packages) = Self::find_site_packages(prefix) {
sys_path.push(site_packages);
}
Some(Self {
python_path: python_path.clone(),
sys_path,
sys_prefix: prefix.to_path_buf(),
})
}
#[cfg(windows)]
fn find_site_packages(prefix: &Path) -> Option<PathBuf> {
Some(prefix.join("Lib").join("site-packages"))
}
#[cfg(not(windows))]
fn find_site_packages(prefix: &Path) -> Option<PathBuf> {
std::fs::read_dir(prefix.join("lib"))
.ok()?
.filter_map(Result::ok)
.find(|e| e.file_name().to_string_lossy().starts_with("python"))
.map(|e| e.path().join("site-packages"))
}
}
impl fmt::Display for PythonEnvironment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Sys prefix: {}", self.sys_prefix.display())?;
writeln!(f, "Sys paths:")?;
for path in &self.sys_path {
writeln!(f, " {}", path.display())?;
}
Ok(())
}
}