Skip to content

Commit c2eab34

Browse files
add support for configuring PYTHONPATH
1 parent c334196 commit c2eab34

File tree

6 files changed

+108
-24
lines changed

6 files changed

+108
-24
lines changed

CHANGELOG.md

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

2121
### Added
2222

23+
- Added `pythonpath` configuration option for specifying additional Python import paths
2324
- Added documentation for Zed extension
2425
- Added documentation for setting up Sublime Text
2526

crates/djls-conf/src/lib.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ pub struct Settings {
3838
venv_path: Option<String>,
3939
django_settings_module: Option<String>,
4040
#[serde(default)]
41+
pythonpath: Vec<String>,
42+
#[serde(default)]
4143
tagspecs: Vec<TagSpecDef>,
4244
}
4345

@@ -54,6 +56,9 @@ impl Settings {
5456
settings.django_settings_module = overrides
5557
.django_settings_module
5658
.or(settings.django_settings_module);
59+
if !overrides.pythonpath.is_empty() {
60+
settings.pythonpath = overrides.pythonpath;
61+
}
5762
if !overrides.tagspecs.is_empty() {
5863
settings.tagspecs = overrides.tagspecs;
5964
}
@@ -120,6 +125,11 @@ impl Settings {
120125
self.django_settings_module.as_deref()
121126
}
122127

128+
#[must_use]
129+
pub fn pythonpath(&self) -> &[String] {
130+
&self.pythonpath
131+
}
132+
123133
#[must_use]
124134
pub fn tagspecs(&self) -> &[TagSpecDef] {
125135
&self.tagspecs
@@ -148,6 +158,7 @@ mod tests {
148158
debug: false,
149159
venv_path: None,
150160
django_settings_module: None,
161+
pythonpath: vec![],
151162
tagspecs: vec![],
152163
}
153164
);
@@ -185,6 +196,24 @@ mod tests {
185196
);
186197
}
187198

199+
#[test]
200+
fn test_load_pythonpath_config() {
201+
let dir = tempdir().unwrap();
202+
fs::write(
203+
dir.path().join("djls.toml"),
204+
r#"pythonpath = ["/path/to/lib", "/another/path"]"#,
205+
)
206+
.unwrap();
207+
let settings = Settings::new(Utf8Path::from_path(dir.path()).unwrap(), None).unwrap();
208+
assert_eq!(
209+
settings,
210+
Settings {
211+
pythonpath: vec!["/path/to/lib".to_string(), "/another/path".to_string()],
212+
..Default::default()
213+
}
214+
);
215+
}
216+
188217
#[test]
189218
fn test_load_dot_djls_toml_only() {
190219
let dir = tempdir().unwrap();

crates/djls-project/src/inspector.rs

Lines changed: 54 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,23 @@ pub fn query<Q: InspectorRequest>(db: &dyn ProjectDb, request: &Q) -> Option<Q::
4040
let interpreter = project.interpreter(db);
4141
let project_path = project.root(db);
4242
let django_settings_module = project.django_settings_module(db);
43+
let pythonpath = project.pythonpath(db);
4344

4445
tracing::debug!(
45-
"Inspector query '{}': interpreter={:?}, project_path={}, django_settings_module={:?}",
46+
"Inspector query '{}': interpreter={:?}, project_path={}, django_settings_module={:?}, pythonpath={:?}",
4647
Q::NAME,
4748
interpreter,
4849
project_path,
49-
django_settings_module
50+
django_settings_module,
51+
pythonpath
5052
);
5153

5254
let inspector = db.inspector();
5355
match inspector.query::<Q, Q::Response>(
5456
interpreter,
5557
project_path,
5658
django_settings_module.as_deref(),
59+
pythonpath,
5760
request,
5861
) {
5962
Ok(response) if response.ok => {
@@ -128,10 +131,16 @@ impl Inspector {
128131
interpreter: &Interpreter,
129132
project_path: &Utf8Path,
130133
django_settings_module: Option<&str>,
134+
pythonpath: &[String],
131135
request: &Q,
132136
) -> Result<InspectorResponse<R>> {
133-
self.inner()
134-
.query::<Q, R>(interpreter, project_path, django_settings_module, request)
137+
self.inner().query::<Q, R>(
138+
interpreter,
139+
project_path,
140+
django_settings_module,
141+
pythonpath,
142+
request,
143+
)
135144
}
136145

137146
/// Manually close the inspector process
@@ -172,9 +181,15 @@ impl InspectorInner {
172181
interpreter: &Interpreter,
173182
project_path: &Utf8Path,
174183
django_settings_module: Option<&str>,
184+
pythonpath: &[String],
175185
request: &Q,
176186
) -> Result<InspectorResponse<R>> {
177-
self.ensure_process(interpreter, project_path, django_settings_module)?;
187+
self.ensure_process(
188+
interpreter,
189+
project_path,
190+
django_settings_module,
191+
pythonpath,
192+
)?;
178193

179194
let process = self.process_mut();
180195
let response = process.query::<Q, R>(request)?;
@@ -196,6 +211,7 @@ impl InspectorInner {
196211
interpreter: &Interpreter,
197212
project_path: &Utf8Path,
198213
django_settings_module: Option<&str>,
214+
pythonpath: &[String],
199215
) -> Result<()> {
200216
let needs_new_process = match &mut self.process {
201217
None => {
@@ -208,11 +224,17 @@ impl InspectorInner {
208224
let path_changed = state.project_path != project_path;
209225
let settings_changed =
210226
state.django_settings_module.as_deref() != django_settings_module;
211-
212-
if not_running || interpreter_changed || path_changed || settings_changed {
227+
let pythonpath_changed = state.pythonpath != pythonpath;
228+
229+
if not_running
230+
|| interpreter_changed
231+
|| path_changed
232+
|| settings_changed
233+
|| pythonpath_changed
234+
{
213235
tracing::debug!(
214-
"Inspector process needs restart: not_running={}, interpreter_changed={}, path_changed={}, settings_changed={}",
215-
not_running, interpreter_changed, path_changed, settings_changed
236+
"Inspector process needs restart: not_running={}, interpreter_changed={}, path_changed={}, settings_changed={}, pythonpath_changed={}",
237+
not_running, interpreter_changed, path_changed, settings_changed, pythonpath_changed
216238
);
217239
true
218240
} else {
@@ -224,13 +246,15 @@ impl InspectorInner {
224246
if needs_new_process {
225247
self.shutdown_process();
226248
tracing::info!(
227-
"Spawning new inspector process with django_settings_module={:?}",
228-
django_settings_module
249+
"Spawning new inspector process with django_settings_module={:?}, pythonpath={:?}",
250+
django_settings_module,
251+
pythonpath
229252
);
230253
self.process = Some(InspectorProcess::spawn(
231254
interpreter.to_owned(),
232-
project_path.to_path_buf(),
255+
&project_path.to_path_buf(),
233256
django_settings_module.map(String::from),
257+
pythonpath.to_vec(),
234258
)?);
235259
}
236260
Ok(())
@@ -288,6 +312,7 @@ struct InspectorProcess {
288312
interpreter: Interpreter,
289313
project_path: Utf8PathBuf,
290314
django_settings_module: Option<String>,
315+
pythonpath: Vec<String>,
291316
// keep a handle on the tempfile so it doesn't get cleaned up
292317
_zipapp_file_handle: InspectorFile,
293318
}
@@ -296,30 +321,36 @@ impl InspectorProcess {
296321
/// Spawn a new inspector process
297322
pub fn spawn(
298323
interpreter: Interpreter,
299-
project_path: Utf8PathBuf,
324+
project_path: &Utf8PathBuf,
300325
django_settings_module: Option<String>,
326+
pythonpath: Vec<String>,
301327
) -> Result<Self> {
302328
let zipapp_file = InspectorFile::create()?;
303329

304330
let python_path = interpreter
305-
.python_path(&project_path)
331+
.python_path(project_path)
306332
.context("Failed to resolve Python interpreter")?;
307333

308334
let mut cmd = Command::new(&python_path);
309335
cmd.arg(zipapp_file.path())
310336
.stdin(Stdio::piped())
311337
.stdout(Stdio::piped())
312338
.stderr(Stdio::piped()) // Capture stderr instead of inheriting
313-
.current_dir(&project_path);
339+
.current_dir(project_path);
314340

315-
if let Ok(pythonpath) = std::env::var("PYTHONPATH") {
316-
let mut paths = vec![project_path.to_string()];
317-
paths.push(pythonpath);
318-
cmd.env("PYTHONPATH", paths.join(":"));
319-
} else {
320-
cmd.env("PYTHONPATH", project_path.clone());
341+
let mut paths = vec![project_path.to_string()];
342+
paths.extend(pythonpath.iter().cloned());
343+
if let Ok(env_pythonpath) = std::env::var("PYTHONPATH") {
344+
paths.push(env_pythonpath);
321345
}
322346

347+
#[cfg(unix)]
348+
let path_separator = ":";
349+
#[cfg(windows)]
350+
let path_separator = ";";
351+
352+
cmd.env("PYTHONPATH", paths.join(path_separator));
353+
323354
// Set Django settings module if we have one
324355
if let Some(ref module) = django_settings_module {
325356
tracing::debug!("Setting DJANGO_SETTINGS_MODULE={}", module);
@@ -355,8 +386,9 @@ impl InspectorProcess {
355386
stdout,
356387
last_used: Instant::now(),
357388
interpreter,
358-
project_path,
389+
project_path: project_path.to_owned(),
359390
django_settings_module,
391+
pythonpath,
360392
_zipapp_file_handle: zipapp_file,
361393
})
362394
}

crates/djls-project/src/project.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ pub struct Project {
2323
/// Django settings module (e.g., "myproject.settings")
2424
#[returns(ref)]
2525
pub django_settings_module: Option<String>,
26+
/// Additional Python import paths (PYTHONPATH entries)
27+
#[returns(ref)]
28+
pub pythonpath: Vec<String>,
2629
}
2730

2831
impl Project {
@@ -31,6 +34,7 @@ impl Project {
3134
root: &Utf8Path,
3235
venv_path: Option<&str>,
3336
django_settings_module: Option<&str>,
37+
pythonpath: &[String],
3438
) -> Project {
3539
let interpreter = Interpreter::discover(venv_path);
3640

@@ -75,6 +79,7 @@ impl Project {
7579
root.to_path_buf(),
7680
interpreter,
7781
resolved_django_settings_module,
82+
pythonpath.to_vec(),
7883
)
7984
}
8085

crates/djls-server/src/db.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ impl DjangoDatabase {
116116
self.settings.lock().unwrap().clone()
117117
}
118118

119-
/// Update the settings, potentially updating the project if `venv_path` or `django_settings_module` changed
119+
/// Update the settings, potentially updating the project if `venv_path`, `django_settings_module`, or `pythonpath` changed
120120
///
121121
/// # Panics
122122
///
@@ -126,6 +126,7 @@ impl DjangoDatabase {
126126
let old = self.settings();
127127
old.venv_path() != settings.venv_path()
128128
|| old.django_settings_module() != settings.django_settings_module()
129+
|| old.pythonpath() != settings.pythonpath()
129130
};
130131

131132
*self.settings.lock().unwrap() = settings;
@@ -145,6 +146,7 @@ impl DjangoDatabase {
145146
root,
146147
settings.venv_path(),
147148
settings.django_settings_module(),
149+
settings.pythonpath(),
148150
);
149151
*self.project.lock().unwrap() = Some(project);
150152
}

docs/configuration.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,19 @@ The server needs access to your virtual environment to discover installed Django
3636
- Your virtual environment is in a non-standard location
3737
- Auto-detection fails for your setup
3838

39+
### `pythonpath`
40+
41+
**Default:** `[]` (empty list)
42+
43+
Additional directories to add to Python's import search path when the inspector process runs. These paths are added to `PYTHONPATH` alongside the project root and any existing `PYTHONPATH` environment variable.
44+
45+
**When to configure:**
46+
47+
- Your project has a non-standard structure where Django code imports from directories outside the project root
48+
- You're working in a monorepo where Django imports shared packages from other directories
49+
- Your project depends on internal libraries in non-standard locations
50+
- You need to make additional packages importable for Django introspection
51+
3952
### `debug`
4053

4154
**Default:** `false`
@@ -66,7 +79,8 @@ Pass configuration through your editor's LSP client using `initializationOptions
6679
```json
6780
{
6881
"django_settings_module": "myproject.settings",
69-
"venv_path": "/path/to/venv"
82+
"venv_path": "/path/to/venv",
83+
"pythonpath": ["/path/to/shared/libs"]
7084
}
7185
```
7286

@@ -82,6 +96,7 @@ If you use `pyproject.toml`, add a `[tool.djls]` section:
8296
[tool.djls]
8397
django_settings_module = "myproject.settings"
8498
venv_path = "/path/to/venv" # Optional: only if auto-detection fails
99+
pythonpath = ["/path/to/shared/libs"] # Optional: additional import paths
85100
```
86101

87102
If you prefer a dedicated config file or don't use `pyproject.toml`, you can use `djls.toml` (same settings, no `[tool.djls]` table).

0 commit comments

Comments
 (0)