mirror of
https://github.com/microsoft/pyright.git
synced 2024-08-16 19:30:26 +03:00
Added support for configuration inheritance through an "extends" configuration option. This addresses #4366. (#7997)
This commit is contained in:
parent
0a83d6459c
commit
421dabee57
@ -16,6 +16,8 @@ Relative paths specified within the config file are relative to the config file
|
|||||||
|
|
||||||
- **strict** [array of paths, optional]: Paths of directories or files that should use “strict” analysis if they are included. This is the same as manually adding a “# pyright: strict” comment. In strict mode, most type-checking rules are enabled. Refer to [this table](configuration.md#diagnostic-settings-defaults) for details about which rules are enabled in strict mode. Paths may contain wildcard characters ** (a directory or multiple levels of directories), * (a sequence of zero or more characters), or ? (a single character).
|
- **strict** [array of paths, optional]: Paths of directories or files that should use “strict” analysis if they are included. This is the same as manually adding a “# pyright: strict” comment. In strict mode, most type-checking rules are enabled. Refer to [this table](configuration.md#diagnostic-settings-defaults) for details about which rules are enabled in strict mode. Paths may contain wildcard characters ** (a directory or multiple levels of directories), * (a sequence of zero or more characters), or ? (a single character).
|
||||||
|
|
||||||
|
- **extends** [path, optional]: Path to another `.json` or `.toml` file that is used as a “base configuration”, allowing this configuration to inherit configuration settings. Top-level keys within this configuration overwrite top-level keys in the base configuration. Multiple levels of inheritance are supported. Relative paths specified in a configuration file are resolved relative to the location of that configuration file.
|
||||||
|
|
||||||
- **defineConstant** [map of constants to values (boolean or string), optional]: Set of identifiers that should be assumed to contain a constant value wherever used within this program. For example, `{ "DEBUG": true }` indicates that pyright should assume that the identifier `DEBUG` will always be equal to `True`. If this identifier is used within a conditional expression (such as `if not DEBUG:`) pyright will use the indicated value to determine whether the guarded block is reachable or not. Member expressions that reference one of these constants (e.g. `my_module.DEBUG`) are also supported.
|
- **defineConstant** [map of constants to values (boolean or string), optional]: Set of identifiers that should be assumed to contain a constant value wherever used within this program. For example, `{ "DEBUG": true }` indicates that pyright should assume that the identifier `DEBUG` will always be equal to `True`. If this identifier is used within a conditional expression (such as `if not DEBUG:`) pyright will use the indicated value to determine whether the guarded block is reachable or not. Member expressions that reference one of these constants (e.g. `my_module.DEBUG`) are also supported.
|
||||||
|
|
||||||
- **typeshedPath** [path, optional]: Path to a directory that contains typeshed type stub files. Pyright ships with a bundled copy of typeshed type stubs. If you want to use a different version of typeshed stubs, you can clone the [typeshed github repo](https://github.com/python/typeshed) to a local directory and reference the location with this path. This option is useful if you’re actively contributing updates to typeshed.
|
- **typeshedPath** [path, optional]: Path to a directory that contains typeshed type stub files. Pyright ships with a bundled copy of typeshed type stubs. If you want to use a different version of typeshed stubs, you can clone the [typeshed github repo](https://github.com/python/typeshed) to a local directory and reference the location with this path. This option is useful if you’re actively contributing updates to typeshed.
|
||||||
|
@ -54,7 +54,7 @@ import { MaxAnalysisTime, Program } from './program';
|
|||||||
import { findPythonSearchPaths } from './pythonPathUtils';
|
import { findPythonSearchPaths } from './pythonPathUtils';
|
||||||
import { IPythonMode } from './sourceFile';
|
import { IPythonMode } from './sourceFile';
|
||||||
|
|
||||||
export const configFileNames = ['pyrightconfig.json'];
|
export const configFileName = 'pyrightconfig.json';
|
||||||
export const pyprojectTomlName = 'pyproject.toml';
|
export const pyprojectTomlName = 'pyproject.toml';
|
||||||
|
|
||||||
// How long since the last user activity should we wait until running
|
// How long since the last user activity should we wait until running
|
||||||
@ -84,6 +84,11 @@ export interface AnalyzerServiceOptions {
|
|||||||
fileSystem?: FileSystem;
|
fileSystem?: FileSystem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ConfigFileContents {
|
||||||
|
configFileDirUri: Uri;
|
||||||
|
configFileJsonObj: object;
|
||||||
|
}
|
||||||
|
|
||||||
// Hold uniqueId for this service. It can be used to distinguish each service later.
|
// Hold uniqueId for this service. It can be used to distinguish each service later.
|
||||||
let _nextServiceId = 1;
|
let _nextServiceId = 1;
|
||||||
|
|
||||||
@ -103,7 +108,8 @@ export class AnalyzerService {
|
|||||||
private _sourceFileWatcher: FileWatcher | undefined;
|
private _sourceFileWatcher: FileWatcher | undefined;
|
||||||
private _reloadConfigTimer: any;
|
private _reloadConfigTimer: any;
|
||||||
private _libraryReanalysisTimer: any;
|
private _libraryReanalysisTimer: any;
|
||||||
private _configFileUri: Uri | undefined;
|
private _primaryConfigFileUri: Uri | undefined;
|
||||||
|
private _extendedConfigFileUris: Uri[] = [];
|
||||||
private _configFileWatcher: FileWatcher | undefined;
|
private _configFileWatcher: FileWatcher | undefined;
|
||||||
private _libraryFileWatcher: FileWatcher | undefined;
|
private _libraryFileWatcher: FileWatcher | undefined;
|
||||||
private _librarySearchUrisToWatch: Uri[] | undefined;
|
private _librarySearchUrisToWatch: Uri[] | undefined;
|
||||||
@ -534,11 +540,12 @@ export class AnalyzerService {
|
|||||||
? Uri.file(commandLineOptions.configFilePath, this.serviceProvider, /* checkRelative */ true)
|
? Uri.file(commandLineOptions.configFilePath, this.serviceProvider, /* checkRelative */ true)
|
||||||
: projectRoot.resolvePaths(commandLineOptions.configFilePath)
|
: projectRoot.resolvePaths(commandLineOptions.configFilePath)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!this.fs.existsSync(configFilePath)) {
|
if (!this.fs.existsSync(configFilePath)) {
|
||||||
this._console.info(`Configuration file not found at ${configFilePath.toUserVisibleString()}.`);
|
this._console.info(`Configuration file not found at ${configFilePath.toUserVisibleString()}.`);
|
||||||
configFilePath = projectRoot;
|
configFilePath = projectRoot;
|
||||||
} else {
|
} else {
|
||||||
if (configFilePath.lastExtension.endsWith('.json')) {
|
if (configFilePath.lastExtension.endsWith('.json') || configFilePath.lastExtension.endsWith('.toml')) {
|
||||||
projectRoot = configFilePath.getDirectory();
|
projectRoot = configFilePath.getDirectory();
|
||||||
} else {
|
} else {
|
||||||
projectRoot = configFilePath;
|
projectRoot = configFilePath;
|
||||||
@ -648,30 +655,23 @@ export class AnalyzerService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._configFileUri = configFilePath || pyprojectFilePath;
|
|
||||||
|
|
||||||
configOptions.disableTaggedHints = !!commandLineOptions.disableTaggedHints;
|
configOptions.disableTaggedHints = !!commandLineOptions.disableTaggedHints;
|
||||||
|
|
||||||
// If we found a config file, parse it to compute the effective options.
|
const configs = this._getExtendedConfigurations(configFilePath ?? pyprojectFilePath);
|
||||||
let configJsonObj: object | undefined;
|
|
||||||
if (configFilePath) {
|
|
||||||
this._console.info(`Loading configuration file at ${configFilePath.toUserVisibleString()}`);
|
|
||||||
configJsonObj = this._parseJsonConfigFile(configFilePath);
|
|
||||||
} else if (pyprojectFilePath) {
|
|
||||||
this._console.info(`Loading pyproject.toml file at ${pyprojectFilePath.toUserVisibleString()}`);
|
|
||||||
configJsonObj = this._parsePyprojectTomlFile(pyprojectFilePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (configJsonObj) {
|
if (configs) {
|
||||||
configOptions.initializeFromJson(
|
for (const config of configs) {
|
||||||
configJsonObj,
|
configOptions.initializeFromJson(
|
||||||
this._typeCheckingMode,
|
config.configFileJsonObj,
|
||||||
this.serviceProvider,
|
config.configFileDirUri,
|
||||||
host,
|
this._typeCheckingMode,
|
||||||
commandLineOptions
|
this.serviceProvider,
|
||||||
);
|
host,
|
||||||
|
commandLineOptions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const configFileDir = this._configFileUri!.getDirectory();
|
const configFileDir = this._primaryConfigFileUri!.getDirectory();
|
||||||
|
|
||||||
// If no include paths were provided, assume that all files within
|
// If no include paths were provided, assume that all files within
|
||||||
// the project should be included.
|
// the project should be included.
|
||||||
@ -860,6 +860,61 @@ export class AnalyzerService {
|
|||||||
return configOptions;
|
return configOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Loads the config JSON object from the specified config file along with any
|
||||||
|
// chained config files specified in the "extends" property (recursively).
|
||||||
|
private _getExtendedConfigurations(primaryConfigFileUri: Uri | undefined): ConfigFileContents[] | undefined {
|
||||||
|
this._primaryConfigFileUri = primaryConfigFileUri;
|
||||||
|
this._extendedConfigFileUris = [];
|
||||||
|
|
||||||
|
if (!primaryConfigFileUri) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
let curConfigFileUri = primaryConfigFileUri;
|
||||||
|
|
||||||
|
const configJsonObjs: ConfigFileContents[] = [];
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
this._extendedConfigFileUris.push(curConfigFileUri);
|
||||||
|
|
||||||
|
let configFileJsonObj: object | undefined;
|
||||||
|
|
||||||
|
// Is this a TOML or JSON file?
|
||||||
|
if (curConfigFileUri.lastExtension.endsWith('.toml')) {
|
||||||
|
this._console.info(`Loading pyproject.toml file at ${curConfigFileUri.toUserVisibleString()}`);
|
||||||
|
configFileJsonObj = this._parsePyprojectTomlFile(curConfigFileUri);
|
||||||
|
} else {
|
||||||
|
this._console.info(`Loading configuration file at ${curConfigFileUri.toUserVisibleString()}`);
|
||||||
|
configFileJsonObj = this._parseJsonConfigFile(curConfigFileUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!configFileJsonObj) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push onto the start of the array so base configs are processed first.
|
||||||
|
configJsonObjs.unshift({ configFileJsonObj, configFileDirUri: curConfigFileUri.getDirectory() });
|
||||||
|
|
||||||
|
const baseConfigUri = ConfigOptions.resolveExtends(configFileJsonObj, curConfigFileUri.getDirectory());
|
||||||
|
if (!baseConfigUri) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for circular references.
|
||||||
|
if (this._extendedConfigFileUris.some((uri) => uri.equals(baseConfigUri))) {
|
||||||
|
this._console.error(
|
||||||
|
`Circular reference in configuration file "extends" setting: ${curConfigFileUri.toUserVisibleString()} ` +
|
||||||
|
`extends ${baseConfigUri.toUserVisibleString()}`
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
curConfigFileUri = baseConfigUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
return configJsonObjs;
|
||||||
|
}
|
||||||
|
|
||||||
private _getTypeStubFolder() {
|
private _getTypeStubFolder() {
|
||||||
const stubPath =
|
const stubPath =
|
||||||
this._configOptions.stubPath ??
|
this._configOptions.stubPath ??
|
||||||
@ -914,12 +969,11 @@ export class AnalyzerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _findConfigFile(searchPath: Uri): Uri | undefined {
|
private _findConfigFile(searchPath: Uri): Uri | undefined {
|
||||||
for (const name of configFileNames) {
|
const fileName = searchPath.resolvePaths(configFileName);
|
||||||
const fileName = searchPath.resolvePaths(name);
|
if (this.fs.existsSync(fileName)) {
|
||||||
if (this.fs.existsSync(fileName)) {
|
return this.fs.realCasePath(fileName);
|
||||||
return this.fs.realCasePath(fileName);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1573,8 +1627,8 @@ export class AnalyzerService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._configFileUri) {
|
if (this._primaryConfigFileUri) {
|
||||||
this._configFileWatcher = this.fs.createFileSystemWatcher([this._configFileUri], (event) => {
|
this._configFileWatcher = this.fs.createFileSystemWatcher(this._extendedConfigFileUris, (event) => {
|
||||||
if (this._verboseOutput) {
|
if (this._verboseOutput) {
|
||||||
this._console.info(`Received fs event '${event}' for config file`);
|
this._console.info(`Received fs event '${event}' for config file`);
|
||||||
}
|
}
|
||||||
@ -1588,7 +1642,7 @@ export class AnalyzerService {
|
|||||||
|
|
||||||
if (event === 'add' || event === 'change') {
|
if (event === 'add' || event === 'change') {
|
||||||
const fileName = getFileName(path);
|
const fileName = getFileName(path);
|
||||||
if (fileName && configFileNames.some((name) => name === fileName)) {
|
if (fileName === configFileName) {
|
||||||
if (this._verboseOutput) {
|
if (this._verboseOutput) {
|
||||||
this._console.info(`Received fs event '${event}' for config file`);
|
this._console.info(`Received fs event '${event}' for config file`);
|
||||||
}
|
}
|
||||||
@ -1624,8 +1678,8 @@ export class AnalyzerService {
|
|||||||
private _reloadConfigFile() {
|
private _reloadConfigFile() {
|
||||||
this._updateConfigFileWatcher();
|
this._updateConfigFileWatcher();
|
||||||
|
|
||||||
if (this._configFileUri) {
|
if (this._primaryConfigFileUri) {
|
||||||
this._console.info(`Reloading configuration file at ${this._configFileUri.toUserVisibleString()}`);
|
this._console.info(`Reloading configuration file at ${this._primaryConfigFileUri.toUserVisibleString()}`);
|
||||||
|
|
||||||
const host = this._backgroundAnalysisProgram.host;
|
const host = this._backgroundAnalysisProgram.host;
|
||||||
|
|
||||||
|
@ -19,8 +19,8 @@ import { DiagnosticRule } from './diagnosticRules';
|
|||||||
import { FileSystem } from './fileSystem';
|
import { FileSystem } from './fileSystem';
|
||||||
import { Host } from './host';
|
import { Host } from './host';
|
||||||
import { PythonVersion, latestStablePythonVersion } from './pythonVersion';
|
import { PythonVersion, latestStablePythonVersion } from './pythonVersion';
|
||||||
import { ServiceProvider } from './serviceProvider';
|
|
||||||
import { ServiceKeys } from './serviceKeys';
|
import { ServiceKeys } from './serviceKeys';
|
||||||
|
import { ServiceProvider } from './serviceProvider';
|
||||||
import { Uri } from './uri/uri';
|
import { Uri } from './uri/uri';
|
||||||
import { FileSpec, getFileSpec, isDirectory } from './uri/uriUtils';
|
import { FileSpec, getFileSpec, isDirectory } from './uri/uriUtils';
|
||||||
|
|
||||||
@ -1104,6 +1104,7 @@ export class ConfigOptions {
|
|||||||
// Initialize the structure from a JSON object.
|
// Initialize the structure from a JSON object.
|
||||||
initializeFromJson(
|
initializeFromJson(
|
||||||
configObj: any,
|
configObj: any,
|
||||||
|
configDirUri: Uri,
|
||||||
typeCheckingMode: string | undefined,
|
typeCheckingMode: string | undefined,
|
||||||
serviceProvider: ServiceProvider,
|
serviceProvider: ServiceProvider,
|
||||||
host: Host,
|
host: Host,
|
||||||
@ -1125,7 +1126,7 @@ export class ConfigOptions {
|
|||||||
} else if (isAbsolute(fileSpec)) {
|
} else if (isAbsolute(fileSpec)) {
|
||||||
console.error(`Ignoring path "${fileSpec}" in "include" array because it is not relative.`);
|
console.error(`Ignoring path "${fileSpec}" in "include" array because it is not relative.`);
|
||||||
} else {
|
} else {
|
||||||
this.include.push(getFileSpec(this.projectRoot, fileSpec));
|
this.include.push(getFileSpec(configDirUri, fileSpec));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1144,7 +1145,7 @@ export class ConfigOptions {
|
|||||||
} else if (isAbsolute(fileSpec)) {
|
} else if (isAbsolute(fileSpec)) {
|
||||||
console.error(`Ignoring path "${fileSpec}" in "exclude" array because it is not relative.`);
|
console.error(`Ignoring path "${fileSpec}" in "exclude" array because it is not relative.`);
|
||||||
} else {
|
} else {
|
||||||
this.exclude.push(getFileSpec(this.projectRoot, fileSpec));
|
this.exclude.push(getFileSpec(configDirUri, fileSpec));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1163,7 +1164,7 @@ export class ConfigOptions {
|
|||||||
} else if (isAbsolute(fileSpec)) {
|
} else if (isAbsolute(fileSpec)) {
|
||||||
console.error(`Ignoring path "${fileSpec}" in "ignore" array because it is not relative.`);
|
console.error(`Ignoring path "${fileSpec}" in "ignore" array because it is not relative.`);
|
||||||
} else {
|
} else {
|
||||||
this.ignore.push(getFileSpec(this.projectRoot, fileSpec));
|
this.ignore.push(getFileSpec(configDirUri, fileSpec));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1182,14 +1183,14 @@ export class ConfigOptions {
|
|||||||
} else if (isAbsolute(fileSpec)) {
|
} else if (isAbsolute(fileSpec)) {
|
||||||
console.error(`Ignoring path "${fileSpec}" in "strict" array because it is not relative.`);
|
console.error(`Ignoring path "${fileSpec}" in "strict" array because it is not relative.`);
|
||||||
} else {
|
} else {
|
||||||
this.strict.push(getFileSpec(this.projectRoot, fileSpec));
|
this.strict.push(getFileSpec(configDirUri, fileSpec));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is a "typeCheckingMode", it can override the provided setting.
|
// If there is a "typeCheckingMode", it can override the provided setting.
|
||||||
let configTypeCheckingMode: string | undefined;
|
let configTypeCheckingMode: string | undefined = this.typeCheckingMode;
|
||||||
if (configObj.typeCheckingMode !== undefined) {
|
if (configObj.typeCheckingMode !== undefined) {
|
||||||
if (
|
if (
|
||||||
configObj.typeCheckingMode === 'off' ||
|
configObj.typeCheckingMode === 'off' ||
|
||||||
@ -1239,17 +1240,15 @@ export class ConfigOptions {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Read the "venvPath".
|
// Read the "venvPath".
|
||||||
this.venvPath = undefined;
|
|
||||||
if (configObj.venvPath !== undefined) {
|
if (configObj.venvPath !== undefined) {
|
||||||
if (typeof configObj.venvPath !== 'string') {
|
if (typeof configObj.venvPath !== 'string') {
|
||||||
console.error(`Config "venvPath" field must contain a string.`);
|
console.error(`Config "venvPath" field must contain a string.`);
|
||||||
} else {
|
} else {
|
||||||
this.venvPath = this.projectRoot.resolvePaths(configObj.venvPath);
|
this.venvPath = configDirUri.resolvePaths(configObj.venvPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the "venv" name.
|
// Read the "venv" name.
|
||||||
this.venv = undefined;
|
|
||||||
if (configObj.venv !== undefined) {
|
if (configObj.venv !== undefined) {
|
||||||
if (typeof configObj.venv !== 'string') {
|
if (typeof configObj.venv !== 'string') {
|
||||||
console.error(`Config "venv" field must contain a string.`);
|
console.error(`Config "venv" field must contain a string.`);
|
||||||
@ -1269,7 +1268,7 @@ export class ConfigOptions {
|
|||||||
if (typeof path !== 'string') {
|
if (typeof path !== 'string') {
|
||||||
console.error(`Config "extraPaths" field ${pathIndex} must be a string.`);
|
console.error(`Config "extraPaths" field ${pathIndex} must be a string.`);
|
||||||
} else {
|
} else {
|
||||||
this.defaultExtraPaths!.push(this.projectRoot.resolvePaths(path));
|
this.defaultExtraPaths!.push(configDirUri.resolvePaths(path));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1312,19 +1311,17 @@ export class ConfigOptions {
|
|||||||
this.ensureDefaultPythonPlatform(host, console);
|
this.ensureDefaultPythonPlatform(host, console);
|
||||||
|
|
||||||
// Read the "typeshedPath" setting.
|
// Read the "typeshedPath" setting.
|
||||||
this.typeshedPath = undefined;
|
|
||||||
if (configObj.typeshedPath !== undefined) {
|
if (configObj.typeshedPath !== undefined) {
|
||||||
if (typeof configObj.typeshedPath !== 'string') {
|
if (typeof configObj.typeshedPath !== 'string') {
|
||||||
console.error(`Config "typeshedPath" field must contain a string.`);
|
console.error(`Config "typeshedPath" field must contain a string.`);
|
||||||
} else {
|
} else {
|
||||||
this.typeshedPath = configObj.typeshedPath
|
this.typeshedPath = configObj.typeshedPath
|
||||||
? this.projectRoot.resolvePaths(configObj.typeshedPath)
|
? configDirUri.resolvePaths(configObj.typeshedPath)
|
||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the "stubPath" setting.
|
// Read the "stubPath" setting.
|
||||||
this.stubPath = undefined;
|
|
||||||
|
|
||||||
// Keep this for backward compatibility
|
// Keep this for backward compatibility
|
||||||
if (configObj.typingsPath !== undefined) {
|
if (configObj.typingsPath !== undefined) {
|
||||||
@ -1332,7 +1329,7 @@ export class ConfigOptions {
|
|||||||
console.error(`Config "typingsPath" field must contain a string.`);
|
console.error(`Config "typingsPath" field must contain a string.`);
|
||||||
} else {
|
} else {
|
||||||
console.error(`Config "typingsPath" is now deprecated. Please, use stubPath instead.`);
|
console.error(`Config "typingsPath" is now deprecated. Please, use stubPath instead.`);
|
||||||
this.stubPath = this.projectRoot.resolvePaths(configObj.typingsPath);
|
this.stubPath = configDirUri.resolvePaths(configObj.typingsPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1340,7 +1337,7 @@ export class ConfigOptions {
|
|||||||
if (typeof configObj.stubPath !== 'string') {
|
if (typeof configObj.stubPath !== 'string') {
|
||||||
console.error(`Config "stubPath" field must contain a string.`);
|
console.error(`Config "stubPath" field must contain a string.`);
|
||||||
} else {
|
} else {
|
||||||
this.stubPath = this.projectRoot.resolvePaths(configObj.stubPath);
|
this.stubPath = configDirUri.resolvePaths(configObj.stubPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1384,14 +1381,23 @@ export class ConfigOptions {
|
|||||||
|
|
||||||
// Read the "executionEnvironments" array. This should be done at the end
|
// Read the "executionEnvironments" array. This should be done at the end
|
||||||
// after we've established default values.
|
// after we've established default values.
|
||||||
this.executionEnvironments = [];
|
|
||||||
if (configObj.executionEnvironments !== undefined) {
|
if (configObj.executionEnvironments !== undefined) {
|
||||||
if (!Array.isArray(configObj.executionEnvironments)) {
|
if (!Array.isArray(configObj.executionEnvironments)) {
|
||||||
console.error(`Config "executionEnvironments" field must contain an array.`);
|
console.error(`Config "executionEnvironments" field must contain an array.`);
|
||||||
} else {
|
} else {
|
||||||
|
this.executionEnvironments = [];
|
||||||
|
|
||||||
const execEnvironments = configObj.executionEnvironments as ExecutionEnvironment[];
|
const execEnvironments = configObj.executionEnvironments as ExecutionEnvironment[];
|
||||||
|
|
||||||
execEnvironments.forEach((env, index) => {
|
execEnvironments.forEach((env, index) => {
|
||||||
const execEnv = this._initExecutionEnvironmentFromJson(env, index, console, commandLineOptions);
|
const execEnv = this._initExecutionEnvironmentFromJson(
|
||||||
|
env,
|
||||||
|
configDirUri,
|
||||||
|
index,
|
||||||
|
console,
|
||||||
|
commandLineOptions
|
||||||
|
);
|
||||||
|
|
||||||
if (execEnv) {
|
if (execEnv) {
|
||||||
this.executionEnvironments.push(execEnv);
|
this.executionEnvironments.push(execEnv);
|
||||||
}
|
}
|
||||||
@ -1450,6 +1456,18 @@ export class ConfigOptions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static resolveExtends(configObj: any, configDirUri: Uri): Uri | undefined {
|
||||||
|
if (configObj.extends !== undefined) {
|
||||||
|
if (typeof configObj.extends !== 'string') {
|
||||||
|
console.error(`Config "extends" field must contain a string.`);
|
||||||
|
} else {
|
||||||
|
return configDirUri.resolvePaths(configObj.extends);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
ensureDefaultPythonPlatform(host: Host, console: ConsoleInterface) {
|
ensureDefaultPythonPlatform(host: Host, console: ConsoleInterface) {
|
||||||
// If no default python platform was specified, assume that the
|
// If no default python platform was specified, assume that the
|
||||||
// user wants to use the current platform.
|
// user wants to use the current platform.
|
||||||
@ -1552,6 +1570,7 @@ export class ConfigOptions {
|
|||||||
|
|
||||||
private _initExecutionEnvironmentFromJson(
|
private _initExecutionEnvironmentFromJson(
|
||||||
envObj: any,
|
envObj: any,
|
||||||
|
configDirUri: Uri,
|
||||||
index: number,
|
index: number,
|
||||||
console: ConsoleInterface,
|
console: ConsoleInterface,
|
||||||
commandLineOptions?: CommandLineOptions
|
commandLineOptions?: CommandLineOptions
|
||||||
@ -1559,7 +1578,7 @@ export class ConfigOptions {
|
|||||||
try {
|
try {
|
||||||
const newExecEnv = new ExecutionEnvironment(
|
const newExecEnv = new ExecutionEnvironment(
|
||||||
this._getEnvironmentName(),
|
this._getEnvironmentName(),
|
||||||
this.projectRoot,
|
configDirUri,
|
||||||
this.defaultPythonVersion,
|
this.defaultPythonVersion,
|
||||||
this.defaultPythonPlatform,
|
this.defaultPythonPlatform,
|
||||||
this.defaultExtraPaths
|
this.defaultExtraPaths
|
||||||
@ -1567,7 +1586,7 @@ export class ConfigOptions {
|
|||||||
|
|
||||||
// Validate the root.
|
// Validate the root.
|
||||||
if (envObj.root && typeof envObj.root === 'string') {
|
if (envObj.root && typeof envObj.root === 'string') {
|
||||||
newExecEnv.root = this.projectRoot.resolvePaths(envObj.root);
|
newExecEnv.root = configDirUri.resolvePaths(envObj.root);
|
||||||
} else {
|
} else {
|
||||||
console.error(`Config executionEnvironments index ${index}: missing root value.`);
|
console.error(`Config executionEnvironments index ${index}: missing root value.`);
|
||||||
}
|
}
|
||||||
@ -1587,7 +1606,7 @@ export class ConfigOptions {
|
|||||||
` extraPaths field ${pathIndex} must be a string.`
|
` extraPaths field ${pathIndex} must be a string.`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
newExecEnv.extraPaths.push(this.projectRoot.resolvePaths(path));
|
newExecEnv.extraPaths.push(configDirUri.resolvePaths(path));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -12,11 +12,11 @@ import {
|
|||||||
FileSystemWatcher,
|
FileSystemWatcher,
|
||||||
WatchKind,
|
WatchKind,
|
||||||
} from 'vscode-languageserver';
|
} from 'vscode-languageserver';
|
||||||
import { DynamicFeature } from './dynamicFeature';
|
import { configFileName } from '../analyzer/service';
|
||||||
import { configFileNames } from '../analyzer/service';
|
import { FileSystem } from '../common/fileSystem';
|
||||||
import { deduplicateFolders, isFile } from '../common/uri/uriUtils';
|
import { deduplicateFolders, isFile } from '../common/uri/uriUtils';
|
||||||
import { WorkspaceFactory } from '../workspaceFactory';
|
import { WorkspaceFactory } from '../workspaceFactory';
|
||||||
import { FileSystem } from '../common/fileSystem';
|
import { DynamicFeature } from './dynamicFeature';
|
||||||
|
|
||||||
export class FileWatcherDynamicFeature extends DynamicFeature {
|
export class FileWatcherDynamicFeature extends DynamicFeature {
|
||||||
constructor(
|
constructor(
|
||||||
@ -33,7 +33,7 @@ export class FileWatcherDynamicFeature extends DynamicFeature {
|
|||||||
|
|
||||||
// Set default (config files and all workspace files) first.
|
// Set default (config files and all workspace files) first.
|
||||||
const watchers: FileSystemWatcher[] = [
|
const watchers: FileSystemWatcher[] = [
|
||||||
...configFileNames.map((fileName) => ({ globPattern: `**/${fileName}`, kind: watchKind })),
|
{ globPattern: `**/${configFileName}`, kind: watchKind },
|
||||||
{ globPattern: '**', kind: watchKind },
|
{ globPattern: '**', kind: watchKind },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -216,7 +216,7 @@ test('PythonPlatform', () => {
|
|||||||
const nullConsole = new NullConsole();
|
const nullConsole = new NullConsole();
|
||||||
|
|
||||||
const sp = createServiceProvider(fs, nullConsole);
|
const sp = createServiceProvider(fs, nullConsole);
|
||||||
configOptions.initializeFromJson(json, undefined, sp, new NoAccessHost());
|
configOptions.initializeFromJson(json, cwd, undefined, sp, new NoAccessHost());
|
||||||
|
|
||||||
const env = configOptions.executionEnvironments[0];
|
const env = configOptions.executionEnvironments[0];
|
||||||
assert.strictEqual(env.pythonPlatform, 'platform');
|
assert.strictEqual(env.pythonPlatform, 'platform');
|
||||||
@ -340,9 +340,10 @@ test('verify config fileSpecs after cloning', () => {
|
|||||||
ignore: ['**/node_modules/**'],
|
ignore: ['**/node_modules/**'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const config = new ConfigOptions(Uri.file(process.cwd(), fs));
|
const rootUri = Uri.file(process.cwd(), fs);
|
||||||
|
const config = new ConfigOptions(rootUri);
|
||||||
const sp = createServiceProvider(fs, new NullConsole());
|
const sp = createServiceProvider(fs, new NullConsole());
|
||||||
config.initializeFromJson(configFile, undefined, sp, new TestAccessHost());
|
config.initializeFromJson(configFile, rootUri, undefined, sp, new TestAccessHost());
|
||||||
const cloned = deserialize(serialize(config));
|
const cloned = deserialize(serialize(config));
|
||||||
|
|
||||||
assert.deepEqual(config.ignore, cloned.ignore);
|
assert.deepEqual(config.ignore, cloned.ignore);
|
||||||
@ -371,3 +372,18 @@ test('extra paths on undefined execution root/default workspace', () => {
|
|||||||
expectedExtraPaths.map((u) => u.getFilePath())
|
expectedExtraPaths.map((u) => u.getFilePath())
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Extended config files', () => {
|
||||||
|
const cwd = normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project_with_extended_config'));
|
||||||
|
const service = createAnalyzer();
|
||||||
|
const commandLineOptions = new CommandLineOptions(cwd, /* fromVsCodeExtension */ true);
|
||||||
|
|
||||||
|
service.setOptions(commandLineOptions);
|
||||||
|
|
||||||
|
const fileList = service.test_getFileNamesFromFileSpecs();
|
||||||
|
const fileNames = fileList.map((p) => p.fileName).sort();
|
||||||
|
assert.deepStrictEqual(fileNames, ['sample.pyi', 'test.py']);
|
||||||
|
|
||||||
|
const configOptions = service.test_getConfigOptions(commandLineOptions);
|
||||||
|
assert.equal(configOptions.typeCheckingMode, 'strict');
|
||||||
|
});
|
||||||
|
@ -180,7 +180,13 @@ export class TestState {
|
|||||||
const configOptions = this._convertGlobalOptionsToConfigOptions(vfsInfo.projectRoot, mountPaths);
|
const configOptions = this._convertGlobalOptionsToConfigOptions(vfsInfo.projectRoot, mountPaths);
|
||||||
|
|
||||||
if (this.rawConfigJson) {
|
if (this.rawConfigJson) {
|
||||||
configOptions.initializeFromJson(this.rawConfigJson, 'standard', this.serviceProvider, testAccessHost);
|
configOptions.initializeFromJson(
|
||||||
|
this.rawConfigJson,
|
||||||
|
Uri.file(projectRoot, this.serviceProvider),
|
||||||
|
'standard',
|
||||||
|
this.serviceProvider,
|
||||||
|
testAccessHost
|
||||||
|
);
|
||||||
this._applyTestConfigOptions(configOptions);
|
this._applyTestConfigOptions(configOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import * as JSONC from 'jsonc-parser';
|
import * as JSONC from 'jsonc-parser';
|
||||||
|
|
||||||
import { configFileNames } from '../../../analyzer/service';
|
import { configFileName } from '../../../analyzer/service';
|
||||||
import { Comparison, toBoolean } from '../../../common/core';
|
import { Comparison, toBoolean } from '../../../common/core';
|
||||||
import { combinePaths, getBaseFileName } from '../../../common/pathUtils';
|
import { combinePaths, getBaseFileName } from '../../../common/pathUtils';
|
||||||
import { getStringComparer } from '../../../common/stringUtils';
|
import { getStringComparer } from '../../../common/stringUtils';
|
||||||
@ -76,5 +76,5 @@ export function getMarkerNames(testData: FourSlashData): string[] {
|
|||||||
|
|
||||||
function isConfig(file: FourSlashFile, ignoreCase: boolean): boolean {
|
function isConfig(file: FourSlashFile, ignoreCase: boolean): boolean {
|
||||||
const comparer = getStringComparer(ignoreCase);
|
const comparer = getStringComparer(ignoreCase);
|
||||||
return configFileNames.some((f) => comparer(getBaseFileName(file.fileName), f) === Comparison.EqualTo);
|
return comparer(getBaseFileName(file.fileName), configFileName) === Comparison.EqualTo;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
[tool.pyright]
|
||||||
|
extends = "sub2/baseconfig2.json"
|
||||||
|
typeCheckingMode = "strict"
|
@ -0,0 +1,2 @@
|
|||||||
|
[tool.pyright]
|
||||||
|
extends = "baseconfig1.toml"
|
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "../sub3/baseconfig3.json",
|
||||||
|
"typeCheckingMode": "standard"
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"stubPath": "stubs",
|
||||||
|
"typeCheckingMode": "basic"
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
x: int
|
@ -0,0 +1,7 @@
|
|||||||
|
# pyright: reportMissingModuleSource=false
|
||||||
|
|
||||||
|
from typing import assert_type
|
||||||
|
from sample import x
|
||||||
|
|
||||||
|
assert_type(x, int)
|
||||||
|
|
@ -23,6 +23,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"extends": {
|
||||||
|
"$id": "#/properties/extends",
|
||||||
|
"type": "string",
|
||||||
|
"title": "Path to configuration file that this configuration extends",
|
||||||
|
"pattern": "^(.*)$"
|
||||||
|
},
|
||||||
"include": {
|
"include": {
|
||||||
"$id": "#/properties/include",
|
"$id": "#/properties/include",
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
Loading…
Reference in New Issue
Block a user