Skip to content

Commit 1040f3c

Browse files
authored
Use stdin if workspace has large number of requirements (#21988)
Closes #21480
1 parent 221b769 commit 1040f3c

File tree

4 files changed

+253
-14
lines changed

4 files changed

+253
-14
lines changed

pythonFiles/create_venv.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import argparse
55
import importlib.util as import_util
6+
import json
67
import os
78
import pathlib
89
import subprocess
@@ -56,6 +57,12 @@ def parse_args(argv: Sequence[str]) -> argparse.Namespace:
5657
metavar="NAME",
5758
action="store",
5859
)
60+
parser.add_argument(
61+
"--stdin",
62+
action="store_true",
63+
default=False,
64+
help="Read arguments from stdin.",
65+
)
5966
return parser.parse_args(argv)
6067

6168

@@ -152,6 +159,16 @@ def install_pip(name: str):
152159
)
153160

154161

162+
def get_requirements_from_args(args: argparse.Namespace) -> List[str]:
163+
requirements = []
164+
if args.stdin:
165+
data = json.loads(sys.stdin.read())
166+
requirements = data.get("requirements", [])
167+
if args.requirements:
168+
requirements.extend(args.requirements)
169+
return requirements
170+
171+
155172
def main(argv: Optional[Sequence[str]] = None) -> None:
156173
if argv is None:
157174
argv = []
@@ -223,9 +240,10 @@ def main(argv: Optional[Sequence[str]] = None) -> None:
223240
print(f"VENV_INSTALLING_PYPROJECT: {args.toml}")
224241
install_toml(venv_path, args.extras)
225242

226-
if args.requirements:
227-
print(f"VENV_INSTALLING_REQUIREMENTS: {args.requirements}")
228-
install_requirements(venv_path, args.requirements)
243+
requirements = get_requirements_from_args(args)
244+
if requirements:
245+
print(f"VENV_INSTALLING_REQUIREMENTS: {requirements}")
246+
install_requirements(venv_path, requirements)
229247

230248

231249
if __name__ == "__main__":

pythonFiles/tests/test_create_venv.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
# Copyright (c) Microsoft Corporation. All rights reserved.
22
# Licensed under the MIT License.
33

4+
import argparse
5+
import contextlib
46
import importlib
7+
import io
8+
import json
59
import os
610
import sys
711

@@ -224,3 +228,50 @@ def run_process(args, error_message):
224228

225229
create_venv.run_process = run_process
226230
create_venv.main([])
231+
232+
233+
@contextlib.contextmanager
234+
def redirect_io(stream: str, new_stream):
235+
"""Redirect stdio streams to a custom stream."""
236+
old_stream = getattr(sys, stream)
237+
setattr(sys, stream, new_stream)
238+
yield
239+
setattr(sys, stream, old_stream)
240+
241+
242+
class CustomIO(io.TextIOWrapper):
243+
"""Custom stream object to replace stdio."""
244+
245+
name: str = "customio"
246+
247+
def __init__(self, name: str, encoding="utf-8", newline=None):
248+
self._buffer = io.BytesIO()
249+
self._buffer.name = name
250+
super().__init__(self._buffer, encoding=encoding, newline=newline)
251+
252+
def close(self):
253+
"""Provide this close method which is used by some tools."""
254+
# This is intentionally empty.
255+
256+
def get_value(self) -> str:
257+
"""Returns value from the buffer as string."""
258+
self.seek(0)
259+
return self.read()
260+
261+
262+
def test_requirements_from_stdin():
263+
importlib.reload(create_venv)
264+
265+
cli_requirements = [f"cli-requirement{i}.txt" for i in range(3)]
266+
args = argparse.Namespace()
267+
args.__dict__.update({"stdin": True, "requirements": cli_requirements})
268+
269+
stdin_requirements = [f"stdin-requirement{i}.txt" for i in range(20)]
270+
text = json.dumps({"requirements": stdin_requirements})
271+
str_input = CustomIO("<stdin>", encoding="utf-8", newline="\n")
272+
with redirect_io("stdin", str_input):
273+
str_input.write(text)
274+
str_input.seek(0)
275+
actual = create_venv.get_requirements_from_args(args)
276+
277+
assert actual == stdin_requirements + cli_requirements

src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,14 @@ import {
3232
CreateEnvironmentResult,
3333
} from '../proposed.createEnvApis';
3434

35-
function generateCommandArgs(installInfo?: IPackageInstallSelection[], addGitIgnore?: boolean): string[] {
35+
interface IVenvCommandArgs {
36+
argv: string[];
37+
stdin: string | undefined;
38+
}
39+
40+
function generateCommandArgs(installInfo?: IPackageInstallSelection[], addGitIgnore?: boolean): IVenvCommandArgs {
3641
const command: string[] = [createVenvScript()];
42+
let stdin: string | undefined;
3743

3844
if (addGitIgnore) {
3945
command.push('--git-ignore');
@@ -52,14 +58,21 @@ function generateCommandArgs(installInfo?: IPackageInstallSelection[], addGitIgn
5258
});
5359

5460
const requirements = installInfo.filter((i) => i.installType === 'requirements').map((i) => i.installItem);
55-
requirements.forEach((r) => {
56-
if (r) {
57-
command.push('--requirements', r);
58-
}
59-
});
61+
62+
if (requirements.length < 10) {
63+
requirements.forEach((r) => {
64+
if (r) {
65+
command.push('--requirements', r);
66+
}
67+
});
68+
} else {
69+
command.push('--stdin');
70+
// Too many requirements can cause the command line to be too long error.
71+
stdin = JSON.stringify({ requirements });
72+
}
6073
}
6174

62-
return command;
75+
return { argv: command, stdin };
6376
}
6477

6578
function getVenvFromOutput(output: string): string | undefined {
@@ -81,7 +94,7 @@ function getVenvFromOutput(output: string): string | undefined {
8194
async function createVenv(
8295
workspace: WorkspaceFolder,
8396
command: string,
84-
args: string[],
97+
args: IVenvCommandArgs,
8598
progress: CreateEnvironmentProgress,
8699
token?: CancellationToken,
87100
): Promise<string | undefined> {
@@ -94,11 +107,15 @@ async function createVenv(
94107
});
95108

96109
const deferred = createDeferred<string | undefined>();
97-
traceLog('Running Env creation script: ', [command, ...args]);
98-
const { proc, out, dispose } = execObservable(command, args, {
110+
traceLog('Running Env creation script: ', [command, ...args.argv]);
111+
if (args.stdin) {
112+
traceLog('Requirements passed in via stdin: ', args.stdin);
113+
}
114+
const { proc, out, dispose } = execObservable(command, args.argv, {
99115
mergeStdOutErr: true,
100116
token,
101117
cwd: workspace.uri.fsPath,
118+
stdinStr: args.stdin,
102119
});
103120

104121
const progressAndTelemetry = new VenvProgressAndTelemetry(progress);

src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts

Lines changed: 154 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import * as rawProcessApis from '../../../../client/common/process/rawProcessApi
1515
import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils';
1616
import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants';
1717
import { createDeferred } from '../../../../client/common/utils/async';
18-
import { Output } from '../../../../client/common/process/types';
18+
import { Output, SpawnOptions } from '../../../../client/common/process/types';
1919
import { VENV_CREATED_MARKER } from '../../../../client/pythonEnvironments/creation/provider/venvProgressAndTelemetry';
2020
import { CreateEnv } from '../../../../client/common/utils/localize';
2121
import * as venvUtils from '../../../../client/pythonEnvironments/creation/provider/venvUtils';
@@ -394,4 +394,157 @@ suite('venv Creation provider tests', () => {
394394
assert.isTrue(showErrorMessageWithLogsStub.notCalled);
395395
assert.isTrue(deleteEnvironmentStub.notCalled);
396396
});
397+
398+
test('Create venv with 1000 requirement files', async () => {
399+
pickWorkspaceFolderStub.resolves(workspace1);
400+
401+
interpreterQuickPick
402+
.setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny()))
403+
.returns(() => Promise.resolve('/usr/bin/python'))
404+
.verifiable(typemoq.Times.once());
405+
406+
const requirements = Array.from({ length: 1000 }, (_, i) => ({
407+
installType: 'requirements',
408+
installItem: `requirements${i}.txt`,
409+
}));
410+
pickPackagesToInstallStub.resolves(requirements);
411+
const expected = JSON.stringify({ requirements: requirements.map((r) => r.installItem) });
412+
413+
const deferred = createDeferred();
414+
let _next: undefined | ((value: Output<string>) => void);
415+
let _complete: undefined | (() => void);
416+
let stdin: undefined | string;
417+
let hasStdinArg = false;
418+
execObservableStub.callsFake((_c, argv: string[], options) => {
419+
stdin = options?.stdinStr;
420+
hasStdinArg = argv.includes('--stdin');
421+
deferred.resolve();
422+
return {
423+
proc: {
424+
exitCode: 0,
425+
},
426+
out: {
427+
subscribe: (
428+
next?: (value: Output<string>) => void,
429+
_error?: (error: unknown) => void,
430+
complete?: () => void,
431+
) => {
432+
_next = next;
433+
_complete = complete;
434+
},
435+
},
436+
dispose: () => undefined,
437+
};
438+
});
439+
440+
progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once());
441+
442+
withProgressStub.callsFake(
443+
(
444+
_options: ProgressOptions,
445+
task: (
446+
progress: CreateEnvironmentProgress,
447+
token?: CancellationToken,
448+
) => Thenable<CreateEnvironmentResult>,
449+
) => task(progressMock.object),
450+
);
451+
452+
const promise = venvProvider.createEnvironment();
453+
await deferred.promise;
454+
assert.isDefined(_next);
455+
assert.isDefined(_complete);
456+
457+
_next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' });
458+
_complete!();
459+
460+
const actual = await promise;
461+
assert.deepStrictEqual(actual, {
462+
path: 'new_environment',
463+
workspaceFolder: workspace1,
464+
});
465+
interpreterQuickPick.verifyAll();
466+
progressMock.verifyAll();
467+
assert.isTrue(showErrorMessageWithLogsStub.notCalled);
468+
assert.isTrue(deleteEnvironmentStub.notCalled);
469+
assert.strictEqual(stdin, expected);
470+
assert.isTrue(hasStdinArg);
471+
});
472+
473+
test('Create venv with 5 requirement files', async () => {
474+
pickWorkspaceFolderStub.resolves(workspace1);
475+
476+
interpreterQuickPick
477+
.setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny()))
478+
.returns(() => Promise.resolve('/usr/bin/python'))
479+
.verifiable(typemoq.Times.once());
480+
481+
const requirements = Array.from({ length: 5 }, (_, i) => ({
482+
installType: 'requirements',
483+
installItem: `requirements${i}.txt`,
484+
}));
485+
pickPackagesToInstallStub.resolves(requirements);
486+
const expectedRequirements = requirements.map((r) => r.installItem).sort();
487+
488+
const deferred = createDeferred();
489+
let _next: undefined | ((value: Output<string>) => void);
490+
let _complete: undefined | (() => void);
491+
let stdin: undefined | string;
492+
let hasStdinArg = false;
493+
let actualRequirements: string[] = [];
494+
execObservableStub.callsFake((_c, argv: string[], options: SpawnOptions) => {
495+
stdin = options?.stdinStr;
496+
actualRequirements = argv.filter((arg) => arg.startsWith('requirements')).sort();
497+
hasStdinArg = argv.includes('--stdin');
498+
deferred.resolve();
499+
return {
500+
proc: {
501+
exitCode: 0,
502+
},
503+
out: {
504+
subscribe: (
505+
next?: (value: Output<string>) => void,
506+
_error?: (error: unknown) => void,
507+
complete?: () => void,
508+
) => {
509+
_next = next;
510+
_complete = complete;
511+
},
512+
},
513+
dispose: () => undefined,
514+
};
515+
});
516+
517+
progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once());
518+
519+
withProgressStub.callsFake(
520+
(
521+
_options: ProgressOptions,
522+
task: (
523+
progress: CreateEnvironmentProgress,
524+
token?: CancellationToken,
525+
) => Thenable<CreateEnvironmentResult>,
526+
) => task(progressMock.object),
527+
);
528+
529+
const promise = venvProvider.createEnvironment();
530+
await deferred.promise;
531+
assert.isDefined(_next);
532+
assert.isDefined(_complete);
533+
534+
_next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' });
535+
_complete!();
536+
537+
const actual = await promise;
538+
assert.deepStrictEqual(actual, {
539+
path: 'new_environment',
540+
workspaceFolder: workspace1,
541+
});
542+
interpreterQuickPick.verifyAll();
543+
progressMock.verifyAll();
544+
assert.isTrue(showErrorMessageWithLogsStub.notCalled);
545+
assert.isTrue(deleteEnvironmentStub.notCalled);
546+
assert.isUndefined(stdin);
547+
assert.deepStrictEqual(actualRequirements, expectedRequirements);
548+
assert.isFalse(hasStdinArg);
549+
});
397550
});

0 commit comments

Comments
 (0)