From 421dabee57387af0d69df1e5601d794b6740d3e0 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Fri, 24 May 2024 17:22:12 -0700 Subject: [PATCH] Added support for configuration inheritance through an "extends" configuration option. This addresses #4366. (#7997) --- docs/configuration.md | 2 + .../pyright-internal/src/analyzer/service.ts | 120 +++++++++++++----- .../src/common/configOptions.ts | 59 ++++++--- .../fileWatcherDynamicFeature.ts | 8 +- .../pyright-internal/src/tests/config.test.ts | 22 +++- .../src/tests/harness/fourslash/testState.ts | 8 +- .../tests/harness/fourslash/testStateUtils.ts | 4 +- .../baseconfig1.toml | 3 + .../pyproject.toml | 2 + .../sub2/baseconfig2.json | 4 + .../sub3/baseconfig3.json | 4 + .../sub3/stubs/sample.pyi | 1 + .../project_with_extended_config/test.py | 7 + .../schemas/pyrightconfig.schema.json | 6 + 14 files changed, 187 insertions(+), 63 deletions(-) create mode 100644 packages/pyright-internal/src/tests/samples/project_with_extended_config/baseconfig1.toml create mode 100644 packages/pyright-internal/src/tests/samples/project_with_extended_config/pyproject.toml create mode 100644 packages/pyright-internal/src/tests/samples/project_with_extended_config/sub2/baseconfig2.json create mode 100644 packages/pyright-internal/src/tests/samples/project_with_extended_config/sub3/baseconfig3.json create mode 100644 packages/pyright-internal/src/tests/samples/project_with_extended_config/sub3/stubs/sample.pyi create mode 100644 packages/pyright-internal/src/tests/samples/project_with_extended_config/test.py diff --git a/docs/configuration.md b/docs/configuration.md index a6791c2de..7e408276a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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). +- **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. - **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. diff --git a/packages/pyright-internal/src/analyzer/service.ts b/packages/pyright-internal/src/analyzer/service.ts index d10e8dbcc..d2e659eb5 100644 --- a/packages/pyright-internal/src/analyzer/service.ts +++ b/packages/pyright-internal/src/analyzer/service.ts @@ -54,7 +54,7 @@ import { MaxAnalysisTime, Program } from './program'; import { findPythonSearchPaths } from './pythonPathUtils'; import { IPythonMode } from './sourceFile'; -export const configFileNames = ['pyrightconfig.json']; +export const configFileName = 'pyrightconfig.json'; export const pyprojectTomlName = 'pyproject.toml'; // How long since the last user activity should we wait until running @@ -84,6 +84,11 @@ export interface AnalyzerServiceOptions { fileSystem?: FileSystem; } +interface ConfigFileContents { + configFileDirUri: Uri; + configFileJsonObj: object; +} + // Hold uniqueId for this service. It can be used to distinguish each service later. let _nextServiceId = 1; @@ -103,7 +108,8 @@ export class AnalyzerService { private _sourceFileWatcher: FileWatcher | undefined; private _reloadConfigTimer: any; private _libraryReanalysisTimer: any; - private _configFileUri: Uri | undefined; + private _primaryConfigFileUri: Uri | undefined; + private _extendedConfigFileUris: Uri[] = []; private _configFileWatcher: FileWatcher | undefined; private _libraryFileWatcher: FileWatcher | undefined; private _librarySearchUrisToWatch: Uri[] | undefined; @@ -534,11 +540,12 @@ export class AnalyzerService { ? Uri.file(commandLineOptions.configFilePath, this.serviceProvider, /* checkRelative */ true) : projectRoot.resolvePaths(commandLineOptions.configFilePath) ); + if (!this.fs.existsSync(configFilePath)) { this._console.info(`Configuration file not found at ${configFilePath.toUserVisibleString()}.`); configFilePath = projectRoot; } else { - if (configFilePath.lastExtension.endsWith('.json')) { + if (configFilePath.lastExtension.endsWith('.json') || configFilePath.lastExtension.endsWith('.toml')) { projectRoot = configFilePath.getDirectory(); } else { projectRoot = configFilePath; @@ -648,30 +655,23 @@ export class AnalyzerService { } } - this._configFileUri = configFilePath || pyprojectFilePath; - configOptions.disableTaggedHints = !!commandLineOptions.disableTaggedHints; - // 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.toUserVisibleString()}`); - configJsonObj = this._parseJsonConfigFile(configFilePath); - } else if (pyprojectFilePath) { - this._console.info(`Loading pyproject.toml file at ${pyprojectFilePath.toUserVisibleString()}`); - configJsonObj = this._parsePyprojectTomlFile(pyprojectFilePath); - } + const configs = this._getExtendedConfigurations(configFilePath ?? pyprojectFilePath); - if (configJsonObj) { - configOptions.initializeFromJson( - configJsonObj, - this._typeCheckingMode, - this.serviceProvider, - host, - commandLineOptions - ); + if (configs) { + for (const config of configs) { + configOptions.initializeFromJson( + config.configFileJsonObj, + config.configFileDirUri, + this._typeCheckingMode, + 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 // the project should be included. @@ -860,6 +860,61 @@ export class AnalyzerService { 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() { const stubPath = this._configOptions.stubPath ?? @@ -914,12 +969,11 @@ export class AnalyzerService { } private _findConfigFile(searchPath: Uri): Uri | undefined { - for (const name of configFileNames) { - const fileName = searchPath.resolvePaths(name); - if (this.fs.existsSync(fileName)) { - return this.fs.realCasePath(fileName); - } + const fileName = searchPath.resolvePaths(configFileName); + if (this.fs.existsSync(fileName)) { + return this.fs.realCasePath(fileName); } + return undefined; } @@ -1573,8 +1627,8 @@ export class AnalyzerService { return; } - if (this._configFileUri) { - this._configFileWatcher = this.fs.createFileSystemWatcher([this._configFileUri], (event) => { + if (this._primaryConfigFileUri) { + this._configFileWatcher = this.fs.createFileSystemWatcher(this._extendedConfigFileUris, (event) => { if (this._verboseOutput) { this._console.info(`Received fs event '${event}' for config file`); } @@ -1588,7 +1642,7 @@ export class AnalyzerService { if (event === 'add' || event === 'change') { const fileName = getFileName(path); - if (fileName && configFileNames.some((name) => name === fileName)) { + if (fileName === configFileName) { if (this._verboseOutput) { this._console.info(`Received fs event '${event}' for config file`); } @@ -1624,8 +1678,8 @@ export class AnalyzerService { private _reloadConfigFile() { this._updateConfigFileWatcher(); - if (this._configFileUri) { - this._console.info(`Reloading configuration file at ${this._configFileUri.toUserVisibleString()}`); + if (this._primaryConfigFileUri) { + this._console.info(`Reloading configuration file at ${this._primaryConfigFileUri.toUserVisibleString()}`); const host = this._backgroundAnalysisProgram.host; diff --git a/packages/pyright-internal/src/common/configOptions.ts b/packages/pyright-internal/src/common/configOptions.ts index 88f7aa14f..384a62d46 100644 --- a/packages/pyright-internal/src/common/configOptions.ts +++ b/packages/pyright-internal/src/common/configOptions.ts @@ -19,8 +19,8 @@ import { DiagnosticRule } from './diagnosticRules'; import { FileSystem } from './fileSystem'; import { Host } from './host'; import { PythonVersion, latestStablePythonVersion } from './pythonVersion'; -import { ServiceProvider } from './serviceProvider'; import { ServiceKeys } from './serviceKeys'; +import { ServiceProvider } from './serviceProvider'; import { Uri } from './uri/uri'; import { FileSpec, getFileSpec, isDirectory } from './uri/uriUtils'; @@ -1104,6 +1104,7 @@ export class ConfigOptions { // Initialize the structure from a JSON object. initializeFromJson( configObj: any, + configDirUri: Uri, typeCheckingMode: string | undefined, serviceProvider: ServiceProvider, host: Host, @@ -1125,7 +1126,7 @@ export class ConfigOptions { } else if (isAbsolute(fileSpec)) { console.error(`Ignoring path "${fileSpec}" in "include" array because it is not relative.`); } 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)) { console.error(`Ignoring path "${fileSpec}" in "exclude" array because it is not relative.`); } 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)) { console.error(`Ignoring path "${fileSpec}" in "ignore" array because it is not relative.`); } 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)) { console.error(`Ignoring path "${fileSpec}" in "strict" array because it is not relative.`); } 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. - let configTypeCheckingMode: string | undefined; + let configTypeCheckingMode: string | undefined = this.typeCheckingMode; if (configObj.typeCheckingMode !== undefined) { if ( configObj.typeCheckingMode === 'off' || @@ -1239,17 +1240,15 @@ export class ConfigOptions { }); // Read the "venvPath". - this.venvPath = undefined; if (configObj.venvPath !== undefined) { if (typeof configObj.venvPath !== 'string') { console.error(`Config "venvPath" field must contain a string.`); } else { - this.venvPath = this.projectRoot.resolvePaths(configObj.venvPath); + this.venvPath = configDirUri.resolvePaths(configObj.venvPath); } } // Read the "venv" name. - this.venv = undefined; if (configObj.venv !== undefined) { if (typeof configObj.venv !== 'string') { console.error(`Config "venv" field must contain a string.`); @@ -1269,7 +1268,7 @@ export class ConfigOptions { if (typeof path !== 'string') { console.error(`Config "extraPaths" field ${pathIndex} must be a string.`); } 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); // Read the "typeshedPath" setting. - this.typeshedPath = undefined; if (configObj.typeshedPath !== undefined) { if (typeof configObj.typeshedPath !== 'string') { console.error(`Config "typeshedPath" field must contain a string.`); } else { this.typeshedPath = configObj.typeshedPath - ? this.projectRoot.resolvePaths(configObj.typeshedPath) + ? configDirUri.resolvePaths(configObj.typeshedPath) : undefined; } } // Read the "stubPath" setting. - this.stubPath = undefined; // Keep this for backward compatibility if (configObj.typingsPath !== undefined) { @@ -1332,7 +1329,7 @@ export class ConfigOptions { console.error(`Config "typingsPath" field must contain a string.`); } else { 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') { console.error(`Config "stubPath" field must contain a string.`); } 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 // after we've established default values. - this.executionEnvironments = []; if (configObj.executionEnvironments !== undefined) { if (!Array.isArray(configObj.executionEnvironments)) { console.error(`Config "executionEnvironments" field must contain an array.`); } else { + this.executionEnvironments = []; + const execEnvironments = configObj.executionEnvironments as ExecutionEnvironment[]; + execEnvironments.forEach((env, index) => { - const execEnv = this._initExecutionEnvironmentFromJson(env, index, console, commandLineOptions); + const execEnv = this._initExecutionEnvironmentFromJson( + env, + configDirUri, + index, + console, + commandLineOptions + ); + if (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) { // If no default python platform was specified, assume that the // user wants to use the current platform. @@ -1552,6 +1570,7 @@ export class ConfigOptions { private _initExecutionEnvironmentFromJson( envObj: any, + configDirUri: Uri, index: number, console: ConsoleInterface, commandLineOptions?: CommandLineOptions @@ -1559,7 +1578,7 @@ export class ConfigOptions { try { const newExecEnv = new ExecutionEnvironment( this._getEnvironmentName(), - this.projectRoot, + configDirUri, this.defaultPythonVersion, this.defaultPythonPlatform, this.defaultExtraPaths @@ -1567,7 +1586,7 @@ export class ConfigOptions { // Validate the root. if (envObj.root && typeof envObj.root === 'string') { - newExecEnv.root = this.projectRoot.resolvePaths(envObj.root); + newExecEnv.root = configDirUri.resolvePaths(envObj.root); } else { console.error(`Config executionEnvironments index ${index}: missing root value.`); } @@ -1587,7 +1606,7 @@ export class ConfigOptions { ` extraPaths field ${pathIndex} must be a string.` ); } else { - newExecEnv.extraPaths.push(this.projectRoot.resolvePaths(path)); + newExecEnv.extraPaths.push(configDirUri.resolvePaths(path)); } }); } diff --git a/packages/pyright-internal/src/languageService/fileWatcherDynamicFeature.ts b/packages/pyright-internal/src/languageService/fileWatcherDynamicFeature.ts index d2a7a6d39..b03d192e3 100644 --- a/packages/pyright-internal/src/languageService/fileWatcherDynamicFeature.ts +++ b/packages/pyright-internal/src/languageService/fileWatcherDynamicFeature.ts @@ -12,11 +12,11 @@ import { FileSystemWatcher, WatchKind, } from 'vscode-languageserver'; -import { DynamicFeature } from './dynamicFeature'; -import { configFileNames } from '../analyzer/service'; +import { configFileName } from '../analyzer/service'; +import { FileSystem } from '../common/fileSystem'; import { deduplicateFolders, isFile } from '../common/uri/uriUtils'; import { WorkspaceFactory } from '../workspaceFactory'; -import { FileSystem } from '../common/fileSystem'; +import { DynamicFeature } from './dynamicFeature'; export class FileWatcherDynamicFeature extends DynamicFeature { constructor( @@ -33,7 +33,7 @@ export class FileWatcherDynamicFeature extends DynamicFeature { // Set default (config files and all workspace files) first. const watchers: FileSystemWatcher[] = [ - ...configFileNames.map((fileName) => ({ globPattern: `**/${fileName}`, kind: watchKind })), + { globPattern: `**/${configFileName}`, kind: watchKind }, { globPattern: '**', kind: watchKind }, ]; diff --git a/packages/pyright-internal/src/tests/config.test.ts b/packages/pyright-internal/src/tests/config.test.ts index 548ef344e..6fcbdb334 100644 --- a/packages/pyright-internal/src/tests/config.test.ts +++ b/packages/pyright-internal/src/tests/config.test.ts @@ -216,7 +216,7 @@ test('PythonPlatform', () => { const nullConsole = new 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]; assert.strictEqual(env.pythonPlatform, 'platform'); @@ -340,9 +340,10 @@ test('verify config fileSpecs after cloning', () => { 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()); - config.initializeFromJson(configFile, undefined, sp, new TestAccessHost()); + config.initializeFromJson(configFile, rootUri, undefined, sp, new TestAccessHost()); const cloned = deserialize(serialize(config)); assert.deepEqual(config.ignore, cloned.ignore); @@ -371,3 +372,18 @@ test('extra paths on undefined execution root/default workspace', () => { 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'); +}); diff --git a/packages/pyright-internal/src/tests/harness/fourslash/testState.ts b/packages/pyright-internal/src/tests/harness/fourslash/testState.ts index a84841d1f..75d4b49e8 100644 --- a/packages/pyright-internal/src/tests/harness/fourslash/testState.ts +++ b/packages/pyright-internal/src/tests/harness/fourslash/testState.ts @@ -180,7 +180,13 @@ export class TestState { const configOptions = this._convertGlobalOptionsToConfigOptions(vfsInfo.projectRoot, mountPaths); 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); } diff --git a/packages/pyright-internal/src/tests/harness/fourslash/testStateUtils.ts b/packages/pyright-internal/src/tests/harness/fourslash/testStateUtils.ts index 9bbde1b40..53c2b1d09 100644 --- a/packages/pyright-internal/src/tests/harness/fourslash/testStateUtils.ts +++ b/packages/pyright-internal/src/tests/harness/fourslash/testStateUtils.ts @@ -9,7 +9,7 @@ import assert from 'assert'; import * as JSONC from 'jsonc-parser'; -import { configFileNames } from '../../../analyzer/service'; +import { configFileName } from '../../../analyzer/service'; import { Comparison, toBoolean } from '../../../common/core'; import { combinePaths, getBaseFileName } from '../../../common/pathUtils'; import { getStringComparer } from '../../../common/stringUtils'; @@ -76,5 +76,5 @@ export function getMarkerNames(testData: FourSlashData): string[] { function isConfig(file: FourSlashFile, ignoreCase: boolean): boolean { const comparer = getStringComparer(ignoreCase); - return configFileNames.some((f) => comparer(getBaseFileName(file.fileName), f) === Comparison.EqualTo); + return comparer(getBaseFileName(file.fileName), configFileName) === Comparison.EqualTo; } diff --git a/packages/pyright-internal/src/tests/samples/project_with_extended_config/baseconfig1.toml b/packages/pyright-internal/src/tests/samples/project_with_extended_config/baseconfig1.toml new file mode 100644 index 000000000..face76340 --- /dev/null +++ b/packages/pyright-internal/src/tests/samples/project_with_extended_config/baseconfig1.toml @@ -0,0 +1,3 @@ +[tool.pyright] +extends = "sub2/baseconfig2.json" +typeCheckingMode = "strict" diff --git a/packages/pyright-internal/src/tests/samples/project_with_extended_config/pyproject.toml b/packages/pyright-internal/src/tests/samples/project_with_extended_config/pyproject.toml new file mode 100644 index 000000000..784825fbc --- /dev/null +++ b/packages/pyright-internal/src/tests/samples/project_with_extended_config/pyproject.toml @@ -0,0 +1,2 @@ +[tool.pyright] +extends = "baseconfig1.toml" diff --git a/packages/pyright-internal/src/tests/samples/project_with_extended_config/sub2/baseconfig2.json b/packages/pyright-internal/src/tests/samples/project_with_extended_config/sub2/baseconfig2.json new file mode 100644 index 000000000..65c0afdf0 --- /dev/null +++ b/packages/pyright-internal/src/tests/samples/project_with_extended_config/sub2/baseconfig2.json @@ -0,0 +1,4 @@ +{ + "extends": "../sub3/baseconfig3.json", + "typeCheckingMode": "standard" +} \ No newline at end of file diff --git a/packages/pyright-internal/src/tests/samples/project_with_extended_config/sub3/baseconfig3.json b/packages/pyright-internal/src/tests/samples/project_with_extended_config/sub3/baseconfig3.json new file mode 100644 index 000000000..33f7b9171 --- /dev/null +++ b/packages/pyright-internal/src/tests/samples/project_with_extended_config/sub3/baseconfig3.json @@ -0,0 +1,4 @@ +{ + "stubPath": "stubs", + "typeCheckingMode": "basic" +} diff --git a/packages/pyright-internal/src/tests/samples/project_with_extended_config/sub3/stubs/sample.pyi b/packages/pyright-internal/src/tests/samples/project_with_extended_config/sub3/stubs/sample.pyi new file mode 100644 index 000000000..957223fd3 --- /dev/null +++ b/packages/pyright-internal/src/tests/samples/project_with_extended_config/sub3/stubs/sample.pyi @@ -0,0 +1 @@ +x: int diff --git a/packages/pyright-internal/src/tests/samples/project_with_extended_config/test.py b/packages/pyright-internal/src/tests/samples/project_with_extended_config/test.py new file mode 100644 index 000000000..80c06c44f --- /dev/null +++ b/packages/pyright-internal/src/tests/samples/project_with_extended_config/test.py @@ -0,0 +1,7 @@ +# pyright: reportMissingModuleSource=false + +from typing import assert_type +from sample import x + +assert_type(x, int) + diff --git a/packages/vscode-pyright/schemas/pyrightconfig.schema.json b/packages/vscode-pyright/schemas/pyrightconfig.schema.json index 42c5de59e..94c43be98 100644 --- a/packages/vscode-pyright/schemas/pyrightconfig.schema.json +++ b/packages/vscode-pyright/schemas/pyrightconfig.schema.json @@ -23,6 +23,12 @@ } }, "properties": { + "extends": { + "$id": "#/properties/extends", + "type": "string", + "title": "Path to configuration file that this configuration extends", + "pattern": "^(.*)$" + }, "include": { "$id": "#/properties/include", "type": "array",