Skip to content

Commit b33d3f8

Browse files
Add support for system-wide installation (#62)
1 parent 84d87a8 commit b33d3f8

File tree

4 files changed

+122
-17
lines changed

4 files changed

+122
-17
lines changed

CHANGELOG.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/
2020

2121
### Added
2222

23+
- Support for system-wide installation using `uv tool` or `pipx` with automatic Python environment detection and virtualenv discovery
24+
25+
### Changed
26+
27+
- Server no longer requires installation in project virtualenv, including robust Python dependency resolution using `PATH` and `site-packages` detection
28+
29+
## [5.1.0a1]
30+
31+
### Added
32+
2333
- Basic Neovim plugin
2434

2535
## [5.1.0a0]
@@ -44,5 +54,6 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/
4454

4555
- Josh Thomas <josh@joshthomas.dev> (maintainer)
4656

47-
[unreleased]: https://github.com/joshuadavidthomas/django-language-server/compare/v5.1.0a0...HEAD
57+
[unreleased]: https://github.com/joshuadavidthomas/django-language-server/compare/v5.1.0a1...HEAD
4858
[5.1.0a0]: https://github.com/joshuadavidthomas/django-language-server/releases/tag/v5.1.0a0
59+
[5.1.0a1]: https://github.com/joshuadavidthomas/django-language-server/releases/tag/v5.1.0a1

README.md

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,19 @@ See the [Versioning](#versioning) section for details on how this project's vers
5353

5454
## Installation
5555

56-
Install the Django Language Server in your project's environment:
56+
The Django Language Server can be installed using your preferred Python package manager.
57+
58+
For system-wide availability using either `uv` or `pipx`:
59+
60+
```bash
61+
uv tool install django-language-server
62+
63+
# or
64+
65+
pipx install django-language-server
66+
```
67+
68+
Or to try it out in your current project:
5769

5870
```bash
5971
uv add --dev django-language-server
@@ -67,11 +79,9 @@ pip install django-language-server
6779
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.
6880

6981
> [!NOTE]
70-
> 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.
82+
> 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.
7183
>
72-
> 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.
73-
>
74-
> 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.
84+
> 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.
7585
7686
## Editor Setup
7787

@@ -144,11 +154,9 @@ The project is written in Rust using PyO3 for Python integration. Here is a high
144154
- Template parsing ([`crates/djls-template-ast/`](./crates/djls-template-ast/))
145155
- Tokio-based background task management ([`crates/djls-worker/`](./crates/djls-worker/))
146156

147-
Code contributions are welcome from developers of all backgrounds. Rust expertise is especially valuable for the LSP server and core components.
148-
149-
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!
157+
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.
150158

151-
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.
159+
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!
152160

153161
## License
154162

crates/djls-project/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ edition = "2021"
66
[dependencies]
77
pyo3 = { workspace = true }
88
tower-lsp = { workspace = true }
9+
10+
which = "7.0.1"

crates/djls-project/src/lib.rs

Lines changed: 91 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,23 @@ mod templatetags;
33
pub use templatetags::TemplateTags;
44

55
use pyo3::prelude::*;
6+
use std::fmt;
67
use std::path::{Path, PathBuf};
78
use tower_lsp::lsp_types::*;
9+
use which::which;
810

911
#[derive(Debug)]
1012
pub struct DjangoProject {
1113
path: PathBuf,
14+
env: Option<PythonEnvironment>,
1215
template_tags: Option<TemplateTags>,
1316
}
1417

1518
impl DjangoProject {
1619
pub fn new(path: PathBuf) -> Self {
1720
Self {
1821
path,
22+
env: None,
1923
template_tags: None,
2024
}
2125
}
@@ -36,19 +40,37 @@ impl DjangoProject {
3640
}
3741

3842
pub fn initialize(&mut self) -> PyResult<()> {
43+
let python_env = PythonEnvironment::new().ok_or_else(|| {
44+
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>("Could not find Python in PATH")
45+
})?;
46+
3947
Python::with_gil(|py| {
40-
// Add project to Python path
4148
let sys = py.import("sys")?;
4249
let py_path = sys.getattr("path")?;
43-
py_path.call_method1("append", (self.path.to_str().unwrap(),))?;
4450

45-
// Setup Django
46-
let django = py.import("django")?;
47-
django.call_method0("setup")?;
51+
if let Some(path_str) = self.path.to_str() {
52+
py_path.call_method1("insert", (0, path_str))?;
53+
}
54+
55+
for path in &python_env.sys_path {
56+
if let Some(path_str) = path.to_str() {
57+
py_path.call_method1("append", (path_str,))?;
58+
}
59+
}
4860

49-
self.template_tags = Some(TemplateTags::from_python(py)?);
61+
self.env = Some(python_env);
5062

51-
Ok(())
63+
match py.import("django") {
64+
Ok(django) => {
65+
django.call_method0("setup")?;
66+
self.template_tags = Some(TemplateTags::from_python(py)?);
67+
Ok(())
68+
}
69+
Err(e) => {
70+
eprintln!("Failed to import Django: {}", e);
71+
Err(e)
72+
}
73+
}
5274
})
5375
}
5476

@@ -60,3 +82,65 @@ impl DjangoProject {
6082
&self.path
6183
}
6284
}
85+
86+
impl fmt::Display for DjangoProject {
87+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88+
writeln!(f, "Project path: {}", self.path.display())?;
89+
if let Some(py_env) = &self.env {
90+
write!(f, "{}", py_env)?;
91+
}
92+
Ok(())
93+
}
94+
}
95+
96+
#[derive(Debug)]
97+
struct PythonEnvironment {
98+
python_path: PathBuf,
99+
sys_path: Vec<PathBuf>,
100+
sys_prefix: PathBuf,
101+
}
102+
103+
impl PythonEnvironment {
104+
fn new() -> Option<Self> {
105+
let python_path = which("python").ok()?;
106+
let prefix = python_path.parent()?.parent()?;
107+
108+
let mut sys_path = Vec::new();
109+
sys_path.push(prefix.join("bin"));
110+
111+
if let Some(site_packages) = Self::find_site_packages(prefix) {
112+
sys_path.push(site_packages);
113+
}
114+
115+
Some(Self {
116+
python_path: python_path.clone(),
117+
sys_path,
118+
sys_prefix: prefix.to_path_buf(),
119+
})
120+
}
121+
122+
#[cfg(windows)]
123+
fn find_site_packages(prefix: &Path) -> Option<PathBuf> {
124+
Some(prefix.join("Lib").join("site-packages"))
125+
}
126+
127+
#[cfg(not(windows))]
128+
fn find_site_packages(prefix: &Path) -> Option<PathBuf> {
129+
std::fs::read_dir(prefix.join("lib"))
130+
.ok()?
131+
.filter_map(Result::ok)
132+
.find(|e| e.file_name().to_string_lossy().starts_with("python"))
133+
.map(|e| e.path().join("site-packages"))
134+
}
135+
}
136+
137+
impl fmt::Display for PythonEnvironment {
138+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139+
writeln!(f, "Sys prefix: {}", self.sys_prefix.display())?;
140+
writeln!(f, "Sys paths:")?;
141+
for path in &self.sys_path {
142+
writeln!(f, " {}", path.display())?;
143+
}
144+
Ok(())
145+
}
146+
}

0 commit comments

Comments
 (0)