Skip to content

Commit bbaaa51

Browse files
tvalloislgeiger
authored andcommitted
Add Pipenv support (#150)
* Start to manage pipenv * rework on pipenv * Clean code * adding unit test for Unix env * add pipenv install to travis * update travis.yml * add sudo * monkeypatch pipenv for unit tests * add mockspawn to devDependencies, split code into smaller functions, remove async
1 parent 0288a82 commit bbaaa51

File tree

4 files changed

+94
-12
lines changed

4 files changed

+94
-12
lines changed

lib/main.js

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const cp = require("child_process");
22
const { shell } = require("electron");
33
const { AutoLanguageClient } = require("atom-languageclient");
4-
const { detectVirtualEnv, sanitizeConfig } = require("./utils");
4+
const { detectVirtualEnv, detectPipEnv, replacePipEnvPathVar, sanitizeConfig } = require("./utils");
55

66
// Ref: https://github.com/nteract/hydrogen/blob/master/lib/autocomplete-provider.js#L33
77
// adapted from http://stackoverflow.com/q/5474008
@@ -42,22 +42,17 @@ class PythonLanguageClient extends AutoLanguageClient {
4242

4343
async startServerProcess(projectPath) {
4444
await new Promise(resolve => atom.whenShellEnvironmentLoaded(resolve));
45-
46-
const venvPath = await detectVirtualEnv(projectPath);
47-
45+
const venvPath = await detectPipEnv(projectPath) || await detectVirtualEnv(projectPath);
4846
const pylsEnvironment = Object.assign({}, process.env);
49-
5047
if (venvPath) {
5148
pylsEnvironment["VIRTUAL_ENV"] = venvPath;
5249
}
53-
54-
const python = atom.config.get("ide-python.python");
55-
50+
let python = atom.config.get("ide-python.python");
51+
python = replacePipEnvPathVar(python, venvPath);
5652
const childProcess = cp.spawn(python, ["-m", "pyls"], {
5753
cwd: projectPath,
5854
env: pylsEnvironment
5955
});
60-
6156
childProcess.on("error", err => {
6257
const description =
6358
err.code == "ENOENT"

lib/utils.js

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,26 @@
1+
const cp = require("child_process");
12
const { Directory } = require("atom");
23

34
const VIRTUAL_ENV_BIN_DIRS = ["bin", "Scripts"];
45
const VIRTUAL_ENV_EXECUTABLES = ["python", "python.exe"];
56

7+
function detectPipEnv(path) {
8+
return new Promise(resolve => {
9+
const pipEnvProcess = cp.spawn('pipenv', ["--venv"], {
10+
cwd: path
11+
});
12+
pipEnvProcess.stdout.on('data', (data) => {
13+
resolve(`${data}`.trim());
14+
});
15+
pipEnvProcess.stderr.on('data', () => {
16+
resolve(null);
17+
});
18+
pipEnvProcess.on('error', () => {
19+
resolve(null);
20+
})
21+
});
22+
}
23+
624
async function detectVirtualEnv(path) {
725
const entries = await new Promise(resolve =>
826
new Directory(path).getEntries((error, entries) => {
@@ -13,7 +31,6 @@ async function detectVirtualEnv(path) {
1331
}
1432
})
1533
);
16-
1734
if (entries) {
1835
for (let entry of entries) {
1936
if (entry.isDirectory()) {
@@ -51,5 +68,14 @@ function sanitizeConfig(config) {
5168
return config;
5269
}
5370

71+
function replacePipEnvPathVar(pythonPath, pipEnvPath) {
72+
if (pythonPath.indexOf('$PIPENV_PATH') !== -1 && pipEnvPath) {
73+
return pythonPath.replace('$PIPENV_PATH', pipEnvPath);
74+
}
75+
return pythonPath;
76+
}
77+
5478
exports.detectVirtualEnv = detectVirtualEnv;
5579
exports.sanitizeConfig = sanitizeConfig;
80+
exports.detectPipEnv = detectPipEnv;
81+
exports.replacePipEnvPathVar = replacePipEnvPathVar;

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
"dependencies": {
2929
"atom-languageclient": "0.9.9"
3030
},
31+
"devDependencies": {
32+
"mock-spawn": "^0.2.6"
33+
},
3134
"enhancedScopes": [
3235
"source.python"
3336
],
@@ -37,7 +40,7 @@
3740
"order": 1,
3841
"type": "string",
3942
"default": "python",
40-
"description": "Absolute path of your Python binary. This is used to launch the Python language server. Make sure to install `pyls` for this version of Python. Changes will take effect after a restart of the language server."
43+
"description": "Absolute path of your Python binary. This is used to launch the Python language server. Make sure to install `pyls` for this version of Python. Changes will take effect after a restart of the language server. Use $PIPENV_PATH if you want to use the pipenv path of your project"
4144
},
4245
"pylsConfigurationSources": {
4346
"order": 2,

spec/utils-spec.js

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
const path = require("path");
2-
const { detectVirtualEnv, sanitizeConfig } = require("../lib/utils");
2+
const mockSpawn = require("mock-spawn");
3+
const assert = require("assert");
4+
const child_process = require('child_process');
5+
const { detectVirtualEnv, sanitizeConfig, detectPipEnv, replacePipEnvPathVar } = require("../lib/utils");
36

47
const venvFixturesDir = path.join(__dirname, "fixtures", "venv");
58

@@ -51,6 +54,61 @@ describe("detectVirtualEnv", () => {
5154
});
5255
});
5356

57+
describe("detect PipEnv", () => {
58+
const spawn = mockSpawn()
59+
child_process.spawn = spawn
60+
spawn.sequence.add(function (cb) {
61+
this.emit('error', new Error('spawn ENOENT'));
62+
setTimeout(function() { return cb(8); }, 10);
63+
});
64+
it("with no pipenv", () => {
65+
waitsForPromise(() => {
66+
return detectPipEnv("/home/mock_pipenv").then(venv => {
67+
expect(venv).toBeNull();
68+
assert.equal('pipenv', spawn.calls[0].command);
69+
assert.deepEqual(['--venv'], spawn.calls[0].args);
70+
});
71+
});
72+
});
73+
74+
it("with Unix pipenv", () => {
75+
spawn.sequence.add(spawn.simple(1, '/home/tvallois/.local/share/virtualenvs/unix-XZE001N_'));
76+
waitsForPromise(() => {
77+
return detectPipEnv("/home/mock_pipenv").then(venv => {
78+
expect(venv).toBe('/home/tvallois/.local/share/virtualenvs/unix-XZE001N_');
79+
assert.equal('pipenv', spawn.calls[1].command);
80+
assert.deepEqual(['--venv'], spawn.calls[1].args);
81+
});
82+
});
83+
});
84+
85+
it("with Windows pipenv", () => {
86+
spawn.sequence.add(spawn.simple(1, 'C:\\Program Files\\tvallois\\virtualenvs\\windows-XZE001N_'));
87+
waitsForPromise(() => {
88+
return detectPipEnv("C:\\Program Files\\mock_pipenv").then(venv => {
89+
expect(venv).toBe('C:\\Program Files\\tvallois\\virtualenvs\\windows-XZE001N_');
90+
assert.equal('pipenv', spawn.calls[2].command);
91+
assert.deepEqual(['--venv'], spawn.calls[2].args);
92+
});
93+
});
94+
});
95+
});
96+
97+
describe("replacePipEnvPathVar", () => {
98+
it("replace $PIPENV_PATH", () => {
99+
expect(replacePipEnvPathVar("$PIPENV_PATH/bin/python",
100+
"/home/tvallois/.local/share/virtualenvs/unix-XZE001N_"))
101+
.toEqual("/home/tvallois/.local/share/virtualenvs/unix-XZE001N_/bin/python");
102+
});
103+
104+
it("no $PIPENV_PATH", () => {
105+
expect(replacePipEnvPathVar("python",
106+
"/home/tvallois/.local/share/virtualenvs/unix-XZE001N_"))
107+
.toEqual("python");
108+
});
109+
110+
});
111+
54112
describe("sanitizeConfig", () => {
55113
it("converts 'null' to null", () => {
56114
const config = {

0 commit comments

Comments
 (0)