Skip to content

Commit 77caeef

Browse files
feat: glob pattern support in executionEnvironments root and extraPaths
1 parent ba43b2d commit 77caeef

8 files changed

Lines changed: 351 additions & 49 deletions

File tree

docs/configuration/config-files.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ The following settings control the *environment* in which basedpyright will chec
3030

3131
- **verboseOutput** [boolean]: Specifies whether output logs should be verbose. This is useful when diagnosing certain problems like import resolution issues.
3232

33-
- **extraPaths** [array of strings, optional]: Additional search paths that will be used when searching for modules imported by files.
33+
- **extraPaths** [array of strings, optional]: Additional search paths that will be used when searching for modules imported by files. Glob patterns (`**`, `*`, `?`) are supported *(basedpyright exclusive)* — matching directories are expanded at configuration time.
3434

3535
- **pythonVersion** [string, optional]: Specifies the version of Python that will be used to execute the source code. The version should be specified as a string in the format "M.m" where M is the major version and m is the minor (e.g. `"3.0"` or `"3.6"`). If a version is provided, pyright will generate errors if the source code makes use of language features that are not supported in that version. It will also tailor its use of type stub files, which conditionalizes type definitions based on the version. If no version is specified, pyright will use the version of the current python interpreter, if one is present.
3636

@@ -314,13 +314,13 @@ The following settings allow more fine grained control over the **typeCheckingMo
314314

315315

316316
## Execution Environment Options
317-
Pyright allows multiple execution environments to be defined for different portions of your source tree. For example, a subtree may be designed to run with different import search paths or a different version of the python interpreter than the rest of the source base.
317+
Pyright allows multiple "execution environments" to be defined for different portions of your source tree. For example, a subtree may be designed to run with different import search paths or a different version of the python interpreter than the rest of the source base.
318318

319-
The following settings can be specified for each execution environment. Each source file within a project is associated with at most one execution environment -- the first one whose root directory contains that file.
319+
The following settings can be specified for each execution environment. Each source file within a project is associated with at most one execution environment -- the first one whose root matches that file. Environments are searched in array order; the first match wins.
320320

321-
- **root** [string, required]: Root path for the code that will execute within this execution environment.
321+
- **root** [string, required]: Root path for the code that will execute within this execution environment. Glob patterns (`**`, `*`, `?`) are supported *(basedpyright exclusive)* — when used, import resolution falls back to the project root.
322322

323-
- **extraPaths** [array of strings, optional]: Additional search paths (in addition to the root path) that will be used when searching for modules imported by files within this execution environment. If specified, this overrides the default extraPaths setting when resolving imports for files within this execution environment. Note that each files execution environment mapping is independent, so if file A is in one execution environment and imports a second file B within a second execution environment, any imports from B will use the extraPaths in the second execution environment.
323+
- **extraPaths** [array of strings, optional]: Additional search paths (in addition to the root path) that will be used when searching for modules imported by files within this execution environment. Glob patterns (`**`, `*`, `?`) are supported *(basedpyright exclusive)* — matching directories are expanded at configuration time. If specified, this overrides the default extraPaths setting when resolving imports for files within this execution environment. Note that each file's execution environment mapping is independent, so if file A is in one execution environment and imports a second file B within a second execution environment, any imports from B will use the extraPaths in the second execution environment.
324324

325325
- **pythonVersion** [string, optional]: The version of Python used for this execution environment. If not specified, the global `pythonVersion` setting is used instead.
326326

@@ -377,10 +377,10 @@ The following is an example of a pyright config file:
377377
]
378378
},
379379
{
380-
"root": "src/tests",
380+
"root": "**/tests",
381381
"reportPrivateUsage": false,
382382
"extraPaths": [
383-
"src/tests/e2e",
383+
"**/fixtures",
384384
"src/sdk"
385385
]
386386
},
@@ -413,7 +413,7 @@ pythonPlatform = "Linux"
413413
executionEnvironments = [
414414
{ root = "src/web", pythonVersion = "3.5", pythonPlatform = "Windows", extraPaths = [ "src/service_libs" ], reportMissingImports = "warning" },
415415
{ root = "src/sdk", pythonVersion = "3.0", extraPaths = [ "src/backend" ] },
416-
{ root = "src/tests", reportPrivateUsage = false, extraPaths = ["src/tests/e2e", "src/sdk" ]},
416+
{ root = "**/tests", reportPrivateUsage = false, extraPaths = ["**/fixtures", "src/sdk"] },
417417
{ root = "src" }
418418
]
419419
```

packages/pyright-internal/src/analyzer/backgroundAnalysisProgram.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,8 @@ export class BackgroundAnalysisProgram {
277277
}
278278

279279
private _ensurePartialStubPackages(execEnv: ExecutionEnvironment) {
280-
this._backgroundAnalysis?.ensurePartialStubPackages(execEnv.root?.toString());
280+
const execEnvIndex = this.configOptions.getExecutionEnvironments().indexOf(execEnv);
281+
this._backgroundAnalysis?.ensurePartialStubPackages(execEnvIndex);
281282
return this._importResolver.ensurePartialStubPackages(execEnv);
282283
}
283284

packages/pyright-internal/src/analyzer/importResolver.ts

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,8 @@ export class ImportResolver {
182182
return suggestions;
183183
}
184184

185-
const root = getParentImportResolutionRoot(sourceFileUri, execEnv.root);
185+
const importRoot = execEnv.root;
186+
const root = getParentImportResolutionRoot(sourceFileUri, importRoot);
186187
const origin = sourceFileUri.getDirectory();
187188

188189
let current: Uri | undefined = origin;
@@ -320,9 +321,10 @@ export class ImportResolver {
320321
detectPyTyped = false
321322
) {
322323
// Cache results of the reverse of resolveImport as we cache resolveImport.
324+
const importRoot = execEnv.root;
323325
const cache = getOrAdd(
324326
this._cachedModuleNameResults,
325-
execEnv.root?.key,
327+
importRoot?.key,
326328
() => new Map<string, ModuleImportInfo>()
327329
);
328330
const key = `${allowInvalidModuleName}.${detectPyTyped}.${fileUri.key}`;
@@ -372,8 +374,9 @@ export class ImportResolver {
372374
}
373375

374376
// The "default" workspace has a root-less execution environment; ignore it.
375-
if (execEnv.root) {
376-
roots.push(execEnv.root);
377+
const importRoot = execEnv.root;
378+
if (importRoot) {
379+
roots.push(importRoot);
377380
}
378381

379382
appendArray(roots, execEnv.extraPaths);
@@ -413,7 +416,8 @@ export class ImportResolver {
413416
return false;
414417
}
415418

416-
if (this.partialStubs.isPartialStubPackagesScanned(execEnv)) {
419+
const importRoot = execEnv.root;
420+
if (importRoot && this.partialStubs.isPathScanned(importRoot)) {
417421
return false;
418422
}
419423

@@ -423,7 +427,7 @@ export class ImportResolver {
423427

424428
// Add paths to search stub packages.
425429
addPaths(this._configOptions.stubPath);
426-
addPaths(execEnv.root ?? this._configOptions.projectRoot);
430+
addPaths(importRoot ?? this._configOptions.projectRoot);
427431
execEnv.extraPaths.forEach((p) => addPaths(p));
428432
addPaths(typeshedPathEx);
429433

@@ -591,7 +595,8 @@ export class ImportResolver {
591595
}
592596

593597
// Check whether the given file is in the parent directory import resolution cache.
594-
const root = getParentImportResolutionRoot(sourceFileUri, execEnv.root);
598+
const importRoot = execEnv.root;
599+
const root = getParentImportResolutionRoot(sourceFileUri, importRoot);
595600
if (!this.cachedParentImportResults.checkValidPath(this.fileSystem, sourceFileUri, root)) {
596601
return importResult;
597602
}
@@ -718,7 +723,8 @@ export class ImportResolver {
718723
// If the import is relative, include the source file path in the key.
719724
const relativeSourceFileUri = moduleDescriptor && moduleDescriptor.leadingDots > 0 ? sourceFileUri : undefined;
720725

721-
getOrAdd(this._cachedImportResults, execEnv.root?.key, () => new Map<string, ImportResult>()).set(
726+
const importRoot = execEnv.root;
727+
getOrAdd(this._cachedImportResults, importRoot?.key, () => new Map<string, ImportResult>()).set(
722728
this._getImportCacheKey(relativeSourceFileUri, importName, fromUserFile),
723729
importResult
724730
);
@@ -1117,11 +1123,12 @@ export class ImportResolver {
11171123
}
11181124

11191125
// Look for it in the root directory of the execution environment.
1120-
if (execEnv.root) {
1126+
const importRoot = execEnv.root;
1127+
if (importRoot) {
11211128
this._getCompletionSuggestionsAbsolute(
11221129
sourceFileUri,
11231130
execEnv,
1124-
execEnv.root,
1131+
importRoot,
11251132
moduleDescriptor,
11261133
suggestions
11271134
);
@@ -1221,8 +1228,9 @@ export class ImportResolver {
12211228
}
12221229

12231230
// Look for it in the root directory of the execution environment.
1224-
if (execEnv.root) {
1225-
const candidateModuleNameInfo = _getModuleNameInfoFromPath(execEnv.root, fileUri);
1231+
const importRoot = execEnv.root;
1232+
if (importRoot) {
1233+
const candidateModuleNameInfo = _getModuleNameInfoFromPath(importRoot, fileUri);
12261234

12271235
if (candidateModuleNameInfo) {
12281236
if (candidateModuleNameInfo.containsInvalidCharacters) {
@@ -1330,7 +1338,7 @@ export class ImportResolver {
13301338
}
13311339

13321340
if (detectPyTyped && importType === ImportType.ThirdParty) {
1333-
const root = getParentImportResolutionRoot(fileUri, execEnv.root);
1341+
const root = getParentImportResolutionRoot(fileUri, importRoot);
13341342

13351343
// Go up directories one by one looking for a py.typed file.
13361344
let current: Uri | undefined = fileUri.getDirectory();
@@ -1580,7 +1588,8 @@ export class ImportResolver {
15801588
moduleDescriptor: ImportedModuleDescriptor,
15811589
fromUserFile: boolean
15821590
) {
1583-
const cacheForExecEnv = this._cachedImportResults.get(execEnv.root?.key ?? '');
1591+
const importRoot = execEnv.root;
1592+
const cacheForExecEnv = this._cachedImportResults.get(importRoot?.key ?? '');
15841593
if (!cacheForExecEnv) {
15851594
return undefined;
15861595
}
@@ -1669,12 +1678,13 @@ export class ImportResolver {
16691678
let localImport: ImportResult | undefined;
16701679

16711680
// Look for it in the root directory of the execution environment.
1672-
if (execEnv.root) {
1673-
importLogger?.log(`Looking in root directory of execution environment ` + `'${execEnv.root}'`);
1681+
const importRoot = execEnv.root;
1682+
if (importRoot) {
1683+
importLogger?.log(`Looking in root directory of execution environment ` + `'${importRoot}'`);
16741684

16751685
localImport = this.resolveAbsoluteImport(
16761686
sourceFileUri,
1677-
execEnv.root,
1687+
importRoot,
16781688
execEnv,
16791689
moduleDescriptor,
16801690
importName,
@@ -1756,7 +1766,7 @@ export class ImportResolver {
17561766
// If a library is fully py.typed, then we have found the best match,
17571767
// unless the execution environment is typeshed itself, in which case
17581768
// we don't want to favor py.typed libraries. Use the typeshed lookup below.
1759-
if (execEnv.root !== this._getTypeshedRoot(this._configOptions.typeshedPath, importLogger)) {
1769+
if (importRoot !== this._getTypeshedRoot(this._configOptions.typeshedPath, importLogger)) {
17601770
if (bestResultSoFar?.pyTypedInfo && !bestResultSoFar.isPartlyResolved) {
17611771
return bestResultSoFar;
17621772
}

packages/pyright-internal/src/backgroundAnalysisBase.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export interface IBackgroundAnalysis extends Disposable {
5555
setConfigOptions(configOptions: ConfigOptions): void;
5656
setTrackedFiles(fileUris: Uri[]): void;
5757
setAllowedThirdPartyImports(importNames: string[]): void;
58-
ensurePartialStubPackages(executionRoot: string | undefined): void;
58+
ensurePartialStubPackages(execEnvIndex: number): void;
5959
setFileOpened(fileUri: Uri, version: number | null, contents: string, options: OpenFileOptions): void;
6060
updateChainedUri(fileUri: Uri, chainedUri: Uri | undefined): void;
6161
setFileClosed(fileUri: Uri, isTracked?: boolean): void;
@@ -158,8 +158,8 @@ export class BackgroundAnalysisBase implements IBackgroundAnalysis {
158158
this.enqueueRequest({ requestType: 'setAllowedThirdPartyImports', data: serialize(importNames) });
159159
}
160160

161-
ensurePartialStubPackages(executionRoot: string | undefined) {
162-
this.enqueueRequest({ requestType: 'ensurePartialStubPackages', data: serialize({ executionRoot }) });
161+
ensurePartialStubPackages(execEnvIndex: number) {
162+
this.enqueueRequest({ requestType: 'ensurePartialStubPackages', data: serialize({ execEnvIndex }) });
163163
}
164164

165165
setFileOpened(fileUri: Uri, version: number | null, contents: string, options: OpenFileOptions) {
@@ -589,8 +589,8 @@ export abstract class BackgroundAnalysisRunnerBase extends BackgroundThreadBase
589589
}
590590

591591
case 'ensurePartialStubPackages': {
592-
const { executionRoot } = deserialize(msg.data);
593-
this.handleEnsurePartialStubPackages(executionRoot);
592+
const { execEnvIndex } = deserialize(msg.data);
593+
this.handleEnsurePartialStubPackages(execEnvIndex);
594594
break;
595595
}
596596

@@ -775,10 +775,8 @@ export abstract class BackgroundAnalysisRunnerBase extends BackgroundThreadBase
775775
this.program.setAllowedThirdPartyImports(importNames);
776776
}
777777

778-
protected handleEnsurePartialStubPackages(executionRoot: string | undefined) {
779-
const execEnv = this._configOptions
780-
.getExecutionEnvironments()
781-
.find((e) => e.root?.toString() === executionRoot);
778+
protected handleEnsurePartialStubPackages(execEnvIndex: number) {
779+
const execEnv = this._configOptions.getExecutionEnvironments()[execEnvIndex];
782780
if (execEnv) {
783781
this.importResolver.ensurePartialStubPackages(execEnv);
784782
}

packages/pyright-internal/src/common/configOptions.ts

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
* Class that holds the configuration options for the analyzer.
88
*/
99

10+
import { globSync } from 'node:fs';
11+
import { matchesGlob } from 'node:path/posix';
12+
1013
import { ImportLogger } from '../analyzer/importLogger';
1114
import { getPathsFromPthFiles } from '../analyzer/pythonPathUtils';
1215
import * as pathConsts from '../common/pathConsts';
@@ -27,7 +30,7 @@ import { PythonVersion, latestStablePythonVersion } from './pythonVersion';
2730
import { ServiceKeys } from './serviceKeys';
2831
import { ServiceProvider } from './serviceProvider';
2932
import { Uri } from './uri/uri';
30-
import { FileSpec, getFileSpec, isDirectory } from './uri/uriUtils';
33+
import { FileSpec, getFileSpec, getWildcardRoot, isDirectory } from './uri/uriUtils';
3134
import { userFacingOptionsList } from './stringUtils';
3235

3336
// prevent upstream changes from sneaking in and adding errors using console.error,
@@ -66,6 +69,8 @@ export class ExecutionEnvironment {
6669
// tools or playgrounds).
6770
skipNativeLibraries: boolean;
6871

72+
rootGlob?: string;
73+
6974
// Default to "." which indicates every file in the project.
7075
constructor(
7176
name: string,
@@ -1526,13 +1531,21 @@ export class ConfigOptions {
15261531
// execution environment is used.
15271532
findExecEnvironment(file: Uri): ExecutionEnvironment {
15281533
return (
1529-
this.executionEnvironments.find((env) => {
1530-
const envRoot = Uri.is(env.root) ? env.root : this.projectRoot.resolvePaths(env.root || '');
1531-
return file.startsWith(envRoot);
1532-
}) ?? this.getDefaultExecEnvironment()
1534+
this.executionEnvironments.find((env) => this._fileMatchesEnvironment(file, env)) ??
1535+
this.getDefaultExecEnvironment()
15331536
);
15341537
}
15351538

1539+
private _fileMatchesEnvironment(file: Uri, env: ExecutionEnvironment): boolean {
1540+
if (env.rootGlob !== undefined) {
1541+
const relative = this.projectRoot.getRelativePath(file)?.slice(2);
1542+
if (relative === undefined) return false;
1543+
return matchesGlob(relative, env.rootGlob + '/**');
1544+
}
1545+
const envRoot = Uri.is(env.root) ? env.root : this.projectRoot.resolvePaths(env.root || '');
1546+
return file.startsWith(envRoot);
1547+
}
1548+
15361549
getExecutionEnvironments(): ExecutionEnvironment[] {
15371550
if (this.executionEnvironments.length > 0) {
15381551
return this.executionEnvironments;
@@ -1691,7 +1704,7 @@ export class ConfigOptions {
16911704
if (typeof path !== 'string') {
16921705
console.error(`Config "extraPaths" field ${pathIndex} must be a string.`);
16931706
} else {
1694-
configExtraPaths!.push(configDirUri.resolvePaths(path));
1707+
this._resolveExtraPath(path, configDirUri, configExtraPaths, console);
16951708
}
16961709
});
16971710
this.defaultExtraPaths = [...configExtraPaths];
@@ -1989,6 +2002,20 @@ export class ConfigOptions {
19892002
return this.pythonEnvironmentName || this.pythonPath?.toString() || 'python';
19902003
}
19912004

2005+
private _resolveExtraPath(path: string, configDirUri: Uri, out: Uri[], console: ConsoleInterface) {
2006+
if (/[*?]/.test(path)) {
2007+
try {
2008+
for (const match of globSync(path, { cwd: configDirUri.getFilePath() })) {
2009+
out.push(configDirUri.resolvePaths(match));
2010+
}
2011+
} catch (e) {
2012+
console.error(`Failed to expand glob pattern "${path}": ${e}`);
2013+
}
2014+
} else {
2015+
out.push(configDirUri.resolvePaths(path));
2016+
}
2017+
}
2018+
19922019
private _convertBoolean(value: any, fieldName: string, defaultValue: boolean): boolean {
19932020
if (value === undefined) {
19942021
return defaultValue;
@@ -2043,7 +2070,8 @@ export class ConfigOptions {
20432070

20442071
// Validate the root.
20452072
if (envObj.root && typeof envObj.root === 'string') {
2046-
newExecEnv.root = configDirUri.resolvePaths(envObj.root);
2073+
newExecEnv.rootGlob = envObj.root;
2074+
newExecEnv.root = getWildcardRoot(configDirUri, envObj.root);
20472075
} else {
20482076
console.error(`Config executionEnvironments index ${index}: missing root value.`);
20492077
}
@@ -2067,7 +2095,7 @@ export class ConfigOptions {
20672095
` extraPaths field ${pathIndex} must be a string.`
20682096
);
20692097
} else {
2070-
newExecEnv.extraPaths.push(configDirUri.resolvePaths(path));
2098+
this._resolveExtraPath(path, configDirUri, newExecEnv.extraPaths, console);
20712099
}
20722100
});
20732101
}

0 commit comments

Comments
 (0)