Skip to content

Commit

Permalink
Add support for system-wide installation (#62)
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuadavidthomas authored Jan 3, 2025
1 parent 84d87a8 commit b33d3f8
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 17 deletions.
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -44,5 +54,6 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/

- Josh Thomas <[email protected]> (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
26 changes: 17 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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.

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!
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.

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

Expand Down
2 changes: 2 additions & 0 deletions crates/djls-project/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ edition = "2021"
[dependencies]
pyo3 = { workspace = true }
tower-lsp = { workspace = true }

which = "7.0.1"
98 changes: 91 additions & 7 deletions crates/djls-project/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,23 @@ 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>,
}

impl DjangoProject {
pub fn new(path: PathBuf) -> Self {
Self {
path,
env: None,
template_tags: None,
}
}
Expand All @@ -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))?;
}

for path in &python_env.sys_path {
if let Some(path_str) = path.to_str() {
py_path.call_method1("append", (path_str,))?;
}
}

self.template_tags = Some(TemplateTags::from_python(py)?);
self.env = Some(python_env);

Ok(())
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)
}
}
})
}

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

0 comments on commit b33d3f8

Please sign in to comment.