Skip to content

Commit 1f49b4c

Browse files
karthiknadiganthonykim1
authored andcommitted
Add setting to control severity of missing package diagnostic. (microsoft#21794)
Closes microsoft#21792
1 parent 1bc8bdd commit 1f49b4c

File tree

6 files changed

+147
-8
lines changed

6 files changed

+147
-8
lines changed

package.json

+15
Original file line numberDiff line numberDiff line change
@@ -1151,6 +1151,21 @@
11511151
"scope": "machine",
11521152
"type": "string"
11531153
},
1154+
"python.missingPackage.severity":{
1155+
"default": "Hint",
1156+
"description": "%python.missingPackage.severity.description%",
1157+
"enum": [
1158+
"Error",
1159+
"Hint",
1160+
"Information",
1161+
"Warning"
1162+
],
1163+
"scope": "resource",
1164+
"type": "string",
1165+
"tags": [
1166+
"experimental"
1167+
]
1168+
},
11541169
"python.pipenvPath": {
11551170
"default": "pipenv",
11561171
"description": "%python.pipenvPath.description%",

package.nls.json

+1
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@
201201
"python.linting.pylintPath.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.",
202202
"python.logging.level.description": "The logging level the extension logs at, defaults to 'error'",
203203
"python.logging.level.deprecation": "This setting is deprecated. Please use command `Developer: Set Log Level...` to set logging level.",
204+
"python.missingPackage.severity.description": "Set severity of missing packages in requirements.txt or pyproject.toml",
204205
"python.pipenvPath.description": "Path to the pipenv executable to use for activation.",
205206
"python.poetryPath.description": "Path to the poetry executable.",
206207
"python.sortImports.args.description": "Arguments passed in. Each argument is a separate item in the array.",

pythonFiles/installed_check.py

+9-4
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
from importlib_metadata import metadata
1616
from packaging.requirements import Requirement
1717

18-
DEFAULT_SEVERITY = 3
18+
DEFAULT_SEVERITY = "3" # 'Hint'
19+
try:
20+
SEVERITY = int(os.getenv("VSCODE_MISSING_PGK_SEVERITY", DEFAULT_SEVERITY))
21+
except ValueError:
22+
SEVERITY = int(DEFAULT_SEVERITY)
1923

2024

2125
def parse_args(argv: Optional[Sequence[str]] = None):
@@ -37,7 +41,8 @@ def parse_requirements(line: str) -> Optional[Requirement]:
3741
elif req.marker.evaluate():
3842
return req
3943
except Exception:
40-
return None
44+
pass
45+
return None
4146

4247

4348
def process_requirements(req_file: pathlib.Path) -> List[Dict[str, Union[str, int]]]:
@@ -60,7 +65,7 @@ def process_requirements(req_file: pathlib.Path) -> List[Dict[str, Union[str, in
6065
"endCharacter": len(req.name),
6166
"package": req.name,
6267
"code": "not-installed",
63-
"severity": DEFAULT_SEVERITY,
68+
"severity": SEVERITY,
6469
}
6570
)
6671
return diagnostics
@@ -100,7 +105,7 @@ def process_pyproject(req_file: pathlib.Path) -> List[Dict[str, Union[str, int]]
100105
"endCharacter": end,
101106
"package": req.name,
102107
"code": "not-installed",
103-
"severity": DEFAULT_SEVERITY,
108+
"severity": SEVERITY,
104109
}
105110
)
106111
return diagnostics

pythonFiles/tests/test_installed_check.py

+51-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import sys
1010

1111
import pytest
12-
from typing import Dict, List, Union
12+
from typing import Dict, List, Optional, Union
1313

1414
SCRIPT_PATH = pathlib.Path(__file__).parent.parent / "installed_check.py"
1515
TEST_DATA = pathlib.Path(__file__).parent / "test_data"
@@ -29,7 +29,12 @@ def generate_file(base_file: pathlib.Path):
2929
os.unlink(str(fullpath))
3030

3131

32-
def run_on_file(file_path: pathlib.Path) -> List[Dict[str, Union[str, int]]]:
32+
def run_on_file(
33+
file_path: pathlib.Path, severity: Optional[str] = None
34+
) -> List[Dict[str, Union[str, int]]]:
35+
env = os.environ.copy()
36+
if severity:
37+
env["VSCODE_MISSING_PGK_SEVERITY"] = severity
3338
result = subprocess.run(
3439
[
3540
sys.executable,
@@ -39,6 +44,7 @@ def run_on_file(file_path: pathlib.Path) -> List[Dict[str, Union[str, int]]]:
3944
stdout=subprocess.PIPE,
4045
stderr=subprocess.PIPE,
4146
check=True,
47+
env=env,
4248
)
4349
assert result.returncode == 0
4450
assert result.stderr == b""
@@ -88,3 +94,46 @@ def test_installed_check(test_name: str):
8894
with generate_file(base_file) as file_path:
8995
result = run_on_file(file_path)
9096
assert result == EXPECTED_DATA[test_name]
97+
98+
99+
EXPECTED_DATA2 = {
100+
"missing-deps": [
101+
{
102+
"line": 6,
103+
"character": 0,
104+
"endLine": 6,
105+
"endCharacter": 10,
106+
"package": "flake8-csv",
107+
"code": "not-installed",
108+
"severity": 0,
109+
},
110+
{
111+
"line": 10,
112+
"character": 0,
113+
"endLine": 10,
114+
"endCharacter": 11,
115+
"package": "levenshtein",
116+
"code": "not-installed",
117+
"severity": 0,
118+
},
119+
],
120+
"pyproject-missing-deps": [
121+
{
122+
"line": 8,
123+
"character": 34,
124+
"endLine": 8,
125+
"endCharacter": 44,
126+
"package": "flake8-csv",
127+
"code": "not-installed",
128+
"severity": 0,
129+
}
130+
],
131+
}
132+
133+
134+
@pytest.mark.parametrize("test_name", EXPECTED_DATA2.keys())
135+
def test_with_severity(test_name: str):
136+
base_file = TEST_DATA / f"{test_name}.data"
137+
with generate_file(base_file) as file_path:
138+
result = run_on_file(file_path, severity="0")
139+
assert result == EXPECTED_DATA2[test_name]

src/client/pythonEnvironments/creation/common/installCheckUtils.ts

+21-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { installedCheckScript } from '../../../common/process/internal/scripts';
66
import { plainExec } from '../../../common/process/rawProcessApis';
77
import { IInterpreterPathService } from '../../../common/types';
88
import { traceInfo, traceVerbose, traceError } from '../../../logging';
9+
import { getConfiguration } from '../../../common/vscodeApis/workspaceApis';
910

1011
interface PackageDiagnostic {
1112
package: string;
@@ -39,6 +40,21 @@ function parseDiagnostics(data: string): Diagnostic[] {
3940
return diagnostics;
4041
}
4142

43+
function getMissingPackageSeverity(doc: TextDocument): number {
44+
const config = getConfiguration('python', doc.uri);
45+
const severity: string = config.get<string>('missingPackage.severity', 'Hint');
46+
if (severity === 'Error') {
47+
return DiagnosticSeverity.Error;
48+
}
49+
if (severity === 'Warning') {
50+
return DiagnosticSeverity.Warning;
51+
}
52+
if (severity === 'Information') {
53+
return DiagnosticSeverity.Information;
54+
}
55+
return DiagnosticSeverity.Hint;
56+
}
57+
4258
export async function getInstalledPackagesDiagnostics(
4359
interpreterPathService: IInterpreterPathService,
4460
doc: TextDocument,
@@ -47,7 +63,11 @@ export async function getInstalledPackagesDiagnostics(
4763
const scriptPath = installedCheckScript();
4864
try {
4965
traceInfo('Running installed packages checker: ', interpreter, scriptPath, doc.uri.fsPath);
50-
const result = await plainExec(interpreter, [scriptPath, doc.uri.fsPath]);
66+
const result = await plainExec(interpreter, [scriptPath, doc.uri.fsPath], {
67+
env: {
68+
VSCODE_MISSING_PGK_SEVERITY: `${getMissingPackageSeverity(doc)}`,
69+
},
70+
});
5171
traceVerbose('Installed packages check result:\n', result.stdout);
5272
if (result.stderr) {
5373
traceError('Installed packages check error:\n', result.stderr);

src/test/pythonEnvironments/creation/common/installCheckUtils.unit.test.ts

+50-1
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import * as chaiAsPromised from 'chai-as-promised';
55
import * as sinon from 'sinon';
66
import * as typemoq from 'typemoq';
77
import { assert, use as chaiUse } from 'chai';
8-
import { Diagnostic, TextDocument, Range, Uri } from 'vscode';
8+
import { Diagnostic, TextDocument, Range, Uri, WorkspaceConfiguration, ConfigurationScope } from 'vscode';
99
import * as rawProcessApis from '../../../../client/common/process/rawProcessApis';
1010
import { getInstalledPackagesDiagnostics } from '../../../../client/pythonEnvironments/creation/common/installCheckUtils';
1111
import { IInterpreterPathService } from '../../../../client/common/types';
12+
import * as workspaceApis from '../../../../client/common/vscodeApis/workspaceApis';
13+
import { SpawnOptions } from '../../../../client/common/process/types';
1214

1315
chaiUse(chaiAsPromised);
1416

@@ -37,29 +39,76 @@ const MISSING_PACKAGES: Diagnostic[] = [
3739
suite('Install check diagnostics tests', () => {
3840
let plainExecStub: sinon.SinonStub;
3941
let interpreterPathService: typemoq.IMock<IInterpreterPathService>;
42+
let getConfigurationStub: sinon.SinonStub;
43+
let configMock: typemoq.IMock<WorkspaceConfiguration>;
4044

4145
setup(() => {
46+
configMock = typemoq.Mock.ofType<WorkspaceConfiguration>();
4247
plainExecStub = sinon.stub(rawProcessApis, 'plainExec');
4348
interpreterPathService = typemoq.Mock.ofType<IInterpreterPathService>();
49+
getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration');
50+
getConfigurationStub.callsFake((section?: string, _scope?: ConfigurationScope | null) => {
51+
if (section === 'python') {
52+
return configMock.object;
53+
}
54+
return undefined;
55+
});
4456
});
4557

4658
teardown(() => {
4759
sinon.restore();
4860
});
4961

5062
test('Test parse diagnostics', async () => {
63+
configMock
64+
.setup((c) => c.get<string>('missingPackage.severity', 'Hint'))
65+
.returns(() => 'Error')
66+
.verifiable(typemoq.Times.atLeastOnce());
5167
plainExecStub.resolves({ stdout: MISSING_PACKAGES_STR, stderr: '' });
5268
const someFile = getSomeRequirementFile();
5369
const result = await getInstalledPackagesDiagnostics(interpreterPathService.object, someFile.object);
5470

5571
assert.deepStrictEqual(result, MISSING_PACKAGES);
72+
configMock.verifyAll();
5673
});
5774

5875
test('Test parse empty diagnostics', async () => {
76+
configMock
77+
.setup((c) => c.get<string>('missingPackage.severity', 'Hint'))
78+
.returns(() => 'Error')
79+
.verifiable(typemoq.Times.atLeastOnce());
5980
plainExecStub.resolves({ stdout: '', stderr: '' });
6081
const someFile = getSomeRequirementFile();
6182
const result = await getInstalledPackagesDiagnostics(interpreterPathService.object, someFile.object);
6283

6384
assert.deepStrictEqual(result, []);
85+
configMock.verifyAll();
86+
});
87+
88+
[
89+
['Error', '0'],
90+
['Warning', '1'],
91+
['Information', '2'],
92+
['Hint', '3'],
93+
].forEach((severityType: string[]) => {
94+
const setting = severityType[0];
95+
const expected = severityType[1];
96+
test(`Test missing package severity: ${setting}`, async () => {
97+
configMock
98+
.setup((c) => c.get<string>('missingPackage.severity', 'Hint'))
99+
.returns(() => setting)
100+
.verifiable(typemoq.Times.atLeastOnce());
101+
let severity: string | undefined;
102+
plainExecStub.callsFake((_cmd: string, _args: string[], options: SpawnOptions) => {
103+
severity = options.env?.VSCODE_MISSING_PGK_SEVERITY;
104+
return { stdout: '', stderr: '' };
105+
});
106+
const someFile = getSomeRequirementFile();
107+
const result = await getInstalledPackagesDiagnostics(interpreterPathService.object, someFile.object);
108+
109+
assert.deepStrictEqual(result, []);
110+
assert.deepStrictEqual(severity, expected);
111+
configMock.verifyAll();
112+
});
64113
});
65114
});

0 commit comments

Comments
 (0)