Skip to content

Commit f6bdccf

Browse files
authored
Merge branch 'main' into copilot/fix-configure-test-command
2 parents 3ee31a1 + 832a9aa commit f6bdccf

File tree

16 files changed

+210
-43
lines changed

16 files changed

+210
-43
lines changed

.github/actions/build-vsix/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ runs:
9393
VSIX_NAME: ${{ inputs.vsix_name }}
9494

9595
- name: Upload VSIX
96-
uses: actions/upload-artifact@v5
96+
uses: actions/upload-artifact@v6
9797
with:
9898
name: ${{ inputs.artifact_name }}
9999
path: ${{ inputs.vsix_name }}

.github/workflows/lock-issues.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
runs-on: ubuntu-latest
1717
steps:
1818
- name: 'Lock Issues'
19-
uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
19+
uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
2020
with:
2121
github-token: ${{ github.token }}
2222
issue-inactive-days: '30'

.github/workflows/pr-check.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -680,7 +680,7 @@ jobs:
680680
run: npm run test:cover:report
681681

682682
- name: Upload HTML report
683-
uses: actions/upload-artifact@v5
683+
uses: actions/upload-artifact@v6
684684
with:
685685
name: ${{ runner.os }}-coverage-report-html
686686
path: ./coverage

python_files/vscode_pytest/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def __init__(self, message):
7575
ERRORS = []
7676
IS_DISCOVERY = False
7777
map_id_to_path = {}
78-
collected_tests_so_far = []
78+
collected_tests_so_far = set()
7979
TEST_RUN_PIPE = os.getenv("TEST_RUN_PIPE")
8080
SYMLINK_PATH = None
8181
INCLUDE_BRANCHES = False
@@ -175,7 +175,7 @@ def pytest_exception_interact(node, call, report):
175175
report_value = "failure"
176176
node_id = get_absolute_test_id(node.nodeid, get_node_path(node))
177177
if node_id not in collected_tests_so_far:
178-
collected_tests_so_far.append(node_id)
178+
collected_tests_so_far.add(node_id)
179179
item_result = create_test_outcome(
180180
node_id,
181181
report_value,
@@ -300,7 +300,7 @@ def pytest_report_teststatus(report, config): # noqa: ARG001
300300
# Calculate the absolute test id and use this as the ID moving forward.
301301
absolute_node_id = get_absolute_test_id(report.nodeid, node_path)
302302
if absolute_node_id not in collected_tests_so_far:
303-
collected_tests_so_far.append(absolute_node_id)
303+
collected_tests_so_far.add(absolute_node_id)
304304
item_result = create_test_outcome(
305305
absolute_node_id,
306306
report_value,
@@ -334,7 +334,7 @@ def pytest_runtest_protocol(item, nextitem): # noqa: ARG001
334334
report_value = "skipped"
335335
cwd = pathlib.Path.cwd()
336336
if absolute_node_id not in collected_tests_so_far:
337-
collected_tests_so_far.append(absolute_node_id)
337+
collected_tests_so_far.add(absolute_node_id)
338338
item_result = create_test_outcome(
339339
absolute_node_id,
340340
report_value,

python_files/vscode_pytest/run_pytest_script.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,13 @@ def run_pytest(args):
5555
try:
5656
# Read the test ids from the file and run pytest.
5757
ids = ids_path.read_text(encoding="utf-8").splitlines()
58-
arg_array = ["-p", "vscode_pytest", *args, *ids]
59-
print("Running pytest with args: " + str(arg_array))
60-
pytest.main(arg_array)
6158
except Exception as e:
6259
print("Error[vscode-pytest]: unable to read testIds from temp file" + str(e))
6360
run_pytest(args)
61+
else:
62+
arg_array = ["-p", "vscode_pytest", *args, *ids]
63+
print("Running pytest with args: " + str(arg_array))
64+
pytest.main(arg_array)
6465
finally:
6566
# Delete the test ids temp file.
6667
try:

src/client/common/application/commands.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu
8787
['jupyter.opennotebook']: [undefined | Uri, undefined | CommandSource];
8888
['jupyter.runallcells']: [Uri];
8989
['extension.open']: [string];
90-
['workbench.action.openIssueReporter']: [{ extensionId: string; issueBody: string }];
90+
['workbench.action.openIssueReporter']: [{ extensionId: string; issueBody: string; extensionData?: string }];
9191
[Commands.GetSelectedInterpreterPath]: [{ workspaceFolder: string } | string[]];
9292
[Commands.TriggerEnvironmentSelection]: [undefined | Uri];
9393
[Commands.Start_Native_REPL]: [undefined | Uri];

src/client/common/application/commands/reportIssueCommand.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export class ReportIssueCommandHandler implements IExtensionSingleActivationServ
121121
await this.commandManager.executeCommand('workbench.action.openIssueReporter', {
122122
extensionId: 'ms-python.python',
123123
issueBody: template,
124-
data: userTemplate.format(
124+
extensionData: userTemplate.format(
125125
pythonVersion,
126126
virtualEnvKind,
127127
languageServer,

src/client/common/application/terminalManager.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ export class TerminalManager implements ITerminalManager {
3838
public onDidEndTerminalShellExecution(handler: (e: TerminalShellExecutionEndEvent) => void): Disposable {
3939
return window.onDidEndTerminalShellExecution(handler);
4040
}
41+
public onDidChangeTerminalState(handler: (e: Terminal) => void): Disposable {
42+
return window.onDidChangeTerminalState(handler);
43+
}
4144
}
4245

4346
/**

src/client/common/application/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -939,6 +939,8 @@ export interface ITerminalManager {
939939
onDidChangeTerminalShellIntegration(handler: (e: TerminalShellIntegrationChangeEvent) => void): Disposable;
940940

941941
onDidEndTerminalShellExecution(handler: (e: TerminalShellExecutionEndEvent) => void): Disposable;
942+
943+
onDidChangeTerminalState(handler: (e: Terminal) => void): Disposable;
942944
}
943945

944946
export const IDebugService = Symbol('IDebugManager');

src/client/common/terminal/service.ts

Lines changed: 94 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { IServiceContainer } from '../../ioc/types';
99
import { captureTelemetry } from '../../telemetry';
1010
import { EventName } from '../../telemetry/constants';
1111
import { ITerminalAutoActivation } from '../../terminals/types';
12-
import { ITerminalManager } from '../application/types';
12+
import { IApplicationShell, ITerminalManager } from '../application/types';
1313
import { _SCRIPTS_DIR } from '../process/internal/scripts/constants';
1414
import { IConfigurationService, IDisposableRegistry } from '../types';
1515
import {
@@ -20,9 +20,9 @@ import {
2020
TerminalShellType,
2121
} from './types';
2222
import { traceVerbose } from '../../logging';
23+
import { sleep } from '../utils/async';
2324
import { useEnvExtension } from '../../envExt/api.internal';
2425
import { ensureTerminalLegacy } from '../../envExt/api.legacy';
25-
import { sleep } from '../utils/async';
2626

2727
@injectable()
2828
export class TerminalService implements ITerminalService, Disposable {
@@ -33,8 +33,13 @@ export class TerminalService implements ITerminalService, Disposable {
3333
private terminalHelper: ITerminalHelper;
3434
private terminalActivator: ITerminalActivator;
3535
private terminalAutoActivator: ITerminalAutoActivation;
36+
private applicationShell: IApplicationShell;
3637
private readonly executeCommandListeners: Set<Disposable> = new Set();
3738
private _terminalFirstLaunched: boolean = true;
39+
private pythonReplCommandQueue: string[] = [];
40+
private isReplReady: boolean = false;
41+
private replPromptListener?: Disposable;
42+
private replShellTypeListener?: Disposable;
3843
public get onDidCloseTerminal(): Event<void> {
3944
return this.terminalClosed.event.bind(this.terminalClosed);
4045
}
@@ -48,11 +53,13 @@ export class TerminalService implements ITerminalService, Disposable {
4853
this.terminalHelper = this.serviceContainer.get<ITerminalHelper>(ITerminalHelper);
4954
this.terminalManager = this.serviceContainer.get<ITerminalManager>(ITerminalManager);
5055
this.terminalAutoActivator = this.serviceContainer.get<ITerminalAutoActivation>(ITerminalAutoActivation);
56+
this.applicationShell = this.serviceContainer.get<IApplicationShell>(IApplicationShell);
5157
this.terminalManager.onDidCloseTerminal(this.terminalCloseHandler, this, disposableRegistry);
5258
this.terminalActivator = this.serviceContainer.get<ITerminalActivator>(ITerminalActivator);
5359
}
5460
public dispose() {
5561
this.terminal?.dispose();
62+
this.disposeReplListener();
5663

5764
if (this.executeCommandListeners && this.executeCommandListeners.size > 0) {
5865
this.executeCommandListeners.forEach((d) => {
@@ -81,7 +88,86 @@ export class TerminalService implements ITerminalService, Disposable {
8188
commandLine: string,
8289
isPythonShell: boolean,
8390
): Promise<TerminalShellExecution | undefined> {
84-
const terminal = this.terminal!;
91+
if (isPythonShell) {
92+
if (this.isReplReady) {
93+
this.terminal?.sendText(commandLine);
94+
traceVerbose(`Python REPL sendText: ${commandLine}`);
95+
} else {
96+
// Queue command to run once REPL is ready.
97+
this.pythonReplCommandQueue.push(commandLine);
98+
traceVerbose(`Python REPL queued command: ${commandLine}`);
99+
this.startReplListener();
100+
}
101+
return undefined;
102+
}
103+
104+
// Non-REPL code execution
105+
return this.executeCommandInternal(commandLine);
106+
}
107+
108+
private startReplListener(): void {
109+
if (this.replPromptListener || this.replShellTypeListener) {
110+
return;
111+
}
112+
113+
this.replShellTypeListener = this.terminalManager.onDidChangeTerminalState((terminal) => {
114+
if (this.terminal && terminal === this.terminal) {
115+
if (terminal.state.shell == 'python') {
116+
traceVerbose('Python REPL ready from terminal shell api');
117+
this.onReplReady();
118+
}
119+
}
120+
});
121+
122+
let terminalData = '';
123+
this.replPromptListener = this.applicationShell.onDidWriteTerminalData((e) => {
124+
if (this.terminal && e.terminal === this.terminal) {
125+
terminalData += e.data;
126+
if (/>>>\s*$/.test(terminalData)) {
127+
traceVerbose('Python REPL ready, from >>> prompt detection');
128+
this.onReplReady();
129+
}
130+
}
131+
});
132+
}
133+
134+
private onReplReady(): void {
135+
if (this.isReplReady) {
136+
return;
137+
}
138+
this.isReplReady = true;
139+
this.flushReplQueue();
140+
this.disposeReplListener();
141+
}
142+
143+
private disposeReplListener(): void {
144+
if (this.replPromptListener) {
145+
this.replPromptListener.dispose();
146+
this.replPromptListener = undefined;
147+
}
148+
if (this.replShellTypeListener) {
149+
this.replShellTypeListener.dispose();
150+
this.replShellTypeListener = undefined;
151+
}
152+
}
153+
154+
private flushReplQueue(): void {
155+
while (this.pythonReplCommandQueue.length > 0) {
156+
const commandLine = this.pythonReplCommandQueue.shift();
157+
if (commandLine) {
158+
traceVerbose(`Executing queued REPL command: ${commandLine}`);
159+
this.terminal?.sendText(commandLine);
160+
}
161+
}
162+
}
163+
164+
private async executeCommandInternal(commandLine: string): Promise<TerminalShellExecution | undefined> {
165+
const terminal = this.terminal;
166+
if (!terminal) {
167+
traceVerbose('Terminal not available, cannot execute command');
168+
return undefined;
169+
}
170+
85171
if (!this.options?.hideFromUser) {
86172
terminal.show(true);
87173
}
@@ -105,11 +191,7 @@ export class TerminalService implements ITerminalService, Disposable {
105191
await promise;
106192
}
107193

108-
if (isPythonShell) {
109-
// Prevent KeyboardInterrupt in Python REPL: https://github.com/microsoft/vscode-python/issues/25468
110-
terminal.sendText(commandLine);
111-
traceVerbose(`Python REPL detected, sendText: ${commandLine}`);
112-
} else if (terminal.shellIntegration) {
194+
if (terminal.shellIntegration) {
113195
const execution = terminal.shellIntegration.executeCommand(commandLine);
114196
traceVerbose(`Shell Integration is enabled, executeCommand: ${commandLine}`);
115197
return execution;
@@ -138,6 +220,7 @@ export class TerminalService implements ITerminalService, Disposable {
138220
name: this.options?.title || 'Python',
139221
hideFromUser: this.options?.hideFromUser,
140222
});
223+
return;
141224
} else {
142225
this.terminalShellType = this.terminalHelper.identifyTerminalShell(this.terminal);
143226
this.terminal = this.terminalManager.createTerminal({
@@ -167,6 +250,9 @@ export class TerminalService implements ITerminalService, Disposable {
167250
if (terminal === this.terminal) {
168251
this.terminalClosed.fire();
169252
this.terminal = undefined;
253+
this.isReplReady = false;
254+
this.disposeReplListener();
255+
this.pythonReplCommandQueue = [];
170256
}
171257
}
172258

0 commit comments

Comments
 (0)