Skip to content

Commit 461fb79

Browse files
authored
feat(vscode): .env file into python env (#4764)
1 parent 8018d7a commit 461fb79

File tree

5 files changed

+240
-35
lines changed

5 files changed

+240
-35
lines changed

vscode/extension/src/utilities/common/python.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { commands, Disposable, Event, EventEmitter, Uri } from 'vscode'
55
import { traceError, traceLog } from './log'
66
import { PythonExtension, ResolvedEnvironment } from '@vscode/python-extension'
77
import path from 'path'
8+
import { err, ok, Result } from '@bus/result'
9+
import * as vscode from 'vscode'
810

911
export interface IInterpreterDetails {
1012
path?: string[]
@@ -15,10 +17,12 @@ export interface IInterpreterDetails {
1517

1618
const onDidChangePythonInterpreterEvent =
1719
new EventEmitter<IInterpreterDetails>()
20+
1821
export const onDidChangePythonInterpreter: Event<IInterpreterDetails> =
1922
onDidChangePythonInterpreterEvent.event
2023

2124
let _api: PythonExtension | undefined
25+
2226
async function getPythonExtensionAPI(): Promise<PythonExtension | undefined> {
2327
if (_api) {
2428
return _api
@@ -118,3 +122,34 @@ export function checkVersion(
118122
traceError('Supported versions are 3.8 and above.')
119123
return false
120124
}
125+
126+
/**
127+
* getPythonEnvVariables returns the environment variables for the current python interpreter.
128+
*
129+
* @returns The environment variables for the current python interpreter.
130+
*/
131+
export async function getPythonEnvVariables(): Promise<
132+
Result<Record<string, string>, string>
133+
> {
134+
const api = await getPythonExtensionAPI()
135+
if (!api) {
136+
return err('Python extension API not found')
137+
}
138+
139+
const workspaces = vscode.workspace.workspaceFolders
140+
if (!workspaces) {
141+
return ok({})
142+
}
143+
const out: Record<string, string> = {}
144+
for (const workspace of workspaces) {
145+
const envVariables = api.environments.getEnvironmentVariables(workspace.uri)
146+
if (envVariables) {
147+
for (const [key, value] of Object.entries(envVariables)) {
148+
if (value) {
149+
out[key] = value
150+
}
151+
}
152+
}
153+
}
154+
return ok(out)
155+
}

vscode/extension/src/utilities/sqlmesh/sqlmesh.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import path from 'path'
22
import { traceInfo, traceLog, traceVerbose } from '../common/log'
3-
import { getInterpreterDetails } from '../common/python'
3+
import { getInterpreterDetails, getPythonEnvVariables } from '../common/python'
44
import { Result, err, isErr, ok } from '@bus/result'
55
import { getProjectRoot } from '../common/utilities'
66
import { isPythonModuleInstalled } from '../python'
@@ -230,6 +230,13 @@ export const sqlmeshExec = async (): Promise<
230230
message: resolvedPath.error,
231231
})
232232
}
233+
const envVariables = await getPythonEnvVariables()
234+
if (isErr(envVariables)) {
235+
return err({
236+
type: 'generic',
237+
message: envVariables.error,
238+
})
239+
}
233240
const workspacePath = resolvedPath.value
234241
const interpreterDetails = await getInterpreterDetails()
235242
traceLog(`Interpreter details: ${JSON.stringify(interpreterDetails)}`)
@@ -268,6 +275,8 @@ export const sqlmeshExec = async (): Promise<
268275
bin: `${tcloudBin.value} sqlmesh`,
269276
workspacePath,
270277
env: {
278+
...process.env,
279+
...envVariables.value,
271280
PYTHONPATH: interpreterDetails.path?.[0],
272281
VIRTUAL_ENV: path.dirname(interpreterDetails.binPath!),
273282
PATH: interpreterDetails.binPath!,
@@ -281,6 +290,8 @@ export const sqlmeshExec = async (): Promise<
281290
bin: binPath,
282291
workspacePath,
283292
env: {
293+
...process.env,
294+
...envVariables.value,
284295
PYTHONPATH: interpreterDetails.path?.[0],
285296
VIRTUAL_ENV: path.dirname(path.dirname(interpreterDetails.binPath!)), // binPath now points to bin dir
286297
PATH: interpreterDetails.binPath!,
@@ -297,7 +308,10 @@ export const sqlmeshExec = async (): Promise<
297308
return ok({
298309
bin: sqlmesh,
299310
workspacePath,
300-
env: {},
311+
env: {
312+
...process.env,
313+
...envVariables.value,
314+
},
301315
args: [],
302316
})
303317
}
@@ -353,6 +367,13 @@ export const sqlmeshLspExec = async (): Promise<
353367
> => {
354368
const sqlmeshLSP = IS_WINDOWS ? 'sqlmesh_lsp.exe' : 'sqlmesh_lsp'
355369
const projectRoot = await getProjectRoot()
370+
const envVariables = await getPythonEnvVariables()
371+
if (isErr(envVariables)) {
372+
return err({
373+
type: 'generic',
374+
message: envVariables.error,
375+
})
376+
}
356377
const resolvedPath = resolveProjectPath(projectRoot)
357378
if (isErr(resolvedPath)) {
358379
return err({
@@ -408,6 +429,8 @@ export const sqlmeshLspExec = async (): Promise<
408429
PYTHONPATH: interpreterDetails.path?.[0],
409430
VIRTUAL_ENV: path.dirname(interpreterDetails.binPath!),
410431
PATH: interpreterDetails.binPath!,
432+
...process.env,
433+
...envVariables.value,
411434
},
412435
args: ['sqlmesh_lsp'],
413436
})
@@ -431,6 +454,8 @@ export const sqlmeshLspExec = async (): Promise<
431454
PYTHONPATH: interpreterDetails.path?.[0],
432455
VIRTUAL_ENV: path.dirname(path.dirname(interpreterDetails.binPath!)), // binPath now points to bin dir
433456
PATH: interpreterDetails.binPath!, // binPath already points to the bin directory
457+
...process.env,
458+
...envVariables.value,
434459
},
435460
args: [],
436461
})
@@ -444,7 +469,10 @@ export const sqlmeshLspExec = async (): Promise<
444469
return ok({
445470
bin: sqlmeshLSP,
446471
workspacePath,
447-
env: {},
472+
env: {
473+
...process.env,
474+
...envVariables.value,
475+
},
448476
args: [],
449477
})
450478
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { test } from '@playwright/test'
2+
import fs from 'fs-extra'
3+
import {
4+
createVirtualEnvironment,
5+
openLineageView,
6+
pipInstall,
7+
PythonEnvironment,
8+
REPO_ROOT,
9+
startVSCode,
10+
SUSHI_SOURCE_PATH,
11+
} from './utils'
12+
import os from 'os'
13+
import path from 'path'
14+
import { setTcloudVersion, setupAuthenticatedState } from './tcloud_utils'
15+
16+
function writeEnvironmentConfig(sushiPath: string) {
17+
const configPath = path.join(sushiPath, 'config.py')
18+
const originalConfig = fs.readFileSync(configPath, 'utf8')
19+
20+
const newConfig =
21+
`
22+
import os
23+
24+
test_var = os.getenv("TEST_VAR")
25+
if test_var is None or test_var == "":
26+
raise Exception("TEST_VAR is not set")
27+
` + originalConfig
28+
29+
fs.writeFileSync(configPath, newConfig)
30+
}
31+
32+
async function setupEnvironment(): Promise<[string, PythonEnvironment]> {
33+
const tempDir = await fs.mkdtemp(
34+
path.join(os.tmpdir(), 'vscode-test-tcloud-'),
35+
)
36+
await fs.copy(SUSHI_SOURCE_PATH, tempDir)
37+
const pythonEnvDir = path.join(tempDir, '.venv')
38+
const pythonDetails = await createVirtualEnvironment(pythonEnvDir)
39+
const custom_materializations = path.join(
40+
REPO_ROOT,
41+
'examples',
42+
'custom_materializations',
43+
)
44+
const sqlmeshWithExtras = `${REPO_ROOT}[bigquery,lsp]`
45+
await pipInstall(pythonDetails, [sqlmeshWithExtras, custom_materializations])
46+
47+
const settings = {
48+
'python.defaultInterpreterPath': pythonDetails.pythonPath,
49+
'sqlmesh.environmentPath': pythonEnvDir,
50+
}
51+
await fs.ensureDir(path.join(tempDir, '.vscode'))
52+
await fs.writeJson(path.join(tempDir, '.vscode', 'settings.json'), settings, {
53+
spaces: 2,
54+
})
55+
56+
return [tempDir, pythonDetails]
57+
}
58+
59+
test.describe('python environment variable injection on sqlmesh_lsp', () => {
60+
test('normal setup - error ', async () => {
61+
const [tempDir, _] = await setupEnvironment()
62+
writeEnvironmentConfig(tempDir)
63+
const { window, close } = await startVSCode(tempDir)
64+
try {
65+
await openLineageView(window)
66+
await window.waitForSelector('text=Error creating context')
67+
} finally {
68+
await close()
69+
}
70+
})
71+
72+
test('normal setup - set', async () => {
73+
const [tempDir, _] = await setupEnvironment()
74+
writeEnvironmentConfig(tempDir)
75+
const env_file = path.join(tempDir, '.env')
76+
fs.writeFileSync(env_file, 'TEST_VAR=test_value')
77+
const { window, close } = await startVSCode(tempDir)
78+
try {
79+
await openLineageView(window)
80+
await window.waitForSelector('text=Loaded SQLMesh context')
81+
} finally {
82+
await close()
83+
}
84+
})
85+
})
86+
87+
async function setupTcloudProject(
88+
tempDir: string,
89+
pythonDetails: PythonEnvironment,
90+
) {
91+
// Install the mock tcloud package
92+
const mockTcloudPath = path.join(__dirname, 'tcloud')
93+
await pipInstall(pythonDetails, [mockTcloudPath])
94+
95+
// Create a tcloud.yaml to mark this as a tcloud project
96+
const tcloudConfig = {
97+
url: 'https://mock.tobikodata.com',
98+
org: 'test-org',
99+
project: 'test-project',
100+
}
101+
await fs.writeFile(
102+
path.join(tempDir, 'tcloud.yaml'),
103+
`url: ${tcloudConfig.url}\norg: ${tcloudConfig.org}\nproject: ${tcloudConfig.project}\n`,
104+
)
105+
// Write mock ".tcloud_auth_state.json" file
106+
await setupAuthenticatedState(tempDir)
107+
// Set tcloud version to 2.10.1
108+
await setTcloudVersion(tempDir, '2.10.1')
109+
}
110+
111+
test.describe('tcloud version', () => {
112+
test('normal setup - error ', async () => {
113+
const [tempDir, pythonDetails] = await setupEnvironment()
114+
await setupTcloudProject(tempDir, pythonDetails)
115+
writeEnvironmentConfig(tempDir)
116+
const { window, close } = await startVSCode(tempDir)
117+
try {
118+
await openLineageView(window)
119+
await window.waitForSelector('text=Error creating context')
120+
} finally {
121+
await close()
122+
}
123+
})
124+
125+
test('normal setup - set', async () => {
126+
const [tempDir, pythonDetails] = await setupEnvironment()
127+
await setupTcloudProject(tempDir, pythonDetails)
128+
writeEnvironmentConfig(tempDir)
129+
const env_file = path.join(tempDir, '.env')
130+
fs.writeFileSync(env_file, 'TEST_VAR=test_value')
131+
const { window, close } = await startVSCode(tempDir)
132+
try {
133+
await openLineageView(window)
134+
await window.waitForSelector('text=Loaded SQLMesh context')
135+
} finally {
136+
await close()
137+
}
138+
})
139+
})

vscode/extension/tests/tcloud.spec.ts

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
startVSCode,
1010
SUSHI_SOURCE_PATH,
1111
} from './utils'
12+
import { setTcloudVersion, setupAuthenticatedState } from './tcloud_utils'
1213

1314
/**
1415
* Helper function to create and set up a Python virtual environment
@@ -33,38 +34,6 @@ async function setupPythonEnvironment(envDir: string): Promise<string> {
3334
return pythonDetails.pythonPath
3435
}
3536

36-
/**
37-
* Helper function to set up a pre-authenticated tcloud state
38-
*/
39-
async function setupAuthenticatedState(tempDir: string): Promise<void> {
40-
const authStateFile = path.join(tempDir, '.tcloud_auth_state.json')
41-
const authState = {
42-
is_logged_in: true,
43-
id_token: {
44-
iss: 'https://mock.tobikodata.com',
45-
aud: 'mock-audience',
46-
sub: 'user-123',
47-
scope: 'openid email profile',
48-
iat: Math.floor(Date.now() / 1000),
49-
exp: Math.floor(Date.now() / 1000) + 3600, // Valid for 1 hour
50-
email: 'test@example.com',
51-
name: 'Test User',
52-
},
53-
}
54-
await fs.writeJson(authStateFile, authState)
55-
}
56-
57-
/**
58-
* Helper function to set the tcloud version for testing
59-
*/
60-
async function setTcloudVersion(
61-
tempDir: string,
62-
version: string,
63-
): Promise<void> {
64-
const versionStateFile = path.join(tempDir, '.tcloud_version_state.json')
65-
await fs.writeJson(versionStateFile, { version })
66-
}
67-
6837
test.describe('Tcloud', () => {
6938
test('not signed in, shows sign in window', async ({}, testInfo) => {
7039
testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import path from 'path'
2+
import fs from 'fs-extra'
3+
4+
/**
5+
* Helper function to set up a pre-authenticated tcloud state
6+
*/
7+
export async function setupAuthenticatedState(tempDir: string): Promise<void> {
8+
const authStateFile = path.join(tempDir, '.tcloud_auth_state.json')
9+
const authState = {
10+
is_logged_in: true,
11+
id_token: {
12+
iss: 'https://mock.tobikodata.com',
13+
aud: 'mock-audience',
14+
sub: 'user-123',
15+
scope: 'openid email profile',
16+
iat: Math.floor(Date.now() / 1000),
17+
exp: Math.floor(Date.now() / 1000) + 3600, // Valid for 1 hour
18+
email: 'test@example.com',
19+
name: 'Test User',
20+
},
21+
}
22+
await fs.writeJson(authStateFile, authState)
23+
}
24+
25+
/**
26+
* Helper function to set the tcloud version for testing
27+
*/
28+
export async function setTcloudVersion(
29+
tempDir: string,
30+
version: string,
31+
): Promise<void> {
32+
const versionStateFile = path.join(tempDir, '.tcloud_version_state.json')
33+
await fs.writeJson(versionStateFile, { version })
34+
}

0 commit comments

Comments
 (0)