Added support for pyproject.toml files.

This commit is contained in:
Eric Traut 2021-04-20 21:51:39 -07:00
parent d659009c72
commit 69a0c5594a
6 changed files with 148 additions and 49 deletions

View File

@ -2,6 +2,8 @@
Pyright offers flexible configuration options specified in a JSON-formatted text configuration. By default, the file is called “pyrightconfig.json” and is located within the root directory of your project. Multi-root workspaces (“Add Folder to Workspace…”) are supported, and each workspace root can have its own “pyrightconfig.json” file.
Pyright settings can also be specified in a `[tool.pyright]` section of a “pyproject.toml” file. A “pyrightconfig.json” file always takes precedent over “pyproject.toml” if both are present.
Relative paths specified within the config file are relative to the config files location. Paths with shell variables (including `~`) are not supported.
## Main Pyright Config Options
@ -223,6 +225,32 @@ The following is an example of a pyright config file:
}
```
## Sample Pyproject.toml File
```toml
[tool.pyright]
include = ["src"]
exclude = ["**/node_modules",
"**/__pycache__",
"src/experimental",
"src/typestubs"
]
ignore = ["src/oldstuff"]
stubPath = "src/stubs"
venv = "env367"
reportMissingImports = true
reportMissingTypeStubs = false
pythonVersion = "3.6"
pythonPlatform = "Linux"
executionEnvironments = [
{ root = "src/web", pythonVersion = "3.5", pythonPlatform = "Windows", extraPaths = [ "src/service_libs" ] },
{ root = "src/sdk", pythonVersion = "3.0", extraPaths = [ "src/backend" ] },
{ root = "src/tests", extraPaths = ["src/tests/e2e", "src/sdk" ]},
{ root = "src" }
]
```
## Diagnostic Rule Defaults

View File

@ -437,6 +437,11 @@
"minimist": "^1.2.0"
}
},
"@iarna/toml": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz",
"integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg=="
},
"@istanbuljs/load-nyc-config": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",

View File

@ -15,6 +15,7 @@
"test:coverage": "jest --forceExit --reporters=jest-junit --reporters=default --coverage --coverageReporters=cobertura --coverageReporters=html --coverageReporters=json"
},
"dependencies": {
"@iarna/toml": "2.2.5",
"assert": "^2.0.0",
"chalk": "^4.1.0",
"chokidar": "^3.5.1",

View File

@ -8,6 +8,7 @@
* Python files.
*/
import * as TOML from '@iarna/toml';
import * as JSONC from 'jsonc-parser';
import {
AbstractCancellationTokenSource,
@ -63,6 +64,7 @@ import { findPythonSearchPaths } from './pythonPathUtils';
import { TypeEvaluator } from './typeEvaluator';
export const configFileNames = ['pyrightconfig.json', 'mspythonconfig.json'];
export const pyprojectTomlName = 'pyproject.toml';
// How long since the last user activity should we wait until running
// the analyzer on any files that have not yet been analyzed?
@ -430,6 +432,7 @@ export class AnalyzerService {
private _getConfigOptions(commandLineOptions: CommandLineOptions): ConfigOptions {
let projectRoot = commandLineOptions.executionRoot;
let configFilePath: string | undefined;
let pyprojectFilePath: string | undefined;
if (commandLineOptions.configFilePath) {
// If the config file path was specified, determine whether it's
@ -455,13 +458,13 @@ export class AnalyzerService {
}
} else if (projectRoot) {
// In a project-based IDE like VS Code, we should assume that the
// project root directory contains the config file. If pyright is
// being executed from the command line, the working directory
// may be deep within a project, and we need to walk up the directory
// hierarchy to find the project root.
if (commandLineOptions.fromVsCodeExtension) {
configFilePath = this._findConfigFile(projectRoot);
} else {
// project root directory contains the config file.
configFilePath = this._findConfigFile(projectRoot);
// If pyright is being executed from the command line, the working
// directory may be deep within a project, and we need to walk up the
// directory hierarchy to find the project root.
if (!configFilePath && !commandLineOptions.fromVsCodeExtension) {
configFilePath = this._findConfigFileHereOrUp(projectRoot);
}
@ -473,6 +476,22 @@ export class AnalyzerService {
}
}
if (!configFilePath) {
// See if we can find a pyproject.toml file in this directory.
pyprojectFilePath = this._findPyprojectTomlFile(projectRoot);
if (!pyprojectFilePath && !commandLineOptions.fromVsCodeExtension) {
pyprojectFilePath = this._findPyprojectTomlFileHereOrUp(projectRoot);
}
if (pyprojectFilePath) {
projectRoot = getDirectoryPath(pyprojectFilePath);
this._console.info(`pyproject.toml file found at ${projectRoot}.`);
} else {
this._console.info(`No pyproject.toml file found.`);
}
}
const configOptions = new ConfigOptions(projectRoot, this._typeCheckingMode);
const defaultExcludes = ['**/node_modules', '**/__pycache__', '.git'];
@ -504,41 +523,46 @@ export class AnalyzerService {
}
}
this._configFilePath = configFilePath;
this._configFilePath = configFilePath || pyprojectFilePath;
// If we found a config file, parse it to compute the effective options.
let configJsonObj: object | undefined;
if (configFilePath) {
this._console.info(`Loading configuration file at ${configFilePath}`);
const configJsonObj = this._parseConfigFile(configFilePath);
if (configJsonObj) {
configOptions.initializeFromJson(
configJsonObj,
this._typeCheckingMode,
this._console,
commandLineOptions.diagnosticSeverityOverrides,
commandLineOptions.pythonPath,
commandLineOptions.fileSpecs.length > 0
);
configJsonObj = this._parseJsonConfigFile(configFilePath);
} else if (pyprojectFilePath) {
this._console.info(`Loading pyproject.toml file at ${pyprojectFilePath}`);
configJsonObj = this._parsePyprojectTomlFile(pyprojectFilePath);
}
const configFileDir = getDirectoryPath(configFilePath);
if (configJsonObj) {
configOptions.initializeFromJson(
configJsonObj,
this._typeCheckingMode,
this._console,
commandLineOptions.diagnosticSeverityOverrides,
commandLineOptions.pythonPath,
commandLineOptions.fileSpecs.length > 0
);
// If no include paths were provided, assume that all files within
// the project should be included.
if (configOptions.include.length === 0) {
this._console.info(`No include entries specified; assuming ${configFileDir}`);
configOptions.include.push(getFileSpec(configFileDir, '.'));
}
const configFileDir = getDirectoryPath(this._configFilePath!);
// If there was no explicit set of excludes, add a few common ones to avoid long scan times.
if (configOptions.exclude.length === 0) {
defaultExcludes.forEach((exclude) => {
this._console.info(`Auto-excluding ${exclude}`);
configOptions.exclude.push(getFileSpec(configFileDir, exclude));
});
// If no include paths were provided, assume that all files within
// the project should be included.
if (configOptions.include.length === 0) {
this._console.info(`No include entries specified; assuming ${configFileDir}`);
configOptions.include.push(getFileSpec(configFileDir, '.'));
}
if (configOptions.autoExcludeVenv === undefined) {
configOptions.autoExcludeVenv = true;
}
// If there was no explicit set of excludes, add a few common ones to avoid long scan times.
if (configOptions.exclude.length === 0) {
defaultExcludes.forEach((exclude) => {
this._console.info(`Auto-excluding ${exclude}`);
configOptions.exclude.push(getFileSpec(configFileDir, exclude));
});
if (configOptions.autoExcludeVenv === undefined) {
configOptions.autoExcludeVenv = true;
}
}
} else {
@ -803,27 +827,63 @@ export class AnalyzerService {
return undefined;
}
private _parseConfigFile(configPath: string): any | undefined {
let configContents = '';
private _findPyprojectTomlFileHereOrUp(searchPath: string): string | undefined {
return forEachAncestorDirectory(searchPath, (ancestor) => this._findPyprojectTomlFile(ancestor));
}
private _findPyprojectTomlFile(searchPath: string) {
const fileName = combinePaths(searchPath, pyprojectTomlName);
if (this._fs.existsSync(fileName)) {
return fileName;
}
return undefined;
}
private _parseJsonConfigFile(configPath: string): object | undefined {
return this._attemptParseFile(configPath, (fileContents) => {
return JSONC.parse(fileContents);
});
}
private _parsePyprojectTomlFile(pyprojectPath: string): object | undefined {
return this._attemptParseFile(pyprojectPath, (fileContents, attemptCount) => {
try {
const configObj = TOML.parse(fileContents);
if (configObj && configObj.tool && (configObj.tool as TOML.JsonMap).pyright) {
return (configObj.tool as TOML.JsonMap).pyright as object;
}
} catch (e) {
this._console.error(`Pyproject file parse attempt ${attemptCount} error: ${JSON.stringify(e)}`);
throw e;
}
this._console.error(`Pyproject file "${pyprojectPath}" is missing "[tool.pyright] section.`);
return undefined;
});
}
private _attemptParseFile(
filePath: string,
parseCallback: (contents: string, attempt: number) => object | undefined
): object | undefined {
let fileContents = '';
let parseAttemptCount = 0;
while (true) {
// Attempt to read the config file contents.
// Attempt to read the file contents.
try {
configContents = this._fs.readFileSync(configPath, 'utf8');
fileContents = this._fs.readFileSync(filePath, 'utf8');
} catch {
this._console.error(`Config file "${configPath}" could not be read.`);
this._console.error(`Config file "${filePath}" could not be read.`);
this._reportConfigParseError();
return undefined;
}
// Attempt to parse the config file.
let configObj: any;
// Attempt to parse the file.
let parseFailed = false;
try {
configObj = JSONC.parse(configContents);
return configObj;
} catch {
return parseCallback(fileContents, parseAttemptCount + 1);
} catch (e) {
parseFailed = true;
}
@ -831,12 +891,11 @@ export class AnalyzerService {
break;
}
// If we attempt to read the config file immediately after it
// was saved, it may have been partially written when we read it,
// resulting in parse errors. We'll give it a little more time and
// try again.
// If we attempt to read the file immediately after it was saved, it
// may have been partially written when we read it, resulting in parse
// errors. We'll give it a little more time and try again.
if (parseAttemptCount++ >= 5) {
this._console.error(`Config file "${configPath}" could not be parsed. Verify that JSON is correct.`);
this._console.error(`Config file "${filePath}" could not be parsed. Verify that format is correct.`);
this._reportConfigParseError();
return undefined;
}

View File

@ -10,6 +10,11 @@
"integrity": "sha512-HyYEUDeIj5rRQU2Hk5HTB2uHsbRQpF70nvMhVzi+VJR0X+xNEhjPui4/kBf3VeH/wqD28PT4sVOm8qqLjBrSZg==",
"dev": true
},
"@iarna/toml": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz",
"integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg=="
},
"@nodelib/fs.scandir": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz",

View File

@ -750,6 +750,7 @@
"webpack-dev": "npm run clean && webpack --mode development --watch --progress"
},
"dependencies": {
"@iarna/toml": "2.2.5",
"vscode-jsonrpc": "6.0.0",
"vscode-languageclient": "7.0.0",
"vscode-languageserver": "7.0.0",