Skip to content

Commit 860a59a

Browse files
authored
Add PYTHONPATH to environment variables if the Python extension makes it available (#4045)
1 parent 5163f0c commit 860a59a

File tree

19 files changed

+194
-50
lines changed

19 files changed

+194
-50
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"MNIST",
3939
"nfiles",
4040
"pseudoterminal",
41+
"PYTHONPATH",
4142
"rwlock",
4243
"sonarjs",
4344
"spyable",

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,13 +160,22 @@ These are the VS Code [settings] available for the Extension:
160160
> **Note** that the `Setup The Workspace` command helps you set up the basic
161161
> ones at the [Workspace level] (saved to `.vscode/setting.json`).
162162
163+
### Python
164+
165+
This extension is integrated with Microsoft's [Python extension]. When possible,
166+
the Python extension's selected interpreter will be used to locate DVC. The
167+
`PYTHONPATH` environment variable identified via the [python.envFile] config
168+
setting is also respected.
169+
163170
[python extension]:
164171
https://marketplace.visualstudio.com/items?itemName=ms-python.python
165172
[studio.token]:
166173
https://dvc.org/doc/user-guide/project-structure/configuration#studio
167174
[Studio]: https://studio.iterative.ai
168175
[workspace level]:
169176
https://code.visualstudio.com/docs/getstarted/settings#_workspace-settings
177+
[python.envFile]:
178+
https://code.visualstudio.com/docs/python/environments#_use-of-the-pythonpath-variable
170179

171180
## Debugging
172181

extension/src/cli/dvc/config.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ describe('DvcConfig', () => {
4242
const dvcConfig = new DvcConfig(
4343
{
4444
getCliPath: () => undefined,
45+
getPYTHONPATH: () => undefined,
4546
getPythonBinPath: () => undefined
4647
} as unknown as Config,
4748
{

extension/src/cli/dvc/executor.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ describe('CliExecutor', () => {
4848
const dvcExecutor = new DvcExecutor(
4949
{
5050
getCliPath: () => undefined,
51+
getPYTHONPATH: () => undefined,
5152
getPythonBinPath: () => undefined
5253
} as unknown as Config,
5354
{

extension/src/cli/dvc/index.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ describe('executeDvcProcess', () => {
6666
const cli = new DvcCli(
6767
{
6868
getCliPath: () => undefined,
69+
getPYTHONPATH: () => undefined,
6970
getPythonBinPath: () => undefined
7071
} as unknown as Config,
7172
{
@@ -111,6 +112,7 @@ describe('executeDvcProcess', () => {
111112
const cli = new DvcCli(
112113
{
113114
getCliPath: () => '/some/path/to/dvc',
115+
getPYTHONPATH: () => undefined,
114116
getPythonBinPath: () => pythonBinPath
115117
} as unknown as Config,
116118
{

extension/src/cli/dvc/index.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,12 @@ export class DvcCli extends Cli {
3535
}
3636

3737
protected getOptions(cwd: string, ...args: Args) {
38-
return getOptions(
39-
this.extensionConfig.getPythonBinPath(),
40-
this.extensionConfig.getCliPath(),
38+
return getOptions({
39+
PYTHONPATH: this.extensionConfig.getPYTHONPATH(),
40+
args: [...args],
41+
cliPath: this.extensionConfig.getCliPath(),
4142
cwd,
42-
...args
43-
)
43+
pythonBinPath: this.extensionConfig.getPythonBinPath()
44+
})
4445
}
4546
}

extension/src/cli/dvc/options.test.ts

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,13 @@ describe('getOptions', () => {
2525
const cwd = join('path', 'to', 'work', 'dir')
2626

2727
it('should give the correct options given a basic environment', () => {
28-
const options = getOptions(undefined, '', cwd, Command.CHECKOUT, Flag.FORCE)
28+
const options = getOptions({
29+
PYTHONPATH: undefined,
30+
args: [Command.CHECKOUT, Flag.FORCE],
31+
cliPath: '',
32+
cwd,
33+
pythonBinPath: undefined
34+
})
2935
expect(options).toStrictEqual({
3036
args: ['checkout', '-f'],
3137
cwd,
@@ -36,7 +42,13 @@ describe('getOptions', () => {
3642

3743
it('should append -m dvc to the args and use the python as the executable if only an isolated python env is in use', () => {
3844
const pythonBinPath = join('path', 'to', 'python', '.venv', 'python')
39-
const options = getOptions(pythonBinPath, '', cwd, Command.PULL)
45+
const options = getOptions({
46+
PYTHONPATH: undefined,
47+
args: [Command.PULL],
48+
cliPath: '',
49+
cwd,
50+
pythonBinPath
51+
})
4052
expect(options).toStrictEqual({
4153
args: ['-m', 'dvc', 'pull'],
4254
cwd,
@@ -53,7 +65,13 @@ describe('getOptions', () => {
5365
it('should append to the PATH but only use the path to the cli if both an isolated python env and path to dvc are in use', () => {
5466
const pythonBinPath = join('path', 'to', 'python', '.venv', 'python')
5567
const cliPath = join('custom', 'path', 'to', 'dvc')
56-
const options = getOptions(pythonBinPath, cliPath, cwd, Command.PULL)
68+
const options = getOptions({
69+
PYTHONPATH: undefined,
70+
args: [Command.PULL],
71+
cliPath,
72+
cwd,
73+
pythonBinPath
74+
})
5775
expect(options).toStrictEqual({
5876
args: ['pull'],
5977
cwd,
@@ -66,4 +84,30 @@ describe('getOptions', () => {
6684
executable: cliPath
6785
})
6886
})
87+
88+
it('should add PYTHONPATH to the environment variables if it is provided', () => {
89+
const PYTHONPATH = join('path', 'to', 'project')
90+
const venvPath = join(PYTHONPATH, '.venv')
91+
const pythonBinPath = join(venvPath, 'python')
92+
const cliPath = ''
93+
const options = getOptions({
94+
PYTHONPATH,
95+
args: [Command.PULL],
96+
cliPath,
97+
cwd,
98+
pythonBinPath
99+
})
100+
expect(options).toStrictEqual({
101+
args: ['-m', 'dvc', 'pull'],
102+
cwd,
103+
env: {
104+
DVCLIVE_OPEN: 'false',
105+
DVC_NO_ANALYTICS: 'true',
106+
GIT_TERMINAL_PROMPT: '0',
107+
PATH: joinEnvPath(venvPath, mockedPATH),
108+
PYTHONPATH
109+
},
110+
executable: pythonBinPath
111+
})
112+
})
69113
})

extension/src/cli/dvc/options.ts

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,23 @@ const getPATH = (existingPath: string, pythonBinPath?: string): string => {
1414
return joinEnvPath(python, existingPath)
1515
}
1616

17-
const getEnv = (pythonBinPath?: string): NodeJS.ProcessEnv => {
18-
const env = getProcessEnv()
19-
const PATH = getPATH(env?.PATH as string, pythonBinPath)
20-
return {
21-
...env,
17+
const getEnv = (
18+
pythonBinPath: string | undefined,
19+
PYTHONPATH: string | undefined
20+
): NodeJS.ProcessEnv => {
21+
const existingEnv = getProcessEnv()
22+
const PATH = getPATH(existingEnv?.PATH as string, pythonBinPath)
23+
const env: NodeJS.ProcessEnv = {
24+
...existingEnv,
2225
DVCLIVE_OPEN: 'false',
2326
DVC_NO_ANALYTICS: 'true',
2427
GIT_TERMINAL_PROMPT: '0',
2528
PATH
2629
}
30+
if (PYTHONPATH) {
31+
env.PYTHONPATH = PYTHONPATH
32+
}
33+
return env
2734
}
2835

2936
const getArgs = (
@@ -35,18 +42,24 @@ const getArgs = (
3542
const getExecutable = (pythonBinPath: string | undefined, cliPath: string) =>
3643
cliPath || pythonBinPath || 'dvc'
3744

38-
export const getOptions = (
39-
pythonBinPath: string | undefined,
40-
cliPath: string,
41-
cwd: string,
42-
...originalArgs: Args
43-
): ExecutionOptions => {
45+
export const getOptions = ({
46+
args = [],
47+
cliPath,
48+
cwd,
49+
pythonBinPath,
50+
PYTHONPATH
51+
}: {
52+
args?: Args
53+
cliPath: string
54+
cwd: string
55+
pythonBinPath: string | undefined
56+
PYTHONPATH: string | undefined
57+
}): ExecutionOptions => {
4458
const executable = getExecutable(pythonBinPath, cliPath)
45-
const args = getArgs(pythonBinPath, cliPath, ...originalArgs)
4659
return {
47-
args,
60+
args: getArgs(pythonBinPath, cliPath, ...args),
4861
cwd: getCaseSensitiveCwd(cwd),
49-
env: getEnv(pythonBinPath),
62+
env: getEnv(pythonBinPath, PYTHONPATH),
5063
executable
5164
}
5265
}

extension/src/cli/dvc/reader.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import expShowFixture from '../../test/fixtures/expShow/base/output'
1313
import plotsDiffFixture from '../../test/fixtures/plotsDiff/output/minimal'
1414
import { Config } from '../../config'
1515
import { joinEnvPath } from '../../util/env'
16+
import { dvcDemoPath } from '../../test/util'
1617

1718
jest.mock('vscode')
1819
jest.mock('@hediet/std/disposable')
@@ -34,6 +35,7 @@ const mockedEnv = {
3435
const JSON_FLAG = '--json'
3536

3637
const mockedGetPythonBinPath = jest.fn()
38+
const mockedGetPYTHONPATH = jest.fn()
3739

3840
beforeEach(() => {
3941
jest.resetAllMocks()
@@ -53,6 +55,7 @@ describe('CliReader', () => {
5355
const dvcReader = new DvcReader(
5456
{
5557
getCliPath: () => undefined,
58+
getPYTHONPATH: mockedGetPYTHONPATH,
5659
getPythonBinPath: mockedGetPythonBinPath
5760
} as unknown as Config,
5861
{
@@ -311,5 +314,21 @@ describe('CliReader', () => {
311314

312315
await expect(dvcReader.version(cwd)).rejects.toBeTruthy()
313316
})
317+
318+
it('should add PYTHONPATH to the environment used to create a DVC process when it is available from the extension config', async () => {
319+
mockedGetPYTHONPATH.mockReturnValue(dvcDemoPath)
320+
const cwd = __dirname
321+
const stdout = '3.9.11'
322+
mockedCreateProcess.mockReturnValueOnce(getMockedProcess(stdout))
323+
const output = await dvcReader.version(cwd)
324+
325+
expect(output).toStrictEqual(stdout)
326+
expect(mockedCreateProcess).toHaveBeenCalledWith({
327+
args: ['--version'],
328+
cwd,
329+
env: { ...mockedEnv, PYTHONPATH: dvcDemoPath },
330+
executable: 'dvc'
331+
})
332+
})
314333
})
315334
})

extension/src/cli/dvc/reader.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,12 +124,13 @@ export class DvcReader extends DvcCli {
124124
}
125125

126126
public globalVersion(cwd: string): Promise<string> {
127-
const options = getOptions(
128-
undefined,
129-
this.extensionConfig.getCliPath(),
127+
const options = getOptions({
128+
PYTHONPATH: undefined,
129+
args: [Flag.VERSION],
130+
cliPath: this.extensionConfig.getCliPath(),
130131
cwd,
131-
Flag.VERSION
132-
)
132+
pythonBinPath: undefined
133+
})
133134

134135
return this.executeProcess(options)
135136
}

0 commit comments

Comments
 (0)