diff --git a/packages/pyright-internal/src/analyzer/backgroundAnalysisProgram.ts b/packages/pyright-internal/src/analyzer/backgroundAnalysisProgram.ts index 6774a7eb9164..6ce09844ffee 100644 --- a/packages/pyright-internal/src/analyzer/backgroundAnalysisProgram.ts +++ b/packages/pyright-internal/src/analyzer/backgroundAnalysisProgram.ts @@ -54,6 +54,10 @@ export class BackgroundAnalysisProgram { this._backgroundAnalysis?.setProgramView(this._program); } + get serviceProvider() { + return this._serviceProvider; + } + get configOptions() { return this._configOptions; } diff --git a/packages/pyright-internal/src/analyzer/importResolver.ts b/packages/pyright-internal/src/analyzer/importResolver.ts index 98f40af556fc..081537b48b67 100644 --- a/packages/pyright-internal/src/analyzer/importResolver.ts +++ b/packages/pyright-internal/src/analyzer/importResolver.ts @@ -819,6 +819,101 @@ export class ImportResolver { return newImportResult; } + protected findImplicitImports( + importingModuleName: string, + dirPath: Uri, + exclusions: Uri[] + ): Map { + const implicitImportMap = new Map(); + + // Enumerate all of the files and directories in the path, expanding links. + const entries = getFileSystemEntriesFromDirEntries( + this.readdirEntriesCached(dirPath), + this.fileSystem, + dirPath + ); + + // Add implicit file-based modules. + for (const filePath of entries.files) { + const fileExt = filePath.lastExtension; + let strippedFileName: string; + let isNativeLib = false; + + if (fileExt === '.py' || fileExt === '.pyi') { + strippedFileName = stripFileExtension(filePath.fileName); + } else if ( + _isNativeModuleFileExtension(fileExt) && + !this.fileExistsCached(filePath.packageUri) && + !this.fileExistsCached(filePath.packageStubUri) + ) { + // Native module. + strippedFileName = filePath.stripAllExtensions().fileName; + isNativeLib = true; + } else { + continue; + } + + if (!exclusions.find((exclusion) => exclusion.equals(filePath))) { + const implicitImport: ImplicitImport = { + isStubFile: filePath.hasExtension('.pyi'), + isNativeLib, + name: strippedFileName, + uri: filePath, + }; + + // Always prefer stub files over non-stub files. + const entry = implicitImportMap.get(implicitImport.name); + if (!entry || !entry.isStubFile) { + // Try resolving resolving native lib to a custom stub. + if (isNativeLib) { + const nativeLibPath = filePath; + const nativeStubPath = this.resolveNativeImportEx( + nativeLibPath, + `${importingModuleName}.${strippedFileName}`, + [] + ); + if (nativeStubPath) { + implicitImport.uri = nativeStubPath; + implicitImport.isNativeLib = false; + } + } + implicitImportMap.set(implicitImport.name, implicitImport); + } + } + } + + // Add implicit directory-based modules. + for (const dirPath of entries.directories) { + const pyFilePath = dirPath.initPyUri; + const pyiFilePath = dirPath.initPyiUri; + let isStubFile = false; + let path: Uri | undefined; + + if (this.fileExistsCached(pyiFilePath)) { + isStubFile = true; + path = pyiFilePath; + } else if (this.fileExistsCached(pyFilePath)) { + path = pyFilePath; + } + + if (path) { + if (!exclusions.find((exclusion) => exclusion.equals(path))) { + const implicitImport: ImplicitImport = { + isStubFile, + isNativeLib: false, + name: dirPath.fileName, + uri: path, + pyTypedInfo: this._getPyTypedInfo(dirPath), + }; + + implicitImportMap.set(implicitImport.name, implicitImport); + } + } + } + + return implicitImportMap; + } + private _resolveImportStrict( importName: string, sourceFileUri: Uri, @@ -1279,7 +1374,7 @@ export class ImportResolver { isNamespacePackage = true; } - implicitImports = this._findImplicitImports(importName, dirPath, [pyFilePath, pyiFilePath]); + implicitImports = this.findImplicitImports(importName, dirPath, [pyFilePath, pyiFilePath]); } else { for (let i = 0; i < moduleDescriptor.nameParts.length; i++) { const isFirstPart = i === 0; @@ -1327,7 +1422,7 @@ export class ImportResolver { continue; } - implicitImports = this._findImplicitImports(moduleDescriptor.nameParts.join('.'), dirPath, [ + implicitImports = this.findImplicitImports(moduleDescriptor.nameParts.join('.'), dirPath, [ pyFilePath, pyiFilePath, ]); @@ -1380,7 +1475,7 @@ export class ImportResolver { resolvedPaths.push(Uri.empty()); if (isLastPart) { - implicitImports = this._findImplicitImports(importName, dirPath, [pyFilePath, pyiFilePath]); + implicitImports = this.findImplicitImports(importName, dirPath, [pyFilePath, pyiFilePath]); isNamespacePackage = true; } } @@ -2544,101 +2639,6 @@ export class ImportResolver { return true; } - private _findImplicitImports( - importingModuleName: string, - dirPath: Uri, - exclusions: Uri[] - ): Map { - const implicitImportMap = new Map(); - - // Enumerate all of the files and directories in the path, expanding links. - const entries = getFileSystemEntriesFromDirEntries( - this.readdirEntriesCached(dirPath), - this.fileSystem, - dirPath - ); - - // Add implicit file-based modules. - for (const filePath of entries.files) { - const fileExt = filePath.lastExtension; - let strippedFileName: string; - let isNativeLib = false; - - if (fileExt === '.py' || fileExt === '.pyi') { - strippedFileName = stripFileExtension(filePath.fileName); - } else if ( - _isNativeModuleFileExtension(fileExt) && - !this.fileExistsCached(filePath.packageUri) && - !this.fileExistsCached(filePath.packageStubUri) - ) { - // Native module. - strippedFileName = filePath.stripAllExtensions().fileName; - isNativeLib = true; - } else { - continue; - } - - if (!exclusions.find((exclusion) => exclusion.equals(filePath))) { - const implicitImport: ImplicitImport = { - isStubFile: filePath.hasExtension('.pyi'), - isNativeLib, - name: strippedFileName, - uri: filePath, - }; - - // Always prefer stub files over non-stub files. - const entry = implicitImportMap.get(implicitImport.name); - if (!entry || !entry.isStubFile) { - // Try resolving resolving native lib to a custom stub. - if (isNativeLib) { - const nativeLibPath = filePath; - const nativeStubPath = this.resolveNativeImportEx( - nativeLibPath, - `${importingModuleName}.${strippedFileName}`, - [] - ); - if (nativeStubPath) { - implicitImport.uri = nativeStubPath; - implicitImport.isNativeLib = false; - } - } - implicitImportMap.set(implicitImport.name, implicitImport); - } - } - } - - // Add implicit directory-based modules. - for (const dirPath of entries.directories) { - const pyFilePath = dirPath.initPyUri; - const pyiFilePath = dirPath.initPyiUri; - let isStubFile = false; - let path: Uri | undefined; - - if (this.fileExistsCached(pyiFilePath)) { - isStubFile = true; - path = pyiFilePath; - } else if (this.fileExistsCached(pyFilePath)) { - path = pyFilePath; - } - - if (path) { - if (!exclusions.find((exclusion) => exclusion.equals(path))) { - const implicitImport: ImplicitImport = { - isStubFile, - isNativeLib: false, - name: dirPath.fileName, - uri: path, - pyTypedInfo: this._getPyTypedInfo(dirPath), - }; - - implicitImportMap.set(implicitImport.name, implicitImport); - } - } - } - - return implicitImportMap; - } - // Retrieves the pytyped info for a directory if it exists. This is a small perf optimization // that allows skipping the search when the pytyped file doesn't exist. private _getPyTypedInfo(filePath: Uri): PyTypedInfo | undefined { @@ -2741,7 +2741,7 @@ export class ImportResolver { return ( current && !current.isEmpty() && - (current.isChild(root) || (current.equals(root) && _isDefaultWorkspace(execEnv.root))) + (current.isChild(root) || (current.equals(root) && isDefaultWorkspace(execEnv.root))) ); } } @@ -2757,7 +2757,7 @@ export function formatImportName(moduleDescriptor: ImportedModuleDescriptor) { } export function getParentImportResolutionRoot(sourceFileUri: Uri, executionRoot: Uri | undefined): Uri { - if (!_isDefaultWorkspace(executionRoot)) { + if (!isDefaultWorkspace(executionRoot)) { return executionRoot!; } @@ -2830,6 +2830,6 @@ function _isNativeModuleFileExtension(fileExtension: string): boolean { return supportedNativeLibExtensions.some((ext) => ext === fileExtension); } -function _isDefaultWorkspace(uri: Uri | undefined) { +export function isDefaultWorkspace(uri: Uri | undefined) { return !uri || uri.isEmpty() || Uri.isDefaultWorkspace(uri); } diff --git a/packages/pyright-internal/src/analyzer/pythonPathUtils.ts b/packages/pyright-internal/src/analyzer/pythonPathUtils.ts index ae09a3e1f003..8c781391c266 100644 --- a/packages/pyright-internal/src/analyzer/pythonPathUtils.ts +++ b/packages/pyright-internal/src/analyzer/pythonPathUtils.ts @@ -177,6 +177,26 @@ function findSitePackagesPath( return undefined; } +export function readPthSearchPaths(pthFile: Uri, fs: FileSystem): Uri[] { + const searchPaths: Uri[] = []; + + if (fs.existsSync(pthFile)) { + const data = fs.readFileSync(pthFile, 'utf8'); + const lines = data.split(/\r?\n/); + lines.forEach((line) => { + const trimmedLine = line.trim(); + if (trimmedLine.length > 0 && !trimmedLine.startsWith('#') && !trimmedLine.match(/^import\s/)) { + const pthPath = pthFile.getDirectory().combinePaths(trimmedLine); + if (fs.existsSync(pthPath) && isDirectory(fs, pthPath)) { + searchPaths.push(fs.realCasePath(pthPath)); + } + } + }); + } + + return searchPaths; +} + export function getPathsFromPthFiles(fs: FileSystem, parentDir: Uri): Uri[] { const searchPaths: Uri[] = []; @@ -192,24 +212,14 @@ export function getPathsFromPthFiles(fs: FileSystem, parentDir: Uri): Uri[] { // Skip all files that are much larger than expected. if (fileStats?.isFile() && fileStats.size > 0 && fileStats.size < 64 * 1024) { - const data = fs.readFileSync(filePath, 'utf8'); - const lines = data.split(/\r?\n/); - lines.forEach((line) => { - const trimmedLine = line.trim(); - if (trimmedLine.length > 0 && !trimmedLine.startsWith('#') && !trimmedLine.match(/^import\s/)) { - const pthPath = parentDir.combinePaths(trimmedLine); - if (fs.existsSync(pthPath) && isDirectory(fs, pthPath)) { - searchPaths.push(fs.realCasePath(pthPath)); - } - } - }); + searchPaths.push(...readPthSearchPaths(filePath, fs)); } }); return searchPaths; } -function addPathIfUnique(pathList: Uri[], pathToAdd: Uri) { +export function addPathIfUnique(pathList: Uri[], pathToAdd: Uri) { if (!pathList.some((path) => path.key === pathToAdd.key)) { pathList.push(pathToAdd); return true; diff --git a/packages/pyright-internal/src/backgroundAnalysis.ts b/packages/pyright-internal/src/backgroundAnalysis.ts index 852bed50a075..ca55c47e9339 100644 --- a/packages/pyright-internal/src/backgroundAnalysis.ts +++ b/packages/pyright-internal/src/backgroundAnalysis.ts @@ -17,6 +17,7 @@ import { FullAccessHost } from './common/fullAccessHost'; import { Host } from './common/host'; import { ServiceProvider } from './common/serviceProvider'; import { getRootUri } from './common/uri/uriUtils'; +import { ServiceKeys } from './common/serviceKeys'; export class BackgroundAnalysis extends BackgroundAnalysisBase { private static _workerIndex = 0; @@ -27,6 +28,7 @@ export class BackgroundAnalysis extends BackgroundAnalysisBase { const index = ++BackgroundAnalysis._workerIndex; const initialData: InitializationData = { rootUri: getRootUri(serviceProvider)?.toString() ?? '', + tempFileName: serviceProvider.get(ServiceKeys.tempFile).tmpdir().getFilePath(), serviceId: index.toString(), cancellationFolderName: getCancellationFolderName(), runner: undefined, diff --git a/packages/pyright-internal/src/backgroundThreadBase.ts b/packages/pyright-internal/src/backgroundThreadBase.ts index 814c5f158d88..379fbaf7cfb5 100644 --- a/packages/pyright-internal/src/backgroundThreadBase.ts +++ b/packages/pyright-internal/src/backgroundThreadBase.ts @@ -16,7 +16,7 @@ import { } from './common/cancellationUtils'; import { ConfigOptions } from './common/configOptions'; import { ConsoleInterface, LogLevel } from './common/console'; -import { Disposable, isThenable } from './common/core'; +import { isThenable } from './common/core'; import * as debug from './common/debug'; import { createFromRealFileSystem, RealTempFile } from './common/realFileSystem'; import { ServiceKeys } from './common/serviceKeys'; @@ -70,14 +70,14 @@ export class BackgroundThreadBase { this._serviceProvider.add(ServiceKeys.console, new BackgroundConsole()); } - let tempFile: RealTempFile | undefined = undefined; - if (!this._serviceProvider.tryGet(ServiceKeys.tempFile)) { - tempFile = new RealTempFile(); + let tempFile = this._serviceProvider.tryGet(ServiceKeys.tempFile); + if (!tempFile) { + tempFile = new RealTempFile(data.tempFileName); this._serviceProvider.add(ServiceKeys.tempFile, tempFile); } if (!this._serviceProvider.tryGet(ServiceKeys.caseSensitivityDetector)) { - this._serviceProvider.add(ServiceKeys.caseSensitivityDetector, tempFile ?? new RealTempFile()); + this._serviceProvider.add(ServiceKeys.caseSensitivityDetector, tempFile as RealTempFile); } if (!this._serviceProvider.tryGet(ServiceKeys.fs)) { @@ -114,11 +114,7 @@ export class BackgroundThreadBase { } protected handleShutdown() { - const tempFile = this._serviceProvider.tryGet(ServiceKeys.tempFile); - if (Disposable.is(tempFile)) { - tempFile.dispose(); - } - + this._serviceProvider.dispose(); parentPort?.close(); } } @@ -256,6 +252,7 @@ export function getBackgroundWaiter(port: MessagePort, deserializer: (v: any) export interface InitializationData { rootUri: string; + tempFileName: string; serviceId: string; workerIndex: number; cancellationFolderName: string | undefined; diff --git a/packages/pyright-internal/src/common/languageServerInterface.ts b/packages/pyright-internal/src/common/languageServerInterface.ts index 24937d810972..712b3fd01153 100644 --- a/packages/pyright-internal/src/common/languageServerInterface.ts +++ b/packages/pyright-internal/src/common/languageServerInterface.ts @@ -126,7 +126,7 @@ export interface LanguageServerBaseInterface { readonly supportAdvancedEdits: boolean; readonly serviceProvider: ServiceProvider; - createBackgroundAnalysis(serviceId: string): BackgroundAnalysisBase | undefined; + createBackgroundAnalysis(serviceId: string, workspaceRoot: Uri): BackgroundAnalysisBase | undefined; reanalyze(): void; restart(): void; diff --git a/packages/pyright-internal/src/common/realFileSystem.ts b/packages/pyright-internal/src/common/realFileSystem.ts index b2c2ea35f6a6..5042841c214f 100644 --- a/packages/pyright-internal/src/common/realFileSystem.ts +++ b/packages/pyright-internal/src/common/realFileSystem.ts @@ -504,8 +504,16 @@ export class RealTempFile implements TempFile, CaseSensitivityDetector { private _caseSensitivity?: boolean; private _tmpdir?: tmp.DirResult; - constructor() { - // Empty + constructor(name?: string) { + if (name) { + this._tmpdir = { + name, + removeCallback: () => { + // If a name is provided, the temp folder is not managed by this instance. + // Do nothing. + }, + }; + } } tmpdir(): Uri { diff --git a/packages/pyright-internal/src/common/serviceProvider.ts b/packages/pyright-internal/src/common/serviceProvider.ts index 99597d290337..eab420311355 100644 --- a/packages/pyright-internal/src/common/serviceProvider.ts +++ b/packages/pyright-internal/src/common/serviceProvider.ts @@ -7,6 +7,7 @@ */ import { addIfUnique, removeArrayElements } from './collectionUtils'; +import { Disposable } from './core'; import * as debug from './debug'; abstract class InternalKey { @@ -102,6 +103,14 @@ export class ServiceProvider { return serviceProvider; } + dispose() { + for (const service of this._container.values()) { + if (Disposable.is(service)) { + service.dispose(); + } + } + } + private _addGroupService(key: GroupServiceKey, newValue: T | undefined) { // Explicitly cast to remove `readonly` const services = this.tryGet(key) as T[] | undefined; diff --git a/packages/pyright-internal/src/languageServerBase.ts b/packages/pyright-internal/src/languageServerBase.ts index 1de47a4128e5..db03653dd423 100644 --- a/packages/pyright-internal/src/languageServerBase.ts +++ b/packages/pyright-internal/src/languageServerBase.ts @@ -239,7 +239,7 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis this._workspaceFoldersChangedDisposable?.dispose(); } - abstract createBackgroundAnalysis(serviceId: string): BackgroundAnalysisBase | undefined; + abstract createBackgroundAnalysis(serviceId: string, workspaceRoot: Uri): BackgroundAnalysisBase | undefined; abstract getSettings(workspace: Workspace): Promise; @@ -247,6 +247,7 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis // program within a workspace. createAnalyzerService( name: string, + workspaceRoot: Uri, services?: WorkspaceServices, libraryReanalysisTimeProvider?: LibraryReanalysisTimeProvider ): AnalyzerService { @@ -257,7 +258,9 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis console: this.console, hostFactory: this.createHost.bind(this), importResolverFactory: this.createImportResolver.bind(this), - backgroundAnalysis: services ? services.backgroundAnalysis : this.createBackgroundAnalysis(serviceId), + backgroundAnalysis: services + ? services.backgroundAnalysis + : this.createBackgroundAnalysis(serviceId, workspaceRoot), maxAnalysisTime: this.serverOptions.maxAnalysisTimeInForeground, backgroundAnalysisProgramFactory: this.createBackgroundAnalysisProgram.bind(this), cancellationProvider: this.serverOptions.cancellationProvider, @@ -1126,6 +1129,7 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis // Stop tracking all open files. this.openFileMap.clear(); + this.serviceProvider.dispose(); return Promise.resolve(); } @@ -1222,14 +1226,14 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis protected createAnalyzerServiceForWorkspace( name: string, - uri: Uri, + workspaceRoot: Uri, kinds: string[], services?: WorkspaceServices ): AnalyzerService { // 5 seconds default const defaultBackOffTime = 5 * 1000; - return this.createAnalyzerService(name, services, () => defaultBackOffTime); + return this.createAnalyzerService(name, workspaceRoot, services, () => defaultBackOffTime); } protected recordUserInteractionTime() { diff --git a/packages/pyright-internal/src/languageService/analyzerServiceExecutor.ts b/packages/pyright-internal/src/languageService/analyzerServiceExecutor.ts index 6b270ccc8e36..78e02ea39bd7 100644 --- a/packages/pyright-internal/src/languageService/analyzerServiceExecutor.ts +++ b/packages/pyright-internal/src/languageService/analyzerServiceExecutor.ts @@ -14,6 +14,7 @@ import { CommandLineOptions } from '../common/commandLineOptions'; import { LogLevel } from '../common/console'; import { FileSystem } from '../common/fileSystem'; import { LanguageServerBaseInterface, ServerSettings } from '../common/languageServerInterface'; +import { EmptyUri } from '../common/uri/emptyUri'; import { Uri } from '../common/uri/uri'; import { WellKnownWorkspaceKinds, Workspace, createInitStatus } from '../workspaceFactory'; @@ -63,7 +64,9 @@ export class AnalyzerServiceExecutor { service: workspace.service.clone( instanceName, serviceId, - options.useBackgroundAnalysis ? ls.createBackgroundAnalysis(serviceId) : undefined, + options.useBackgroundAnalysis + ? ls.createBackgroundAnalysis(serviceId, workspace.rootUri || EmptyUri.instance) + : undefined, options.fileSystem ), disableLanguageServices: true, diff --git a/packages/pyright-internal/src/pyright.ts b/packages/pyright-internal/src/pyright.ts index cdce996a46c1..fb7a58c59bc4 100644 --- a/packages/pyright-internal/src/pyright.ts +++ b/packages/pyright-internal/src/pyright.ts @@ -36,6 +36,7 @@ import { FullAccessHost } from './common/fullAccessHost'; import { combinePaths, normalizePath } from './common/pathUtils'; import { PythonVersion } from './common/pythonVersion'; import { RealTempFile, createFromRealFileSystem } from './common/realFileSystem'; +import { ServiceKeys } from './common/serviceKeys'; import { ServiceProvider } from './common/serviceProvider'; import { createServiceProvider } from './common/serviceProviderExtensions'; import { getStdin } from './common/streamUtils'; @@ -658,7 +659,14 @@ async function runMultiThreaded( // Launch worker processes. for (let i = 0; i < workerCount; i++) { const mainModulePath = process.mainModule!.filename; - const worker = fork(mainModulePath, ['worker', i.toString()]); + + // Ensure forked processes use the temp folder owned by the main process. + // This allows for automatic deletion when the main process exits. + const worker = fork(mainModulePath, [ + 'worker', + i.toString(), + service.serviceProvider.get(ServiceKeys.tempFile).tmpdir().getFilePath(), + ]); worker.on('message', (message) => { let messageObj: any; @@ -725,7 +733,7 @@ async function runMultiThreaded( // This is the message loop for a worker process used used for // multi-threaded analysis. -function runWorkerMessageLoop(workerNum: number) { +function runWorkerMessageLoop(workerNum: number, tempFolderName: string) { let serviceProvider: ServiceProvider | undefined; let service: AnalyzerService | undefined; let fileSystem: PyrightFileSystem | undefined; @@ -759,7 +767,7 @@ function runWorkerMessageLoop(workerNum: number) { } const output = new StderrConsole(logLevel); - const tempFile = new RealTempFile(); + const tempFile = new RealTempFile(tempFolderName); fileSystem = new PyrightFileSystem( createFromRealFileSystem(tempFile, output, new ChokidarFileWatcherProvider(output)) ); @@ -1332,7 +1340,7 @@ export async function main() { // Is this a worker process for multi-threaded analysis? if (process.argv[2] === 'worker') { const workerNumber = parseInt(process.argv[3]); - runWorkerMessageLoop(workerNumber); + runWorkerMessageLoop(workerNumber, process.argv[4]); return; } diff --git a/packages/pyright-internal/src/server.ts b/packages/pyright-internal/src/server.ts index e4738b0d74f4..704a969e6417 100644 --- a/packages/pyright-internal/src/server.ts +++ b/packages/pyright-internal/src/server.ts @@ -215,7 +215,7 @@ export class PyrightServer extends LanguageServerBase { return serverSettings; } - createBackgroundAnalysis(serviceId: string): BackgroundAnalysisBase | undefined { + createBackgroundAnalysis(serviceId: string, workspaceRoot: Uri): BackgroundAnalysisBase | undefined { if (isDebugMode() || !getCancellationFolderName()) { // Don't do background analysis if we're in debug mode or an old client // is used where cancellation is not supported. diff --git a/packages/pyright-internal/src/tests/config.test.ts b/packages/pyright-internal/src/tests/config.test.ts index 5b4da4d29f24..4de293280a8d 100644 --- a/packages/pyright-internal/src/tests/config.test.ts +++ b/packages/pyright-internal/src/tests/config.test.ts @@ -24,557 +24,563 @@ import { UriEx } from '../common/uri/uriUtils'; import { TestAccessHost } from './harness/testAccessHost'; import { TestFileSystem } from './harness/vfs/filesystem'; -function createAnalyzer(console?: ConsoleInterface) { +describe(`config test'}`, () => { const tempFile = new RealTempFile(); - const cons = console ?? new NullConsole(); - const fs = createFromRealFileSystem(tempFile, cons); - const serviceProvider = createServiceProvider(fs, cons, tempFile); - return new AnalyzerService('', serviceProvider, { console: cons }); -} - -test('FindFilesWithConfigFile', () => { - const cwd = normalizePath(process.cwd()); - const service = createAnalyzer(); - const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ true); - commandLineOptions.configFilePath = 'src/tests/samples/project1'; - - const configOptions = service.test_getConfigOptions(commandLineOptions); - service.setOptions(commandLineOptions); - - // The config file specifies a single file spec (a directory). - assert.strictEqual(configOptions.include.length, 1, `failed creating options from ${cwd}`); - assert.strictEqual( - configOptions.projectRoot.key, - service.fs.realCasePath(Uri.file(combinePaths(cwd, commandLineOptions.configFilePath), service.serviceProvider)) - .key - ); - - const fileList = service.test_getFileNamesFromFileSpecs(); - - // The config file specifies a subdirectory, so we should find - // only two of the three "*.py" files present in the project - // directory. - assert.strictEqual(fileList.length, 2); -}); - -test('FindFilesVirtualEnvAutoDetectExclude', () => { - const cwd = normalizePath(process.cwd()); - const service = createAnalyzer(); - const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ true); - commandLineOptions.configFilePath = 'src/tests/samples/project_with_venv_auto_detect_exclude'; - - service.setOptions(commandLineOptions); - - // The config file is empty, so no 'exclude' are specified - // The myVenv directory is detected as a venv and will be automatically excluded - const fileList = service.test_getFileNamesFromFileSpecs(); - - // There are 3 python files in the workspace, outside of myVenv - // There is 1 python file in myVenv, which should be excluded - const fileNames = fileList.map((p) => p.fileName).sort(); - assert.deepStrictEqual(fileNames, ['sample1.py', 'sample2.py', 'sample3.py']); -}); - -test('FindFilesVirtualEnvAutoDetectInclude', () => { - const cwd = normalizePath(process.cwd()); - const service = createAnalyzer(); - const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ true); - commandLineOptions.configFilePath = 'src/tests/samples/project_with_venv_auto_detect_include'; - - service.setOptions(commandLineOptions); - - // Config file defines 'exclude' folder so virtual env will be included - const fileList = service.test_getFileNamesFromFileSpecs(); - - // There are 3 python files in the workspace, outside of myVenv - // There is 1 more python file in excluded folder - // There is 1 python file in myVenv, which should be included - const fileNames = fileList.map((p) => p.fileName).sort(); - assert.deepStrictEqual(fileNames, ['library1.py', 'sample1.py', 'sample2.py', 'sample3.py']); -}); - -test('FileSpecNotAnArray', () => { - const cwd = normalizePath(process.cwd()); - const nullConsole = new NullConsole(); - const service = createAnalyzer(nullConsole); - const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ false); - commandLineOptions.configFilePath = 'src/tests/samples/project2'; - service.setOptions(commandLineOptions); - - service.test_getConfigOptions(commandLineOptions); - - // The method should return a default config and log an error. - assert(nullConsole.infoCount > 0); -}); - -test('FileSpecNotAString', () => { - const cwd = normalizePath(process.cwd()); - const nullConsole = new NullConsole(); - const service = createAnalyzer(nullConsole); - const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ false); - commandLineOptions.configFilePath = 'src/tests/samples/project3'; - service.setOptions(commandLineOptions); - - service.test_getConfigOptions(commandLineOptions); - - // The method should return a default config and log an error. - assert(nullConsole.infoCount > 0); -}); - -test('SomeFileSpecsAreInvalid', () => { - const cwd = normalizePath(process.cwd()); - const nullConsole = new NullConsole(); - const service = createAnalyzer(nullConsole); - const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ false); - commandLineOptions.configFilePath = 'src/tests/samples/project4'; - service.setOptions(commandLineOptions); - - const configOptions = service.test_getConfigOptions(commandLineOptions); - - // The config file specifies four file specs in the include array - // and one in the exclude array. - assert.strictEqual(configOptions.include.length, 4, `failed creating options from ${cwd}`); - assert.strictEqual(configOptions.exclude.length, 1); - assert.strictEqual( - configOptions.projectRoot.getFilePath(), - service.fs - .realCasePath(Uri.file(combinePaths(cwd, commandLineOptions.configFilePath), service.serviceProvider)) - .getFilePath() - ); - - const fileList = service.test_getFileNamesFromFileSpecs(); - - // We should receive two final files that match the include/exclude rules. - assert.strictEqual(fileList.length, 2); -}); - -test('ConfigBadJson', () => { - const cwd = normalizePath(process.cwd()); - const nullConsole = new NullConsole(); - const service = createAnalyzer(nullConsole); - const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ false); - commandLineOptions.configFilePath = 'src/tests/samples/project5'; - service.setOptions(commandLineOptions); - - service.test_getConfigOptions(commandLineOptions); - - // The method should return a default config and log an error. - assert(nullConsole.infoCount > 0); -}); - -test('FindExecEnv1', () => { - const cwd = UriEx.file(normalizePath(process.cwd())); - const configOptions = new ConfigOptions(cwd); - - // Build a config option with three execution environments. - const execEnv1 = new ExecutionEnvironment( - 'python', - cwd.resolvePaths('src/foo'), - getStandardDiagnosticRuleSet(), - /* defaultPythonVersion */ undefined, - /* defaultPythonPlatform */ undefined, - /* defaultExtraPaths */ undefined - ); - configOptions.executionEnvironments.push(execEnv1); - const execEnv2 = new ExecutionEnvironment( - 'python', - cwd.resolvePaths('src'), - getStandardDiagnosticRuleSet(), - /* defaultPythonVersion */ undefined, - /* defaultPythonPlatform */ undefined, - /* defaultExtraPaths */ undefined - ); - configOptions.executionEnvironments.push(execEnv2); - - const file1 = cwd.resolvePaths('src/foo/bar.py'); - assert.strictEqual(configOptions.findExecEnvironment(file1), execEnv1); - const file2 = cwd.resolvePaths('src/foo2/bar.py'); - assert.strictEqual(configOptions.findExecEnvironment(file2), execEnv2); - - // If none of the execution environments matched, we should get - // a default environment with the root equal to that of the config. - const file4 = UriEx.file('/nothing/bar.py'); - const defaultExecEnv = configOptions.findExecEnvironment(file4); - assert(defaultExecEnv.root); - const rootFilePath = Uri.is(defaultExecEnv.root) ? defaultExecEnv.root.getFilePath() : defaultExecEnv.root; - assert.strictEqual(normalizeSlashes(rootFilePath), normalizeSlashes(configOptions.projectRoot.getFilePath())); -}); - -test('PythonPlatform', () => { - const nullConsole = new NullConsole(); - const service = createAnalyzer(nullConsole); - const cwd = Uri.file( - normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project_with_pyproject_toml_platform')), - service.serviceProvider - ); - const commandLineOptions = new CommandLineOptions(cwd.getFilePath(), /* fromLanguageServer */ false); - service.setOptions(commandLineOptions); - - const configOptions = service.test_getConfigOptions(commandLineOptions); - assert.ok(configOptions.executionEnvironments[0]); - assert.equal(configOptions.executionEnvironments[0].pythonPlatform, 'platform'); -}); - -test('AutoSearchPathsOn', () => { - const nullConsole = new NullConsole(); - const service = createAnalyzer(nullConsole); - const cwd = Uri.file( - normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project_src')), - service.serviceProvider - ); - const commandLineOptions = new CommandLineOptions(cwd.getFilePath(), /* fromLanguageServer */ false); - commandLineOptions.configSettings.autoSearchPaths = true; - service.setOptions(commandLineOptions); - - const configOptions = service.test_getConfigOptions(commandLineOptions); - - const expectedExtraPaths = [service.fs.realCasePath(cwd.combinePaths('src'))]; - assert.deepStrictEqual(configOptions.defaultExtraPaths, expectedExtraPaths); -}); - -test('AutoSearchPathsOff', () => { - const cwd = normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project_src')); - const nullConsole = new NullConsole(); - const service = createAnalyzer(nullConsole); - const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ false); - commandLineOptions.configSettings.autoSearchPaths = false; - service.setOptions(commandLineOptions); - - const configOptions = service.test_getConfigOptions(commandLineOptions); - - assert.deepStrictEqual(configOptions.executionEnvironments, []); -}); - -test('AutoSearchPathsOnSrcIsPkg', () => { - const cwd = normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project_src_is_pkg')); - const nullConsole = new NullConsole(); - const service = createAnalyzer(nullConsole); - const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ false); - commandLineOptions.configSettings.autoSearchPaths = true; - service.setOptions(commandLineOptions); - - const configOptions = service.test_getConfigOptions(commandLineOptions); - - // The src folder is a package (has __init__.py) and so should not be automatically added as extra path - assert.deepStrictEqual(configOptions.executionEnvironments, []); -}); - -test('AutoSearchPathsOnWithConfigExecEnv', () => { - const cwd = normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project_src_with_config_extra_paths')); - const nullConsole = new NullConsole(); - const service = createAnalyzer(nullConsole); - const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ false); - commandLineOptions.configFilePath = combinePaths(cwd, 'pyrightconfig.json'); - commandLineOptions.configSettings.autoSearchPaths = true; - service.setOptions(commandLineOptions); - - const configOptions = service.test_getConfigOptions(commandLineOptions); - - // The extraPaths in the config file should override the setting. - const expectedExtraPaths: string[] = []; - - assert.deepStrictEqual(configOptions.defaultExtraPaths, expectedExtraPaths); -}); - -test('AutoSearchPathsOnAndExtraPaths', () => { - const nullConsole = new NullConsole(); - const service = createAnalyzer(nullConsole); - const cwd = Uri.file( - normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project_src_with_config_no_extra_paths')), - service.serviceProvider - ); - const commandLineOptions = new CommandLineOptions(cwd.getFilePath(), /* fromLanguageServer */ false); - commandLineOptions.configSettings.autoSearchPaths = true; - commandLineOptions.configSettings.extraPaths = ['src/_vendored']; - service.setOptions(commandLineOptions); - - const configOptions = service.test_getConfigOptions(commandLineOptions); - - const expectedExtraPaths: Uri[] = [ - service.fs.realCasePath(cwd.combinePaths('src')), - service.fs.realCasePath(cwd.combinePaths('src', '_vendored')), - ]; - - assert.deepStrictEqual(configOptions.defaultExtraPaths, expectedExtraPaths); -}); - -test('BasicPyprojectTomlParsing', () => { - const cwd = normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project_with_pyproject_toml')); - const service = createAnalyzer(); - const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ true); - - service.setOptions(commandLineOptions); - - const configOptions = service.test_getConfigOptions(commandLineOptions); - assert.strictEqual(configOptions.defaultPythonVersion!.toString(), pythonVersion3_9.toString()); - assert.strictEqual(configOptions.diagnosticRuleSet.reportMissingImports, 'error'); - assert.strictEqual(configOptions.diagnosticRuleSet.reportUnusedClass, 'warning'); -}); - -test('FindFilesInMemoryOnly', () => { - const cwd = normalizePath(process.cwd()); - const service = createAnalyzer(); - const commandLineOptions = new CommandLineOptions(undefined, /* fromLanguageServer */ true); - // Force a lookup of the typeshed path. This causes us to try and generate a module path for the untitled file. - commandLineOptions.configSettings.typeshedPath = combinePaths(cwd, 'src', 'tests', 'samples'); - service.setOptions(commandLineOptions); - - // Open a file that is not backed by the file system. - const untitled = Uri.parse('untitled:Untitled-1.py', service.serviceProvider); - service.setFileOpened(untitled, 1, '# empty'); - - const fileList = service.test_getFileNamesFromFileSpecs(); - assert(fileList.filter((f) => f.equals(untitled))); -}); - -test('verify config fileSpecs after cloning', () => { - const fs = new TestFileSystem(/* ignoreCase */ true); - const configFile = { - ignore: ['**/node_modules/**'], - }; - - const rootUri = Uri.file(process.cwd(), fs); - const config = new ConfigOptions(rootUri); - const sp = createServiceProvider(fs, new NullConsole()); - config.initializeFromJson(configFile, rootUri, sp, new TestAccessHost()); - const cloned = deserialize(serialize(config)); - - assert.deepEqual(config.ignore, cloned.ignore); -}); - -test('verify can serialize config options', () => { - const config = new ConfigOptions(UriEx.file(process.cwd())); - const serialized = serialize(config); - const deserialized = deserialize(serialized); - assert.deepEqual(config, deserialized); - assert.ok(deserialized.findExecEnvironment(UriEx.file('foo/bar.py'))); -}); - -test('extra paths on undefined execution root/default workspace', () => { - const nullConsole = new NullConsole(); - const service = createAnalyzer(nullConsole); - const commandLineOptions = new CommandLineOptions(undefined, /* fromLanguageServer */ false); - commandLineOptions.configSettings.extraPaths = ['/extraPaths']; - - service.setOptions(commandLineOptions); - const configOptions = service.test_getConfigOptions(commandLineOptions); - - const expectedExtraPaths = [Uri.file('/extraPaths', service.serviceProvider)]; - assert.deepStrictEqual( - configOptions.defaultExtraPaths?.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, /* fromLanguageServer */ 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.diagnosticRuleSet.strictListInference, true); -}); - -test('Typechecking mode is standard when just config file is present', () => { - const cwd = normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project_with_pyproject_toml')); - const service = createAnalyzer(); - const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ true); - commandLineOptions.configSettings.typeCheckingMode = 'off'; - - service.setOptions(commandLineOptions); - - const configOptions = service.test_getConfigOptions(commandLineOptions); - assert.equal(configOptions.diagnosticRuleSet.reportPossiblyUnboundVariable, 'error'); -}); - -test('Typechecking mode depends upon if vscode extension or not', () => { - const cwd = normalizePath(combinePaths(process.cwd(), 'src/tests/samples/package1')); - let service = createAnalyzer(); - let commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ true); - - service.setOptions(commandLineOptions); - - let configOptions = service.test_getConfigOptions(commandLineOptions); - assert.equal(configOptions.diagnosticRuleSet.reportPossiblyUnboundVariable, 'none'); - - service = createAnalyzer(); - commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ false); - - service.setOptions(commandLineOptions); - - configOptions = service.test_getConfigOptions(commandLineOptions); - assert.equal(configOptions.diagnosticRuleSet.reportPossiblyUnboundVariable, 'error'); - - commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ true); - commandLineOptions.configSettings.typeCheckingMode = 'strict'; - service = createAnalyzer(); - service.setOptions(commandLineOptions); - - configOptions = service.test_getConfigOptions(commandLineOptions); - assert.equal(configOptions.diagnosticRuleSet.reportPossiblyUnboundVariable, 'error'); -}); - -test('Include file paths are only set in the config file when using extension', () => { - const cwd = normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project1')); - const service = createAnalyzer(); - const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ true); - commandLineOptions.configSettings.includeFileSpecs = ['test']; - service.setOptions(commandLineOptions); - - const configOptions = service.test_getConfigOptions(commandLineOptions); - assert.equal(configOptions.include.length, 1); - assert.ok(configOptions.include[0].regExp.source.includes('/subfolder1)')); -}); - -test('Include file paths can be added to on the command line with a config', () => { - const cwd = normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project1')); - const service = createAnalyzer(); - const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ false); - commandLineOptions.configSettings.includeFileSpecs = ['test']; - service.setOptions(commandLineOptions); - - const configOptions = service.test_getConfigOptions(commandLineOptions); - assert.equal(configOptions.include.length, 2); - assert.ok(configOptions.include[1].regExp.source.includes('/test)')); -}); - -test('Include file paths can be added to by an extension without a config', () => { - const cwd = normalizePath(combinePaths(process.cwd(), 'src/tests/samples/package1')); - const service = createAnalyzer(); - const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ true); - commandLineOptions.configSettings.includeFileSpecs = ['test']; - service.setOptions(commandLineOptions); - - const configOptions = service.test_getConfigOptions(commandLineOptions); - assert.equal(configOptions.include.length, 1); - assert.ok(configOptions.include[0].regExp.source.includes('/test)')); -}); - -test('Command line options can override config but only when not using extension', () => { - const cwd = normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project_with_all_config')); - const service = createAnalyzer(); - const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ false); - service.setOptions(commandLineOptions); - - // First get the default. - const defaultOptions = service.test_getConfigOptions(commandLineOptions); - - // Now set all of the different options and make sure the command line options override. - commandLineOptions.configSettings.typeCheckingMode = 'strict'; - commandLineOptions.configSettings.venvPath = 'test2'; - commandLineOptions.configSettings.typeshedPath = 'test2'; - commandLineOptions.configSettings.stubPath = 'test2'; - commandLineOptions.configSettings.useLibraryCodeForTypes = true; - commandLineOptions.configSettings.includeFileSpecs = ['test2']; - commandLineOptions.configSettings.excludeFileSpecs = ['test2']; - commandLineOptions.configSettings.diagnosticSeverityOverrides = { - reportMissingImports: DiagnosticSeverityOverrides.Error, - }; - commandLineOptions.configSettings.ignoreFileSpecs = ['test2']; - - service.setOptions(commandLineOptions); - const overriddenOptions = service.test_getConfigOptions(commandLineOptions); - assert.notDeepStrictEqual(defaultOptions.include, overriddenOptions.include); - assert.notDeepStrictEqual(defaultOptions.exclude, overriddenOptions.exclude); - assert.notDeepStrictEqual(defaultOptions.ignore, overriddenOptions.ignore); - assert.notDeepStrictEqual(defaultOptions.diagnosticRuleSet, overriddenOptions.diagnosticRuleSet); - assert.notDeepStrictEqual(defaultOptions.venvPath, overriddenOptions.venvPath); - // Typeshed and stub path are an exception, it should just be reported as a dupe. - assert.deepStrictEqual(defaultOptions.typeshedPath, overriddenOptions.typeshedPath); - assert.deepStrictEqual(defaultOptions.stubPath, overriddenOptions.stubPath); - - // Do the same with an extension based config, but make sure we get the default back. - const commandLineOptions2 = new CommandLineOptions(cwd, /* fromLanguageServer */ true); - service.setOptions(commandLineOptions2); - const overriddenOptions2 = service.test_getConfigOptions(commandLineOptions2); - assert.deepStrictEqual(defaultOptions, overriddenOptions2); -}); - -test('Config venvPath take precedences over language server settings', () => { - const cwd = normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project_with_all_config')); - const service = createAnalyzer(); - const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ true); - commandLineOptions.languageServerSettings.venvPath = 'test_from_language_server'; - service.setOptions(commandLineOptions); - - // Verify language server options don't override - const options = service.test_getConfigOptions(commandLineOptions); - assert.equal(options.venvPath?.pathIncludes('from_language_server'), false); -}); - -test('Command line venvPath take precedences over everything else', () => { - const cwd = normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project_with_all_config')); - const service = createAnalyzer(); - const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ false); - commandLineOptions.configSettings.venvPath = 'test_from_command_line'; - commandLineOptions.languageServerSettings.venvPath = 'test_from_language_server'; - service.setOptions(commandLineOptions); - - // Verify command line overrides everything - const options = service.test_getConfigOptions(commandLineOptions); - assert.ok(options.venvPath?.pathIncludes('test_from_command_line')); -}); - -test('Config empty venvPath does not take precedences over language server settings', () => { - const cwd = normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project_src_with_config_extra_paths')); - const service = createAnalyzer(); - const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ true); - commandLineOptions.languageServerSettings.venvPath = 'test_from_language_server'; - service.setOptions(commandLineOptions); - - // Verify language server options don't override - const options = service.test_getConfigOptions(commandLineOptions); - assert.ok(options.venvPath?.pathIncludes('from_language_server')); -}); -test('Language server specific settings are set whether or not there is a pyproject.toml', () => { - const cwd = normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project_with_all_config')); - const service = createAnalyzer(); - const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ false); - commandLineOptions.languageServerSettings.autoImportCompletions = true; - commandLineOptions.languageServerSettings.indexing = true; - commandLineOptions.languageServerSettings.taskListTokens = [{ priority: TaskListPriority.High, text: 'test' }]; - commandLineOptions.languageServerSettings.logTypeEvaluationTime = true; - commandLineOptions.languageServerSettings.typeEvaluationTimeThreshold = 1; - commandLineOptions.languageServerSettings.enableAmbientAnalysis = false; - commandLineOptions.languageServerSettings.disableTaggedHints = true; - commandLineOptions.languageServerSettings.watchForSourceChanges = true; - commandLineOptions.languageServerSettings.watchForLibraryChanges = true; - commandLineOptions.languageServerSettings.watchForConfigChanges = true; - commandLineOptions.languageServerSettings.typeStubTargetImportName = 'test'; - commandLineOptions.languageServerSettings.checkOnlyOpenFiles = true; - commandLineOptions.languageServerSettings.disableTaggedHints = true; - commandLineOptions.languageServerSettings.pythonPath = 'test_python_path'; - - service.setOptions(commandLineOptions); - let options = service.test_getConfigOptions(commandLineOptions); - assert.strictEqual(options.autoImportCompletions, true); - assert.strictEqual(options.indexing, true); - assert.strictEqual(options.taskListTokens?.length, 1); - assert.strictEqual(options.logTypeEvaluationTime, true); - assert.strictEqual(options.typeEvaluationTimeThreshold, 1); - assert.strictEqual(options.disableTaggedHints, true); - assert.ok(options.pythonPath?.pathIncludes('test_python_path')); - - // Test with language server set to true to make sure they are still set. - commandLineOptions.fromLanguageServer = true; - commandLineOptions.languageServerSettings.venvPath = 'test_venv_path'; - service.setOptions(commandLineOptions); - options = service.test_getConfigOptions(commandLineOptions); - assert.strictEqual(options.autoImportCompletions, true); - assert.strictEqual(options.indexing, true); - assert.strictEqual(options.taskListTokens?.length, 1); - assert.strictEqual(options.logTypeEvaluationTime, true); - assert.strictEqual(options.typeEvaluationTimeThreshold, 1); - assert.strictEqual(options.disableTaggedHints, true); - assert.ok(options.pythonPath?.pathIncludes('test_python_path')); - - // Verify language server options don't override the config setting. Only command line should - assert.equal(options.venvPath?.pathIncludes('test_venv_path'), false); + afterAll(() => tempFile.dispose()); + + test('FindFilesWithConfigFile', () => { + const cwd = normalizePath(process.cwd()); + const service = createAnalyzer(); + const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ true); + commandLineOptions.configFilePath = 'src/tests/samples/project1'; + + const configOptions = service.test_getConfigOptions(commandLineOptions); + service.setOptions(commandLineOptions); + + // The config file specifies a single file spec (a directory). + assert.strictEqual(configOptions.include.length, 1, `failed creating options from ${cwd}`); + assert.strictEqual( + configOptions.projectRoot.key, + service.fs.realCasePath( + Uri.file(combinePaths(cwd, commandLineOptions.configFilePath), service.serviceProvider) + ).key + ); + + const fileList = service.test_getFileNamesFromFileSpecs(); + + // The config file specifies a subdirectory, so we should find + // only two of the three "*.py" files present in the project + // directory. + assert.strictEqual(fileList.length, 2); + }); + + test('FindFilesVirtualEnvAutoDetectExclude', () => { + const cwd = normalizePath(process.cwd()); + const service = createAnalyzer(); + const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ true); + commandLineOptions.configFilePath = 'src/tests/samples/project_with_venv_auto_detect_exclude'; + + service.setOptions(commandLineOptions); + + // The config file is empty, so no 'exclude' are specified + // The myVenv directory is detected as a venv and will be automatically excluded + const fileList = service.test_getFileNamesFromFileSpecs(); + + // There are 3 python files in the workspace, outside of myVenv + // There is 1 python file in myVenv, which should be excluded + const fileNames = fileList.map((p) => p.fileName).sort(); + assert.deepStrictEqual(fileNames, ['sample1.py', 'sample2.py', 'sample3.py']); + }); + + test('FindFilesVirtualEnvAutoDetectInclude', () => { + const cwd = normalizePath(process.cwd()); + const service = createAnalyzer(); + const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ true); + commandLineOptions.configFilePath = 'src/tests/samples/project_with_venv_auto_detect_include'; + + service.setOptions(commandLineOptions); + + // Config file defines 'exclude' folder so virtual env will be included + const fileList = service.test_getFileNamesFromFileSpecs(); + + // There are 3 python files in the workspace, outside of myVenv + // There is 1 more python file in excluded folder + // There is 1 python file in myVenv, which should be included + const fileNames = fileList.map((p) => p.fileName).sort(); + assert.deepStrictEqual(fileNames, ['library1.py', 'sample1.py', 'sample2.py', 'sample3.py']); + }); + + test('FileSpecNotAnArray', () => { + const cwd = normalizePath(process.cwd()); + const nullConsole = new NullConsole(); + const service = createAnalyzer(nullConsole); + const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ false); + commandLineOptions.configFilePath = 'src/tests/samples/project2'; + service.setOptions(commandLineOptions); + + service.test_getConfigOptions(commandLineOptions); + + // The method should return a default config and log an error. + assert(nullConsole.infoCount > 0); + }); + + test('FileSpecNotAString', () => { + const cwd = normalizePath(process.cwd()); + const nullConsole = new NullConsole(); + const service = createAnalyzer(nullConsole); + const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ false); + commandLineOptions.configFilePath = 'src/tests/samples/project3'; + service.setOptions(commandLineOptions); + + service.test_getConfigOptions(commandLineOptions); + + // The method should return a default config and log an error. + assert(nullConsole.infoCount > 0); + }); + + test('SomeFileSpecsAreInvalid', () => { + const cwd = normalizePath(process.cwd()); + const nullConsole = new NullConsole(); + const service = createAnalyzer(nullConsole); + const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ false); + commandLineOptions.configFilePath = 'src/tests/samples/project4'; + service.setOptions(commandLineOptions); + + const configOptions = service.test_getConfigOptions(commandLineOptions); + + // The config file specifies four file specs in the include array + // and one in the exclude array. + assert.strictEqual(configOptions.include.length, 4, `failed creating options from ${cwd}`); + assert.strictEqual(configOptions.exclude.length, 1); + assert.strictEqual( + configOptions.projectRoot.getFilePath(), + service.fs + .realCasePath(Uri.file(combinePaths(cwd, commandLineOptions.configFilePath), service.serviceProvider)) + .getFilePath() + ); + + const fileList = service.test_getFileNamesFromFileSpecs(); + + // We should receive two final files that match the include/exclude rules. + assert.strictEqual(fileList.length, 2); + }); + + test('ConfigBadJson', () => { + const cwd = normalizePath(process.cwd()); + const nullConsole = new NullConsole(); + const service = createAnalyzer(nullConsole); + const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ false); + commandLineOptions.configFilePath = 'src/tests/samples/project5'; + service.setOptions(commandLineOptions); + + service.test_getConfigOptions(commandLineOptions); + + // The method should return a default config and log an error. + assert(nullConsole.infoCount > 0); + }); + + test('FindExecEnv1', () => { + const cwd = UriEx.file(normalizePath(process.cwd())); + const configOptions = new ConfigOptions(cwd); + + // Build a config option with three execution environments. + const execEnv1 = new ExecutionEnvironment( + 'python', + cwd.resolvePaths('src/foo'), + getStandardDiagnosticRuleSet(), + /* defaultPythonVersion */ undefined, + /* defaultPythonPlatform */ undefined, + /* defaultExtraPaths */ undefined + ); + configOptions.executionEnvironments.push(execEnv1); + const execEnv2 = new ExecutionEnvironment( + 'python', + cwd.resolvePaths('src'), + getStandardDiagnosticRuleSet(), + /* defaultPythonVersion */ undefined, + /* defaultPythonPlatform */ undefined, + /* defaultExtraPaths */ undefined + ); + configOptions.executionEnvironments.push(execEnv2); + + const file1 = cwd.resolvePaths('src/foo/bar.py'); + assert.strictEqual(configOptions.findExecEnvironment(file1), execEnv1); + const file2 = cwd.resolvePaths('src/foo2/bar.py'); + assert.strictEqual(configOptions.findExecEnvironment(file2), execEnv2); + + // If none of the execution environments matched, we should get + // a default environment with the root equal to that of the config. + const file4 = UriEx.file('/nothing/bar.py'); + const defaultExecEnv = configOptions.findExecEnvironment(file4); + assert(defaultExecEnv.root); + const rootFilePath = Uri.is(defaultExecEnv.root) ? defaultExecEnv.root.getFilePath() : defaultExecEnv.root; + assert.strictEqual(normalizeSlashes(rootFilePath), normalizeSlashes(configOptions.projectRoot.getFilePath())); + }); + + test('PythonPlatform', () => { + const nullConsole = new NullConsole(); + const service = createAnalyzer(nullConsole); + const cwd = Uri.file( + normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project_with_pyproject_toml_platform')), + service.serviceProvider + ); + const commandLineOptions = new CommandLineOptions(cwd.getFilePath(), /* fromLanguageServer */ false); + service.setOptions(commandLineOptions); + + const configOptions = service.test_getConfigOptions(commandLineOptions); + assert.ok(configOptions.executionEnvironments[0]); + assert.equal(configOptions.executionEnvironments[0].pythonPlatform, 'platform'); + }); + + test('AutoSearchPathsOn', () => { + const nullConsole = new NullConsole(); + const service = createAnalyzer(nullConsole); + const cwd = Uri.file( + normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project_src')), + service.serviceProvider + ); + const commandLineOptions = new CommandLineOptions(cwd.getFilePath(), /* fromLanguageServer */ false); + commandLineOptions.configSettings.autoSearchPaths = true; + service.setOptions(commandLineOptions); + + const configOptions = service.test_getConfigOptions(commandLineOptions); + + const expectedExtraPaths = [service.fs.realCasePath(cwd.combinePaths('src'))]; + assert.deepStrictEqual(configOptions.defaultExtraPaths, expectedExtraPaths); + }); + + test('AutoSearchPathsOff', () => { + const cwd = normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project_src')); + const nullConsole = new NullConsole(); + const service = createAnalyzer(nullConsole); + const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ false); + commandLineOptions.configSettings.autoSearchPaths = false; + service.setOptions(commandLineOptions); + + const configOptions = service.test_getConfigOptions(commandLineOptions); + + assert.deepStrictEqual(configOptions.executionEnvironments, []); + }); + + test('AutoSearchPathsOnSrcIsPkg', () => { + const cwd = normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project_src_is_pkg')); + const nullConsole = new NullConsole(); + const service = createAnalyzer(nullConsole); + const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ false); + commandLineOptions.configSettings.autoSearchPaths = true; + service.setOptions(commandLineOptions); + + const configOptions = service.test_getConfigOptions(commandLineOptions); + + // The src folder is a package (has __init__.py) and so should not be automatically added as extra path + assert.deepStrictEqual(configOptions.executionEnvironments, []); + }); + + test('AutoSearchPathsOnWithConfigExecEnv', () => { + const cwd = normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project_src_with_config_extra_paths')); + const nullConsole = new NullConsole(); + const service = createAnalyzer(nullConsole); + const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ false); + commandLineOptions.configFilePath = combinePaths(cwd, 'pyrightconfig.json'); + commandLineOptions.configSettings.autoSearchPaths = true; + service.setOptions(commandLineOptions); + + const configOptions = service.test_getConfigOptions(commandLineOptions); + + // The extraPaths in the config file should override the setting. + const expectedExtraPaths: string[] = []; + + assert.deepStrictEqual(configOptions.defaultExtraPaths, expectedExtraPaths); + }); + + test('AutoSearchPathsOnAndExtraPaths', () => { + const nullConsole = new NullConsole(); + const service = createAnalyzer(nullConsole); + const cwd = Uri.file( + normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project_src_with_config_no_extra_paths')), + service.serviceProvider + ); + const commandLineOptions = new CommandLineOptions(cwd.getFilePath(), /* fromLanguageServer */ false); + commandLineOptions.configSettings.autoSearchPaths = true; + commandLineOptions.configSettings.extraPaths = ['src/_vendored']; + service.setOptions(commandLineOptions); + + const configOptions = service.test_getConfigOptions(commandLineOptions); + + const expectedExtraPaths: Uri[] = [ + service.fs.realCasePath(cwd.combinePaths('src')), + service.fs.realCasePath(cwd.combinePaths('src', '_vendored')), + ]; + + assert.deepStrictEqual(configOptions.defaultExtraPaths, expectedExtraPaths); + }); + + test('BasicPyprojectTomlParsing', () => { + const cwd = normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project_with_pyproject_toml')); + const service = createAnalyzer(); + const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ true); + + service.setOptions(commandLineOptions); + + const configOptions = service.test_getConfigOptions(commandLineOptions); + assert.strictEqual(configOptions.defaultPythonVersion!.toString(), pythonVersion3_9.toString()); + assert.strictEqual(configOptions.diagnosticRuleSet.reportMissingImports, 'error'); + assert.strictEqual(configOptions.diagnosticRuleSet.reportUnusedClass, 'warning'); + }); + + test('FindFilesInMemoryOnly', () => { + const cwd = normalizePath(process.cwd()); + const service = createAnalyzer(); + const commandLineOptions = new CommandLineOptions(undefined, /* fromLanguageServer */ true); + // Force a lookup of the typeshed path. This causes us to try and generate a module path for the untitled file. + commandLineOptions.configSettings.typeshedPath = combinePaths(cwd, 'src', 'tests', 'samples'); + service.setOptions(commandLineOptions); + + // Open a file that is not backed by the file system. + const untitled = Uri.parse('untitled:Untitled-1.py', service.serviceProvider); + service.setFileOpened(untitled, 1, '# empty'); + + const fileList = service.test_getFileNamesFromFileSpecs(); + assert(fileList.filter((f) => f.equals(untitled))); + }); + + test('verify config fileSpecs after cloning', () => { + const fs = new TestFileSystem(/* ignoreCase */ true); + const configFile = { + ignore: ['**/node_modules/**'], + }; + + const rootUri = Uri.file(process.cwd(), fs); + const config = new ConfigOptions(rootUri); + const sp = createServiceProvider(fs, new NullConsole()); + config.initializeFromJson(configFile, rootUri, sp, new TestAccessHost()); + const cloned = deserialize(serialize(config)); + + assert.deepEqual(config.ignore, cloned.ignore); + }); + + test('verify can serialize config options', () => { + const config = new ConfigOptions(UriEx.file(process.cwd())); + const serialized = serialize(config); + const deserialized = deserialize(serialized); + assert.deepEqual(config, deserialized); + assert.ok(deserialized.findExecEnvironment(UriEx.file('foo/bar.py'))); + }); + + test('extra paths on undefined execution root/default workspace', () => { + const nullConsole = new NullConsole(); + const service = createAnalyzer(nullConsole); + const commandLineOptions = new CommandLineOptions(undefined, /* fromLanguageServer */ false); + commandLineOptions.configSettings.extraPaths = ['/extraPaths']; + + service.setOptions(commandLineOptions); + const configOptions = service.test_getConfigOptions(commandLineOptions); + + const expectedExtraPaths = [Uri.file('/extraPaths', service.serviceProvider)]; + assert.deepStrictEqual( + configOptions.defaultExtraPaths?.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, /* fromLanguageServer */ 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.diagnosticRuleSet.strictListInference, true); + }); + + test('Typechecking mode is standard when just config file is present', () => { + const cwd = normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project_with_pyproject_toml')); + const service = createAnalyzer(); + const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ true); + commandLineOptions.configSettings.typeCheckingMode = 'off'; + + service.setOptions(commandLineOptions); + + const configOptions = service.test_getConfigOptions(commandLineOptions); + assert.equal(configOptions.diagnosticRuleSet.reportPossiblyUnboundVariable, 'error'); + }); + + test('Typechecking mode depends upon if vscode extension or not', () => { + const cwd = normalizePath(combinePaths(process.cwd(), 'src/tests/samples/package1')); + let service = createAnalyzer(); + let commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ true); + + service.setOptions(commandLineOptions); + + let configOptions = service.test_getConfigOptions(commandLineOptions); + assert.equal(configOptions.diagnosticRuleSet.reportPossiblyUnboundVariable, 'none'); + + service = createAnalyzer(); + commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ false); + + service.setOptions(commandLineOptions); + + configOptions = service.test_getConfigOptions(commandLineOptions); + assert.equal(configOptions.diagnosticRuleSet.reportPossiblyUnboundVariable, 'error'); + + commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ true); + commandLineOptions.configSettings.typeCheckingMode = 'strict'; + service = createAnalyzer(); + service.setOptions(commandLineOptions); + + configOptions = service.test_getConfigOptions(commandLineOptions); + assert.equal(configOptions.diagnosticRuleSet.reportPossiblyUnboundVariable, 'error'); + }); + + test('Include file paths are only set in the config file when using extension', () => { + const cwd = normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project1')); + const service = createAnalyzer(); + const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ true); + commandLineOptions.configSettings.includeFileSpecs = ['test']; + service.setOptions(commandLineOptions); + + const configOptions = service.test_getConfigOptions(commandLineOptions); + assert.equal(configOptions.include.length, 1); + assert.ok(configOptions.include[0].regExp.source.includes('/subfolder1)')); + }); + + test('Include file paths can be added to on the command line with a config', () => { + const cwd = normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project1')); + const service = createAnalyzer(); + const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ false); + commandLineOptions.configSettings.includeFileSpecs = ['test']; + service.setOptions(commandLineOptions); + + const configOptions = service.test_getConfigOptions(commandLineOptions); + assert.equal(configOptions.include.length, 2); + assert.ok(configOptions.include[1].regExp.source.includes('/test)')); + }); + + test('Include file paths can be added to by an extension without a config', () => { + const cwd = normalizePath(combinePaths(process.cwd(), 'src/tests/samples/package1')); + const service = createAnalyzer(); + const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ true); + commandLineOptions.configSettings.includeFileSpecs = ['test']; + service.setOptions(commandLineOptions); + + const configOptions = service.test_getConfigOptions(commandLineOptions); + assert.equal(configOptions.include.length, 1); + assert.ok(configOptions.include[0].regExp.source.includes('/test)')); + }); + + test('Command line options can override config but only when not using extension', () => { + const cwd = normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project_with_all_config')); + const service = createAnalyzer(); + const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ false); + service.setOptions(commandLineOptions); + + // First get the default. + const defaultOptions = service.test_getConfigOptions(commandLineOptions); + + // Now set all of the different options and make sure the command line options override. + commandLineOptions.configSettings.typeCheckingMode = 'strict'; + commandLineOptions.configSettings.venvPath = 'test2'; + commandLineOptions.configSettings.typeshedPath = 'test2'; + commandLineOptions.configSettings.stubPath = 'test2'; + commandLineOptions.configSettings.useLibraryCodeForTypes = true; + commandLineOptions.configSettings.includeFileSpecs = ['test2']; + commandLineOptions.configSettings.excludeFileSpecs = ['test2']; + commandLineOptions.configSettings.diagnosticSeverityOverrides = { + reportMissingImports: DiagnosticSeverityOverrides.Error, + }; + commandLineOptions.configSettings.ignoreFileSpecs = ['test2']; + + service.setOptions(commandLineOptions); + const overriddenOptions = service.test_getConfigOptions(commandLineOptions); + assert.notDeepStrictEqual(defaultOptions.include, overriddenOptions.include); + assert.notDeepStrictEqual(defaultOptions.exclude, overriddenOptions.exclude); + assert.notDeepStrictEqual(defaultOptions.ignore, overriddenOptions.ignore); + assert.notDeepStrictEqual(defaultOptions.diagnosticRuleSet, overriddenOptions.diagnosticRuleSet); + assert.notDeepStrictEqual(defaultOptions.venvPath, overriddenOptions.venvPath); + // Typeshed and stub path are an exception, it should just be reported as a dupe. + assert.deepStrictEqual(defaultOptions.typeshedPath, overriddenOptions.typeshedPath); + assert.deepStrictEqual(defaultOptions.stubPath, overriddenOptions.stubPath); + + // Do the same with an extension based config, but make sure we get the default back. + const commandLineOptions2 = new CommandLineOptions(cwd, /* fromLanguageServer */ true); + service.setOptions(commandLineOptions2); + const overriddenOptions2 = service.test_getConfigOptions(commandLineOptions2); + assert.deepStrictEqual(defaultOptions, overriddenOptions2); + }); + + test('Config venvPath take precedences over language server settings', () => { + const cwd = normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project_with_all_config')); + const service = createAnalyzer(); + const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ true); + commandLineOptions.languageServerSettings.venvPath = 'test_from_language_server'; + service.setOptions(commandLineOptions); + + // Verify language server options don't override + const options = service.test_getConfigOptions(commandLineOptions); + assert.equal(options.venvPath?.pathIncludes('from_language_server'), false); + }); + + test('Command line venvPath take precedences over everything else', () => { + const cwd = normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project_with_all_config')); + const service = createAnalyzer(); + const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ false); + commandLineOptions.configSettings.venvPath = 'test_from_command_line'; + commandLineOptions.languageServerSettings.venvPath = 'test_from_language_server'; + service.setOptions(commandLineOptions); + + // Verify command line overrides everything + const options = service.test_getConfigOptions(commandLineOptions); + assert.ok(options.venvPath?.pathIncludes('test_from_command_line')); + }); + + test('Config empty venvPath does not take precedences over language server settings', () => { + const cwd = normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project_src_with_config_extra_paths')); + const service = createAnalyzer(); + const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ true); + commandLineOptions.languageServerSettings.venvPath = 'test_from_language_server'; + service.setOptions(commandLineOptions); + + // Verify language server options don't override + const options = service.test_getConfigOptions(commandLineOptions); + assert.ok(options.venvPath?.pathIncludes('from_language_server')); + }); + + test('Language server specific settings are set whether or not there is a pyproject.toml', () => { + const cwd = normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project_with_all_config')); + const service = createAnalyzer(); + const commandLineOptions = new CommandLineOptions(cwd, /* fromLanguageServer */ false); + commandLineOptions.languageServerSettings.autoImportCompletions = true; + commandLineOptions.languageServerSettings.indexing = true; + commandLineOptions.languageServerSettings.taskListTokens = [{ priority: TaskListPriority.High, text: 'test' }]; + commandLineOptions.languageServerSettings.logTypeEvaluationTime = true; + commandLineOptions.languageServerSettings.typeEvaluationTimeThreshold = 1; + commandLineOptions.languageServerSettings.enableAmbientAnalysis = false; + commandLineOptions.languageServerSettings.disableTaggedHints = true; + commandLineOptions.languageServerSettings.watchForSourceChanges = true; + commandLineOptions.languageServerSettings.watchForLibraryChanges = true; + commandLineOptions.languageServerSettings.watchForConfigChanges = true; + commandLineOptions.languageServerSettings.typeStubTargetImportName = 'test'; + commandLineOptions.languageServerSettings.checkOnlyOpenFiles = true; + commandLineOptions.languageServerSettings.disableTaggedHints = true; + commandLineOptions.languageServerSettings.pythonPath = 'test_python_path'; + + service.setOptions(commandLineOptions); + let options = service.test_getConfigOptions(commandLineOptions); + assert.strictEqual(options.autoImportCompletions, true); + assert.strictEqual(options.indexing, true); + assert.strictEqual(options.taskListTokens?.length, 1); + assert.strictEqual(options.logTypeEvaluationTime, true); + assert.strictEqual(options.typeEvaluationTimeThreshold, 1); + assert.strictEqual(options.disableTaggedHints, true); + assert.ok(options.pythonPath?.pathIncludes('test_python_path')); + + // Test with language server set to true to make sure they are still set. + commandLineOptions.fromLanguageServer = true; + commandLineOptions.languageServerSettings.venvPath = 'test_venv_path'; + service.setOptions(commandLineOptions); + options = service.test_getConfigOptions(commandLineOptions); + assert.strictEqual(options.autoImportCompletions, true); + assert.strictEqual(options.indexing, true); + assert.strictEqual(options.taskListTokens?.length, 1); + assert.strictEqual(options.logTypeEvaluationTime, true); + assert.strictEqual(options.typeEvaluationTimeThreshold, 1); + assert.strictEqual(options.disableTaggedHints, true); + assert.ok(options.pythonPath?.pathIncludes('test_python_path')); + + // Verify language server options don't override the config setting. Only command line should + assert.equal(options.venvPath?.pathIncludes('test_venv_path'), false); + }); + + function createAnalyzer(console?: ConsoleInterface) { + const cons = console ?? new NullConsole(); + const fs = createFromRealFileSystem(tempFile, cons); + const serviceProvider = createServiceProvider(fs, cons, tempFile); + return new AnalyzerService('', serviceProvider, { console: cons }); + } }); diff --git a/packages/pyright-internal/src/tests/harness/fourslash/testState.ts b/packages/pyright-internal/src/tests/harness/fourslash/testState.ts index 87eb2f37b886..f01a6ea09fab 100644 --- a/packages/pyright-internal/src/tests/harness/fourslash/testState.ts +++ b/packages/pyright-internal/src/tests/harness/fourslash/testState.ts @@ -98,6 +98,7 @@ import { getRangeByMarkerName, } from './testStateUtils'; import { verifyWorkspaceEdit } from './workspaceEditTestUtils'; +import { Host } from '../../../common/host'; export interface TextChange { span: TextRange; @@ -187,11 +188,12 @@ export class TestState { this._applyTestConfigOptions(configOptions); } - const service = this._createAnalysisService( + const service = this.createAnalysisService( this.console, this._hostSpecificFeatures.importResolverFactory, this._hostSpecificFeatures.backgroundAnalysisProgramFactory, - configOptions + configOptions, + testAccessHost ); this.workspace = { @@ -1703,6 +1705,39 @@ export class TestState { } } + protected createAnalysisService( + nullConsole: ConsoleInterface, + importResolverFactory: ImportResolverFactory, + backgroundAnalysisProgramFactory: BackgroundAnalysisProgramFactory, + configOptions: ConfigOptions, + host: Host + ) { + // we do not initiate automatic analysis or file watcher in test. + const service = new AnalyzerService('test service', this.serviceProvider, { + console: nullConsole, + hostFactory: () => host, + importResolverFactory, + backgroundAnalysisProgramFactory, + configOptions, + fileSystem: this.fs, + libraryReanalysisTimeProvider: () => 0, + }); + + // directly set files to track rather than using fileSpec from config + // to discover those files from file system + service.test_program.setTrackedFiles( + this.files + .filter((path) => { + const fileExtension = getFileExtension(path).toLowerCase(); + return fileExtension === '.py' || fileExtension === '.pyi'; + }) + .map((path) => Uri.file(path, this.serviceProvider)) + .filter((path) => service.isTracked(path)) + ); + + return service; + } + private _convertGlobalOptionsToConfigOptions(projectRoot: string, mountPaths?: Map): ConfigOptions { const configOptions = new ConfigOptions(Uri.file(projectRoot, this.serviceProvider)); @@ -1958,38 +1993,6 @@ export class TestState { return new Map(results); } - private _createAnalysisService( - nullConsole: ConsoleInterface, - importResolverFactory: ImportResolverFactory, - backgroundAnalysisProgramFactory: BackgroundAnalysisProgramFactory, - configOptions: ConfigOptions - ) { - // we do not initiate automatic analysis or file watcher in test. - const service = new AnalyzerService('test service', this.serviceProvider, { - console: nullConsole, - hostFactory: () => testAccessHost, - importResolverFactory, - backgroundAnalysisProgramFactory, - configOptions, - fileSystem: this.fs, - libraryReanalysisTimeProvider: () => 0, - }); - - // directly set files to track rather than using fileSpec from config - // to discover those files from file system - service.test_program.setTrackedFiles( - this.files - .filter((path) => { - const fileExtension = getFileExtension(path).toLowerCase(); - return fileExtension === '.py' || fileExtension === '.pyi'; - }) - .map((path) => Uri.file(path, this.serviceProvider)) - .filter((path) => service.isTracked(path)) - ); - - return service; - } - private _deepEqual(a: any, e: any) { try { // NOTE: find better way. diff --git a/packages/pyright-internal/src/tests/importResolver.test.ts b/packages/pyright-internal/src/tests/importResolver.test.ts index aa42bd06edd8..9032e0c8a655 100644 --- a/packages/pyright-internal/src/tests/importResolver.test.ts +++ b/packages/pyright-internal/src/tests/importResolver.test.ts @@ -32,39 +32,293 @@ function usingTrueVenv() { return process.env.CI_IMPORT_TEST_VENVPATH !== undefined || process.env.CI_IMPORT_TEST_PYTHONPATH !== undefined; } -if (!usingTrueVenv()) { - describe('Import tests that cannot run in a true venv', () => { - test('partial stub file exists', () => { +describe('Import tests with fake venv', () => { + const tempFile = new RealTempFile(); + + afterAll(() => tempFile.dispose()); + + if (!usingTrueVenv()) { + describe('Import tests that cannot run in a true venv', () => { + test('partial stub file exists', () => { + const files = [ + { + path: combinePaths(libraryRoot, 'myLib-stubs', 'partialStub.pyi'), + content: 'def test(): ...', + }, + { + path: combinePaths(libraryRoot, 'myLib-stubs', 'py.typed'), + content: 'partial\n', + }, + { + path: combinePaths(libraryRoot, 'myLib', 'partialStub.py'), + content: 'def test(): pass', + }, + ]; + + const importResult = getImportResult(files, ['myLib', 'partialStub']); + assert(importResult.isImportFound); + assert(importResult.isStubFile); + assert.strictEqual( + 1, + importResult.resolvedUris.filter( + (f) => !f.isEmpty() && f.getFilePath() === combinePaths(libraryRoot, 'myLib', 'partialStub.pyi') + ).length + ); + }); + + test('partial stub __init__ exists', () => { + const files = [ + { + path: combinePaths(libraryRoot, 'myLib-stubs', '__init__.pyi'), + content: 'def test(): ...', + }, + { + path: combinePaths(libraryRoot, 'myLib-stubs', 'py.typed'), + content: 'partial\n', + }, + { + path: combinePaths(libraryRoot, 'myLib', '__init__.py'), + content: 'def test(): pass', + }, + ]; + + const importResult = getImportResult(files, ['myLib']); + assert(importResult.isImportFound); + assert(importResult.isStubFile); + assert.strictEqual( + 1, + importResult.resolvedUris.filter( + (f) => f.getFilePath() === combinePaths(libraryRoot, 'myLib', '__init__.pyi') + ).length + ); + }); + + test('stub package', () => { + const files = [ + { + path: combinePaths(libraryRoot, 'myLib-stubs', 'stub.pyi'), + content: '# empty', + }, + { + path: combinePaths(libraryRoot, 'myLib-stubs', '__init__.pyi'), + content: '# empty', + }, + { + path: combinePaths(libraryRoot, 'myLib', 'partialStub.py'), + content: 'def test(): pass', + }, + ]; + + // If fully typed stub package exists, that wins over the real package. + const importResult = getImportResult(files, ['myLib', 'partialStub']); + assert(!importResult.isImportFound); + }); + + test('partial stub package in typing folder', () => { + const typingFolder = combinePaths(normalizeSlashes('/'), 'typing'); + const files = [ + { + path: combinePaths(typingFolder, 'myLib-stubs', '__init__.pyi'), + content: 'def test(): ...', + }, + { + path: combinePaths(typingFolder, 'myLib-stubs', 'py.typed'), + content: 'partial\n', + }, + { + path: combinePaths(libraryRoot, 'myLib', '__init__.py'), + content: 'def test(): pass', + }, + ]; + + const importResult = getImportResult(files, ['myLib'], (c) => (c.stubPath = UriEx.file(typingFolder))); + assert(importResult.isImportFound); + assert(importResult.isStubFile); + assert.strictEqual( + 1, + importResult.resolvedUris.filter( + (f) => f.getFilePath() === combinePaths(libraryRoot, 'myLib', '__init__.pyi') + ).length + ); + }); + + test('typeshed folder', () => { + const typeshedFolder = combinePaths(normalizeSlashes('/'), 'ts'); + const files = [ + { + path: combinePaths(libraryRoot, 'myLib-stubs', '__init__.pyi'), + content: 'def test(): ...', + }, + { + path: combinePaths(libraryRoot, 'myLib-stubs', 'py.typed'), + content: 'partial\n', + }, + { + path: combinePaths(typeshedFolder, 'stubs', 'myLibPackage', 'myLib.pyi'), + content: '# empty', + }, + { + path: combinePaths(libraryRoot, 'myLib', '__init__.py'), + content: 'def test(): pass', + }, + ]; + + // Stub packages win over typeshed. + const importResult = getImportResult( + files, + ['myLib'], + (c) => (c.typeshedPath = UriEx.file(typeshedFolder)) + ); + assert(importResult.isImportFound); + assert(importResult.isStubFile); + assert.strictEqual( + 1, + importResult.resolvedUris.filter( + (f) => f.getFilePath() === combinePaths(libraryRoot, 'myLib', '__init__.pyi') + ).length + ); + }); + + test('typeshed fallback folder', () => { + const files = [ + { + path: combinePaths(libraryRoot, 'myLib-stubs', '__init__.pyi'), + content: 'def test(): ...', + }, + { + path: combinePaths(libraryRoot, 'myLib-stubs', 'py.typed'), + content: 'partial\n', + }, + { + path: combinePaths('/', typeshedFallback, 'stubs', 'myLibPackage', 'myLib.pyi'), + content: '# empty', + }, + { + path: combinePaths(libraryRoot, 'myLib', '__init__.py'), + content: 'def test(): pass', + }, + ]; + + // Stub packages win over typeshed. + const importResult = getImportResult(files, ['myLib']); + assert(importResult.isImportFound); + assert(importResult.isStubFile); + assert.strictEqual( + 1, + importResult.resolvedUris.filter( + (f) => f.getFilePath() === combinePaths(libraryRoot, 'myLib', '__init__.pyi') + ).length + ); + }); + + test('py.typed file', () => { + const files = [ + { + path: combinePaths(libraryRoot, 'myLib-stubs', '__init__.pyi'), + content: 'def test(): ...', + }, + { + path: combinePaths(libraryRoot, 'myLib-stubs', 'py.typed'), + content: 'partial\n', + }, + { + path: combinePaths(libraryRoot, 'myLib', '__init__.py'), + content: 'def test(): pass', + }, + { + path: combinePaths(libraryRoot, 'myLib', 'py.typed'), + content: '# typed', + }, + ]; + + // Partial stub package always overrides original package. + const importResult = getImportResult(files, ['myLib']); + assert(importResult.isImportFound); + assert(importResult.isStubFile); + }); + + test('py.typed library', () => { + const files = [ + { + path: combinePaths(libraryRoot, 'os', '__init__.py'), + content: 'def test(): ...', + }, + { + path: combinePaths(libraryRoot, 'os', 'py.typed'), + content: '', + }, + { + path: combinePaths('/', typeshedFallback, 'stubs', 'os', 'os', '__init__.pyi'), + content: '# empty', + }, + ]; + + const importResult = getImportResult(files, ['os']); + assert(importResult.isImportFound); + assert.strictEqual( + files[0].path, + importResult.resolvedUris[importResult.resolvedUris.length - 1].getFilePath() + ); + }); + + test('import side by side file sub under lib folder', () => { + const files = [ + { + path: combinePaths('/lib/site-packages/myLib', 'file1.py'), + content: 'def test1(): ...', + }, + { + path: combinePaths('/lib/site-packages/myLib', 'file2.py'), + content: 'def test2(): ...', + }, + ]; + + const importResult = getImportResult(files, ['file1']); + assert(!importResult.isImportFound); + }); + }); + + test('getModuleNameForImport library file', () => { const files = [ { - path: combinePaths(libraryRoot, 'myLib-stubs', 'partialStub.pyi'), - content: 'def test(): ...', + path: combinePaths(libraryRoot, 'myLib', 'myModule', 'file1.py'), + content: '# empty', }, + ]; + + const moduleImportInfo = getModuleNameForImport(files); + + assert.strictEqual(moduleImportInfo.importType, ImportType.ThirdParty); + assert(!moduleImportInfo.isThirdPartyPyTypedPresent); + assert(!moduleImportInfo.isLocalTypingsFile); + }); + + test('getModuleNameForImport py.typed library file', () => { + const files = [ { - path: combinePaths(libraryRoot, 'myLib-stubs', 'py.typed'), - content: 'partial\n', + path: combinePaths(libraryRoot, 'myLib', 'py.typed'), + content: '', }, { - path: combinePaths(libraryRoot, 'myLib', 'partialStub.py'), - content: 'def test(): pass', + path: combinePaths(libraryRoot, 'myLib', 'myModule', 'file1.py'), + content: '# empty', }, ]; - const importResult = getImportResult(files, ['myLib', 'partialStub']); - assert(importResult.isImportFound); - assert(importResult.isStubFile); - assert.strictEqual( - 1, - importResult.resolvedUris.filter( - (f) => !f.isEmpty() && f.getFilePath() === combinePaths(libraryRoot, 'myLib', 'partialStub.pyi') - ).length - ); + const moduleImportInfo = getModuleNameForImport(files); + + assert.strictEqual(moduleImportInfo.importType, ImportType.ThirdParty); + assert(moduleImportInfo.isThirdPartyPyTypedPresent); + assert(!moduleImportInfo.isLocalTypingsFile); }); + } - test('partial stub __init__ exists', () => { + describe('Import tests that can run with or without a true venv', () => { + test('side by side files', () => { + const myFile = combinePaths('src', 'file.py'); const files = [ { - path: combinePaths(libraryRoot, 'myLib-stubs', '__init__.pyi'), + path: combinePaths(libraryRoot, 'myLib-stubs', 'partialStub.pyi'), content: 'def test(): ...', }, { @@ -72,32 +326,70 @@ if (!usingTrueVenv()) { content: 'partial\n', }, { - path: combinePaths(libraryRoot, 'myLib', '__init__.py'), + path: combinePaths(libraryRoot, 'myLib', 'partialStub.pyi'), + content: '# empty', + }, + { + path: combinePaths(libraryRoot, 'myLib', 'partialStub.py'), + content: 'def test(): pass', + }, + { + path: combinePaths(libraryRoot, 'myLib-stubs', 'partialStub2.pyi'), + content: 'def test(): ...', + }, + { + path: combinePaths(libraryRoot, 'myLib', 'partialStub2.py'), content: 'def test(): pass', }, + { + path: myFile, + content: '# not used', + }, ]; - const importResult = getImportResult(files, ['myLib']); - assert(importResult.isImportFound); - assert(importResult.isStubFile); - assert.strictEqual( - 1, - importResult.resolvedUris.filter( - (f) => f.getFilePath() === combinePaths(libraryRoot, 'myLib', '__init__.pyi') - ).length + const sp = createServiceProviderFromFiles(files); + const configOptions = new ConfigOptions(UriEx.file('/')); + const importResolver = new ImportResolver( + sp, + configOptions, + new TestAccessHost(sp.fs().getModulePath(), [UriEx.file(libraryRoot)]) ); + + // Stub package wins over original package (per PEP 561 rules). + const myUri = UriEx.file(myFile); + const sideBySideResult = importResolver.resolveImport(myUri, configOptions.findExecEnvironment(myUri), { + leadingDots: 0, + nameParts: ['myLib', 'partialStub'], + importedSymbols: new Set(), + }); + + assert(sideBySideResult.isImportFound); + assert(sideBySideResult.isStubFile); + + const sideBySideStubFile = UriEx.file(combinePaths(libraryRoot, 'myLib', 'partialStub.pyi')); + assert.strictEqual(1, sideBySideResult.resolvedUris.filter((f) => f.key === sideBySideStubFile.key).length); + assert.strictEqual('def test(): ...', sp.fs().readFileSync(sideBySideStubFile, 'utf8')); + + // Side by side stub doesn't completely disable partial stub. + const partialStubResult = importResolver.resolveImport(myUri, configOptions.findExecEnvironment(myUri), { + leadingDots: 0, + nameParts: ['myLib', 'partialStub2'], + importedSymbols: new Set(), + }); + + assert(partialStubResult.isImportFound); + assert(partialStubResult.isStubFile); + + const partialStubFile = UriEx.file(combinePaths(libraryRoot, 'myLib', 'partialStub2.pyi')); + assert.strictEqual(1, partialStubResult.resolvedUris.filter((f) => f.key === partialStubFile.key).length); }); - test('stub package', () => { + test('stub namespace package', () => { const files = [ { path: combinePaths(libraryRoot, 'myLib-stubs', 'stub.pyi'), content: '# empty', }, - { - path: combinePaths(libraryRoot, 'myLib-stubs', '__init__.pyi'), - content: '# empty', - }, { path: combinePaths(libraryRoot, 'myLib', 'partialStub.py'), content: 'def test(): pass', @@ -106,24 +398,35 @@ if (!usingTrueVenv()) { // If fully typed stub package exists, that wins over the real package. const importResult = getImportResult(files, ['myLib', 'partialStub']); - assert(!importResult.isImportFound); + assert(importResult.isImportFound); + assert(!importResult.isStubFile); + assert.strictEqual( + 1, + importResult.resolvedUris.filter( + (f) => !f.isEmpty() && f.getFilePath() === combinePaths(libraryRoot, 'myLib', 'partialStub.py') + ).length + ); }); - test('partial stub package in typing folder', () => { + test('py.typed namespace package plus stubs', () => { const typingFolder = combinePaths(normalizeSlashes('/'), 'typing'); const files = [ { - path: combinePaths(typingFolder, 'myLib-stubs', '__init__.pyi'), - content: 'def test(): ...', + path: combinePaths(typingFolder, 'myLib/core', 'foo.pyi'), + content: 'def test(): pass', }, { - path: combinePaths(typingFolder, 'myLib-stubs', 'py.typed'), - content: 'partial\n', + path: combinePaths(libraryRoot, 'myLib', 'py.typed'), + content: '', }, { path: combinePaths(libraryRoot, 'myLib', '__init__.py'), content: 'def test(): pass', }, + { + path: combinePaths(libraryRoot, 'myLib', '__init__.pyi'), + content: 'def test(): pass', + }, ]; const importResult = getImportResult(files, ['myLib'], (c) => (c.stubPath = UriEx.file(typingFolder))); @@ -132,13 +435,13 @@ if (!usingTrueVenv()) { assert.strictEqual( 1, importResult.resolvedUris.filter( - (f) => f.getFilePath() === combinePaths(libraryRoot, 'myLib', '__init__.pyi') + (f) => !f.isEmpty() && f.getFilePath() === combinePaths(libraryRoot, 'myLib', '__init__.pyi') ).length ); }); - test('typeshed folder', () => { - const typeshedFolder = combinePaths(normalizeSlashes('/'), 'ts'); + test('stub in typing folder over partial stub package', () => { + const typingFolder = combinePaths(normalizeSlashes('/'), 'typing'); const files = [ { path: combinePaths(libraryRoot, 'myLib-stubs', '__init__.pyi'), @@ -149,7 +452,7 @@ if (!usingTrueVenv()) { content: 'partial\n', }, { - path: combinePaths(typeshedFolder, 'stubs', 'myLibPackage', 'myLib.pyi'), + path: combinePaths(typingFolder, 'myLib.pyi'), content: '# empty', }, { @@ -158,716 +461,426 @@ if (!usingTrueVenv()) { }, ]; - // Stub packages win over typeshed. - const importResult = getImportResult( - files, - ['myLib'], - (c) => (c.typeshedPath = UriEx.file(typeshedFolder)) - ); + // If the package exists in typing folder, that gets picked up first. + const importResult = getImportResult(files, ['myLib'], (c) => (c.stubPath = UriEx.file(typingFolder))); assert(importResult.isImportFound); assert(importResult.isStubFile); assert.strictEqual( - 1, + 0, importResult.resolvedUris.filter( (f) => f.getFilePath() === combinePaths(libraryRoot, 'myLib', '__init__.pyi') ).length ); }); - test('typeshed fallback folder', () => { + test('non py.typed library', () => { const files = [ { - path: combinePaths(libraryRoot, 'myLib-stubs', '__init__.pyi'), + path: combinePaths(libraryRoot, 'os', '__init__.py'), content: 'def test(): ...', }, { - path: combinePaths(libraryRoot, 'myLib-stubs', 'py.typed'), - content: 'partial\n', + path: combinePaths('/', typeshedFallback, 'stubs', 'os', 'os', '__init__.pyi'), + content: '# empty', }, + ]; + + const importResult = getImportResult(files, ['os']); + assert(importResult.isImportFound); + assert.strictEqual( + files[1].path, + importResult.resolvedUris[importResult.resolvedUris.length - 1].getFilePath() + ); + }); + + test('no empty import roots', () => { + const sp = createServiceProviderFromFiles([]); + const configOptions = new ConfigOptions(Uri.empty()); // Empty, like open-file mode. + const importResolver = new ImportResolver( + sp, + configOptions, + new TestAccessHost(sp.fs().getModulePath(), [UriEx.file(libraryRoot)]) + ); + importResolver.getImportRoots(configOptions.getDefaultExecEnvironment()).forEach((path) => assert(path)); + }); + + test('multiple typeshedFallback', () => { + const files = [ { - path: combinePaths('/', typeshedFallback, 'stubs', 'myLibPackage', 'myLib.pyi'), + path: combinePaths('/', typeshedFallback, 'stubs', 'aLib', 'aLib', '__init__.pyi'), content: '# empty', }, { - path: combinePaths(libraryRoot, 'myLib', '__init__.py'), - content: 'def test(): pass', + path: combinePaths('/', typeshedFallback, 'stubs', 'bLib', 'bLib', '__init__.pyi'), + content: '# empty', }, ]; - // Stub packages win over typeshed. - const importResult = getImportResult(files, ['myLib']); - assert(importResult.isImportFound); - assert(importResult.isStubFile); + const sp = createServiceProviderFromFiles(files); + const configOptions = new ConfigOptions(Uri.empty()); // Empty, like open-file mode. + const importResolver = new ImportResolver( + sp, + configOptions, + new TestAccessHost(sp.fs().getModulePath(), [UriEx.file(libraryRoot)]) + ); + const importRoots = importResolver.getImportRoots(configOptions.getDefaultExecEnvironment()); + assert.strictEqual( 1, - importResult.resolvedUris.filter( - (f) => f.getFilePath() === combinePaths(libraryRoot, 'myLib', '__init__.pyi') + importRoots.filter( + (f) => !f.isEmpty() && f.getFilePath() === combinePaths('/', typeshedFallback, 'stubs', 'aLib') + ).length + ); + assert.strictEqual( + 1, + importRoots.filter( + (f) => !f.isEmpty() && f.getFilePath() === combinePaths('/', typeshedFallback, 'stubs', 'bLib') ).length ); }); - test('py.typed file', () => { + test('import side by side file root', () => { const files = [ { - path: combinePaths(libraryRoot, 'myLib-stubs', '__init__.pyi'), - content: 'def test(): ...', + path: combinePaths('/', 'file1.py'), + content: 'def test1(): ...', }, { - path: combinePaths(libraryRoot, 'myLib-stubs', 'py.typed'), - content: 'partial\n', + path: combinePaths('/', 'file2.py'), + content: 'def test2(): ...', }, + ]; + + const importResult = getImportResult(files, ['file1']); + assert(importResult.isImportFound); + assert.strictEqual( + 1, + importResult.resolvedUris.filter((f) => f.getFilePath() === combinePaths('/', 'file1.py')).length + ); + }); + + test('import side by side file sub folder', () => { + const files = [ { - path: combinePaths(libraryRoot, 'myLib', '__init__.py'), - content: 'def test(): pass', + path: combinePaths('/test', 'file1.py'), + content: 'def test1(): ...', }, { - path: combinePaths(libraryRoot, 'myLib', 'py.typed'), - content: '# typed', + path: combinePaths('/test', 'file2.py'), + content: 'def test2(): ...', }, ]; - // Partial stub package always overrides original package. - const importResult = getImportResult(files, ['myLib']); + const importResult = getImportResult(files, ['file1']); assert(importResult.isImportFound); - assert(importResult.isStubFile); + assert.strictEqual( + 1, + importResult.resolvedUris.filter((f) => f.getFilePath() === combinePaths('/test', 'file1.py')).length + ); }); - test('py.typed library', () => { + test('import side by side file sub under src folder', () => { const files = [ { - path: combinePaths(libraryRoot, 'os', '__init__.py'), - content: 'def test(): ...', - }, - { - path: combinePaths(libraryRoot, 'os', 'py.typed'), - content: '', + path: combinePaths('/src/nested', 'file1.py'), + content: 'def test1(): ...', }, { - path: combinePaths('/', typeshedFallback, 'stubs', 'os', 'os', '__init__.pyi'), - content: '# empty', + path: combinePaths('/src/nested', 'file2.py'), + content: 'def test2(): ...', }, ]; - const importResult = getImportResult(files, ['os']); + const importResult = getImportResult(files, ['file1']); assert(importResult.isImportFound); assert.strictEqual( - files[0].path, - importResult.resolvedUris[importResult.resolvedUris.length - 1].getFilePath() + 1, + importResult.resolvedUris.filter((f) => f.getFilePath() === combinePaths('/src/nested', 'file1.py')) + .length ); }); - test('import side by side file sub under lib folder', () => { + test('import file sub under containing folder', () => { const files = [ { - path: combinePaths('/lib/site-packages/myLib', 'file1.py'), + path: combinePaths('/src/nested', 'file1.py'), content: 'def test1(): ...', }, { - path: combinePaths('/lib/site-packages/myLib', 'file2.py'), + path: combinePaths('/src/nested/nested2', 'file2.py'), content: 'def test2(): ...', }, ]; const importResult = getImportResult(files, ['file1']); - assert(!importResult.isImportFound); + assert(importResult.isImportFound); + assert.strictEqual( + 1, + importResult.resolvedUris.filter((f) => f.getFilePath() === combinePaths('/src/nested', 'file1.py')) + .length + ); }); - }); - - test('getModuleNameForImport library file', () => { - const files = [ - { - path: combinePaths(libraryRoot, 'myLib', 'myModule', 'file1.py'), - content: '# empty', - }, - ]; - - const moduleImportInfo = getModuleNameForImport(files); - assert.strictEqual(moduleImportInfo.importType, ImportType.ThirdParty); - assert(!moduleImportInfo.isThirdPartyPyTypedPresent); - assert(!moduleImportInfo.isLocalTypingsFile); - }); - - test('getModuleNameForImport py.typed library file', () => { - const files = [ - { - path: combinePaths(libraryRoot, 'myLib', 'py.typed'), - content: '', - }, - { - path: combinePaths(libraryRoot, 'myLib', 'myModule', 'file1.py'), - content: '# empty', - }, - ]; - - const moduleImportInfo = getModuleNameForImport(files); - - assert.strictEqual(moduleImportInfo.importType, ImportType.ThirdParty); - assert(moduleImportInfo.isThirdPartyPyTypedPresent); - assert(!moduleImportInfo.isLocalTypingsFile); - }); -} - -describe('Import tests that can run with or without a true venv', () => { - test('side by side files', () => { - const myFile = combinePaths('src', 'file.py'); - const files = [ - { - path: combinePaths(libraryRoot, 'myLib-stubs', 'partialStub.pyi'), - content: 'def test(): ...', - }, - { - path: combinePaths(libraryRoot, 'myLib-stubs', 'py.typed'), - content: 'partial\n', - }, - { - path: combinePaths(libraryRoot, 'myLib', 'partialStub.pyi'), - content: '# empty', - }, - { - path: combinePaths(libraryRoot, 'myLib', 'partialStub.py'), - content: 'def test(): pass', - }, - { - path: combinePaths(libraryRoot, 'myLib-stubs', 'partialStub2.pyi'), - content: 'def test(): ...', - }, - { - path: combinePaths(libraryRoot, 'myLib', 'partialStub2.py'), - content: 'def test(): pass', - }, - { - path: myFile, - content: '# not used', - }, - ]; - - const sp = createServiceProviderFromFiles(files); - const configOptions = new ConfigOptions(UriEx.file('/')); - const importResolver = new ImportResolver( - sp, - configOptions, - new TestAccessHost(sp.fs().getModulePath(), [UriEx.file(libraryRoot)]) - ); + test("don't walk up the root", () => { + const files = [ + { + path: combinePaths('/', 'file1.py'), + content: 'def test1(): ...', + }, + ]; - // Stub package wins over original package (per PEP 561 rules). - const myUri = UriEx.file(myFile); - const sideBySideResult = importResolver.resolveImport(myUri, configOptions.findExecEnvironment(myUri), { - leadingDots: 0, - nameParts: ['myLib', 'partialStub'], - importedSymbols: new Set(), + const importResult = getImportResult(files, ['notExist'], (c) => (c.projectRoot = Uri.empty())); + assert(!importResult.isImportFound); }); - assert(sideBySideResult.isImportFound); - assert(sideBySideResult.isStubFile); - - const sideBySideStubFile = UriEx.file(combinePaths(libraryRoot, 'myLib', 'partialStub.pyi')); - assert.strictEqual(1, sideBySideResult.resolvedUris.filter((f) => f.key === sideBySideStubFile.key).length); - assert.strictEqual('def test(): ...', sp.fs().readFileSync(sideBySideStubFile, 'utf8')); + test('nested namespace package 1', () => { + const files = [ + { + path: combinePaths('/', 'packages1', 'a', 'b', 'c', 'd.py'), + content: 'def f(): pass', + }, + { + path: combinePaths('/', 'packages1', 'a', '__init__.py'), + content: '', + }, + { + path: combinePaths('/', 'packages2', 'a', '__init__.py'), + content: '', + }, + ]; - // Side by side stub doesn't completely disable partial stub. - const partialStubResult = importResolver.resolveImport(myUri, configOptions.findExecEnvironment(myUri), { - leadingDots: 0, - nameParts: ['myLib', 'partialStub2'], - importedSymbols: new Set(), + const importResult = getImportResult(files, ['a', 'b', 'c', 'd'], (config) => { + config.defaultExtraPaths = [ + UriEx.file(combinePaths('/', 'packages1')), + UriEx.file(combinePaths('/', 'packages2')), + ]; + }); + assert(importResult.isImportFound); }); - assert(partialStubResult.isImportFound); - assert(partialStubResult.isStubFile); - - const partialStubFile = UriEx.file(combinePaths(libraryRoot, 'myLib', 'partialStub2.pyi')); - assert.strictEqual(1, partialStubResult.resolvedUris.filter((f) => f.key === partialStubFile.key).length); - }); - - test('stub namespace package', () => { - const files = [ - { - path: combinePaths(libraryRoot, 'myLib-stubs', 'stub.pyi'), - content: '# empty', - }, - { - path: combinePaths(libraryRoot, 'myLib', 'partialStub.py'), - content: 'def test(): pass', - }, - ]; - - // If fully typed stub package exists, that wins over the real package. - const importResult = getImportResult(files, ['myLib', 'partialStub']); - assert(importResult.isImportFound); - assert(!importResult.isStubFile); - assert.strictEqual( - 1, - importResult.resolvedUris.filter( - (f) => !f.isEmpty() && f.getFilePath() === combinePaths(libraryRoot, 'myLib', 'partialStub.py') - ).length - ); - }); - - test('py.typed namespace package plus stubs', () => { - const typingFolder = combinePaths(normalizeSlashes('/'), 'typing'); - const files = [ - { - path: combinePaths(typingFolder, 'myLib/core', 'foo.pyi'), - content: 'def test(): pass', - }, - { - path: combinePaths(libraryRoot, 'myLib', 'py.typed'), - content: '', - }, - { - path: combinePaths(libraryRoot, 'myLib', '__init__.py'), - content: 'def test(): pass', - }, - { - path: combinePaths(libraryRoot, 'myLib', '__init__.pyi'), - content: 'def test(): pass', - }, - ]; - - const importResult = getImportResult(files, ['myLib'], (c) => (c.stubPath = UriEx.file(typingFolder))); - assert(importResult.isImportFound); - assert(importResult.isStubFile); - assert.strictEqual( - 1, - importResult.resolvedUris.filter( - (f) => !f.isEmpty() && f.getFilePath() === combinePaths(libraryRoot, 'myLib', '__init__.pyi') - ).length - ); - }); - - test('stub in typing folder over partial stub package', () => { - const typingFolder = combinePaths(normalizeSlashes('/'), 'typing'); - const files = [ - { - path: combinePaths(libraryRoot, 'myLib-stubs', '__init__.pyi'), - content: 'def test(): ...', - }, - { - path: combinePaths(libraryRoot, 'myLib-stubs', 'py.typed'), - content: 'partial\n', - }, - { - path: combinePaths(typingFolder, 'myLib.pyi'), - content: '# empty', - }, - { - path: combinePaths(libraryRoot, 'myLib', '__init__.py'), - content: 'def test(): pass', - }, - ]; - - // If the package exists in typing folder, that gets picked up first. - const importResult = getImportResult(files, ['myLib'], (c) => (c.stubPath = UriEx.file(typingFolder))); - assert(importResult.isImportFound); - assert(importResult.isStubFile); - assert.strictEqual( - 0, - importResult.resolvedUris.filter( - (f) => f.getFilePath() === combinePaths(libraryRoot, 'myLib', '__init__.pyi') - ).length - ); - }); - - test('non py.typed library', () => { - const files = [ - { - path: combinePaths(libraryRoot, 'os', '__init__.py'), - content: 'def test(): ...', - }, - { - path: combinePaths('/', typeshedFallback, 'stubs', 'os', 'os', '__init__.pyi'), - content: '# empty', - }, - ]; - - const importResult = getImportResult(files, ['os']); - assert(importResult.isImportFound); - assert.strictEqual( - files[1].path, - importResult.resolvedUris[importResult.resolvedUris.length - 1].getFilePath() - ); - }); - - test('no empty import roots', () => { - const sp = createServiceProviderFromFiles([]); - const configOptions = new ConfigOptions(Uri.empty()); // Empty, like open-file mode. - const importResolver = new ImportResolver( - sp, - configOptions, - new TestAccessHost(sp.fs().getModulePath(), [UriEx.file(libraryRoot)]) - ); - importResolver.getImportRoots(configOptions.getDefaultExecEnvironment()).forEach((path) => assert(path)); - }); - - test('multiple typeshedFallback', () => { - const files = [ - { - path: combinePaths('/', typeshedFallback, 'stubs', 'aLib', 'aLib', '__init__.pyi'), - content: '# empty', - }, - { - path: combinePaths('/', typeshedFallback, 'stubs', 'bLib', 'bLib', '__init__.pyi'), - content: '# empty', - }, - ]; - - const sp = createServiceProviderFromFiles(files); - const configOptions = new ConfigOptions(Uri.empty()); // Empty, like open-file mode. - const importResolver = new ImportResolver( - sp, - configOptions, - new TestAccessHost(sp.fs().getModulePath(), [UriEx.file(libraryRoot)]) - ); - const importRoots = importResolver.getImportRoots(configOptions.getDefaultExecEnvironment()); - - assert.strictEqual( - 1, - importRoots.filter( - (f) => !f.isEmpty() && f.getFilePath() === combinePaths('/', typeshedFallback, 'stubs', 'aLib') - ).length - ); - assert.strictEqual( - 1, - importRoots.filter( - (f) => !f.isEmpty() && f.getFilePath() === combinePaths('/', typeshedFallback, 'stubs', 'bLib') - ).length - ); - }); - - test('import side by side file root', () => { - const files = [ - { - path: combinePaths('/', 'file1.py'), - content: 'def test1(): ...', - }, - { - path: combinePaths('/', 'file2.py'), - content: 'def test2(): ...', - }, - ]; - - const importResult = getImportResult(files, ['file1']); - assert(importResult.isImportFound); - assert.strictEqual( - 1, - importResult.resolvedUris.filter((f) => f.getFilePath() === combinePaths('/', 'file1.py')).length - ); - }); - - test('import side by side file sub folder', () => { - const files = [ - { - path: combinePaths('/test', 'file1.py'), - content: 'def test1(): ...', - }, - { - path: combinePaths('/test', 'file2.py'), - content: 'def test2(): ...', - }, - ]; - - const importResult = getImportResult(files, ['file1']); - assert(importResult.isImportFound); - assert.strictEqual( - 1, - importResult.resolvedUris.filter((f) => f.getFilePath() === combinePaths('/test', 'file1.py')).length - ); - }); - - test('import side by side file sub under src folder', () => { - const files = [ - { - path: combinePaths('/src/nested', 'file1.py'), - content: 'def test1(): ...', - }, - { - path: combinePaths('/src/nested', 'file2.py'), - content: 'def test2(): ...', - }, - ]; - - const importResult = getImportResult(files, ['file1']); - assert(importResult.isImportFound); - assert.strictEqual( - 1, - importResult.resolvedUris.filter((f) => f.getFilePath() === combinePaths('/src/nested', 'file1.py')).length - ); - }); - - test('import file sub under containing folder', () => { - const files = [ - { - path: combinePaths('/src/nested', 'file1.py'), - content: 'def test1(): ...', - }, - { - path: combinePaths('/src/nested/nested2', 'file2.py'), - content: 'def test2(): ...', - }, - ]; - - const importResult = getImportResult(files, ['file1']); - assert(importResult.isImportFound); - assert.strictEqual( - 1, - importResult.resolvedUris.filter((f) => f.getFilePath() === combinePaths('/src/nested', 'file1.py')).length - ); - }); - - test("don't walk up the root", () => { - const files = [ - { - path: combinePaths('/', 'file1.py'), - content: 'def test1(): ...', - }, - ]; - - const importResult = getImportResult(files, ['notExist'], (c) => (c.projectRoot = Uri.empty())); - assert(!importResult.isImportFound); - }); - - test('nested namespace package 1', () => { - const files = [ - { - path: combinePaths('/', 'packages1', 'a', 'b', 'c', 'd.py'), - content: 'def f(): pass', - }, - { - path: combinePaths('/', 'packages1', 'a', '__init__.py'), - content: '', - }, - { - path: combinePaths('/', 'packages2', 'a', '__init__.py'), - content: '', - }, - ]; - - const importResult = getImportResult(files, ['a', 'b', 'c', 'd'], (config) => { - config.defaultExtraPaths = [ - UriEx.file(combinePaths('/', 'packages1')), - UriEx.file(combinePaths('/', 'packages2')), + test('nested namespace package 2', () => { + const files = [ + { + path: combinePaths('/', 'packages1', 'a', 'b', 'c', 'd.py'), + content: 'def f(): pass', + }, + { + path: combinePaths('/', 'packages1', 'a', 'b', 'c', '__init__.py'), + content: '', + }, + { + path: combinePaths('/', 'packages2', 'a', 'b', 'c', '__init__.py'), + content: '', + }, ]; - }); - assert(importResult.isImportFound); - }); - test('nested namespace package 2', () => { - const files = [ - { - path: combinePaths('/', 'packages1', 'a', 'b', 'c', 'd.py'), - content: 'def f(): pass', - }, - { - path: combinePaths('/', 'packages1', 'a', 'b', 'c', '__init__.py'), - content: '', - }, - { - path: combinePaths('/', 'packages2', 'a', 'b', 'c', '__init__.py'), - content: '', - }, - ]; - - const importResult = getImportResult(files, ['a', 'b', 'c', 'd'], (config) => { - config.defaultExtraPaths = [ - UriEx.file(combinePaths('/', 'packages1')), - UriEx.file(combinePaths('/', 'packages2')), - ]; + const importResult = getImportResult(files, ['a', 'b', 'c', 'd'], (config) => { + config.defaultExtraPaths = [ + UriEx.file(combinePaths('/', 'packages1')), + UriEx.file(combinePaths('/', 'packages2')), + ]; + }); + assert(importResult.isImportFound); }); - assert(importResult.isImportFound); - }); - test('nested namespace package 3', () => { - const files = [ - { - path: combinePaths('/', 'packages1', 'a', 'b', 'c', 'd.py'), - content: 'def f(): pass', - }, - { - path: combinePaths('/', 'packages2', 'a', '__init__.py'), - content: '', - }, - ]; - - const importResult = getImportResult(files, ['a', 'b', 'c', 'd'], (config) => { - config.defaultExtraPaths = [ - UriEx.file(combinePaths('/', 'packages1')), - UriEx.file(combinePaths('/', 'packages2')), + test('nested namespace package 3', () => { + const files = [ + { + path: combinePaths('/', 'packages1', 'a', 'b', 'c', 'd.py'), + content: 'def f(): pass', + }, + { + path: combinePaths('/', 'packages2', 'a', '__init__.py'), + content: '', + }, ]; + + const importResult = getImportResult(files, ['a', 'b', 'c', 'd'], (config) => { + config.defaultExtraPaths = [ + UriEx.file(combinePaths('/', 'packages1')), + UriEx.file(combinePaths('/', 'packages2')), + ]; + }); + assert(!importResult.isImportFound); }); - assert(!importResult.isImportFound); - }); - test('nested namespace package 4', () => { - const files = [ - { - path: combinePaths('/', 'packages1', 'a', 'b', '__init__.py'), - content: '', - }, - { - path: combinePaths('/', 'packages1', 'a', 'b', 'c.py'), - content: 'def f(): pass', - }, - { - path: combinePaths('/', 'packages2', 'a', '__init__.py'), - content: '', - }, - { - path: combinePaths('/', 'packages2', 'a', 'b', '__init__.py'), - content: '', - }, - ]; - - const importResult = getImportResult(files, ['a', 'b', 'c'], (config) => { - config.defaultExtraPaths = [ - UriEx.file(combinePaths('/', 'packages1')), - UriEx.file(combinePaths('/', 'packages2')), + test('nested namespace package 4', () => { + const files = [ + { + path: combinePaths('/', 'packages1', 'a', 'b', '__init__.py'), + content: '', + }, + { + path: combinePaths('/', 'packages1', 'a', 'b', 'c.py'), + content: 'def f(): pass', + }, + { + path: combinePaths('/', 'packages2', 'a', '__init__.py'), + content: '', + }, + { + path: combinePaths('/', 'packages2', 'a', 'b', '__init__.py'), + content: '', + }, ]; - }); - assert(!importResult.isImportFound); - }); - test('default workspace importing side by side file', () => { - const files = [ - { - path: combinePaths('/', 'src', 'a', 'b', 'file1.py'), - content: 'import file2', - }, - { - path: combinePaths('/', 'src', 'a', 'b', 'file2.py'), - content: 'def f(): pass', - }, - ]; - - const importResult = getImportResult(files, ['file2'], (config) => { - config.projectRoot = Uri.defaultWorkspace({ isCaseSensitive: () => true }); + const importResult = getImportResult(files, ['a', 'b', 'c'], (config) => { + config.defaultExtraPaths = [ + UriEx.file(combinePaths('/', 'packages1')), + UriEx.file(combinePaths('/', 'packages2')), + ]; + }); + assert(!importResult.isImportFound); }); - assert(importResult.isImportFound); - }); - test('getModuleNameForImport user file', () => { - const files = [ - { - path: combinePaths('/', 'src', 'file1.py'), - content: '# empty', - }, - ]; - - const moduleImportInfo = getModuleNameForImport(files); + test('default workspace importing side by side file', () => { + const files = [ + { + path: combinePaths('/', 'src', 'a', 'b', 'file1.py'), + content: 'import file2', + }, + { + path: combinePaths('/', 'src', 'a', 'b', 'file2.py'), + content: 'def f(): pass', + }, + ]; - assert.strictEqual(moduleImportInfo.importType, ImportType.Local); - assert(!moduleImportInfo.isThirdPartyPyTypedPresent); - assert(!moduleImportInfo.isLocalTypingsFile); - }); -}); + const importResult = getImportResult(files, ['file2'], (config) => { + config.projectRoot = Uri.defaultWorkspace({ isCaseSensitive: () => true }); + }); + assert(importResult.isImportFound); + }); -if (usingTrueVenv()) { - describe('Import tests that have to run with a venv', () => { - test('venv can find imports', () => { + test('getModuleNameForImport user file', () => { const files = [ { - path: combinePaths('/', 'file1.py'), - content: 'import pytest', + path: combinePaths('/', 'src', 'file1.py'), + content: '# empty', }, ]; - const importResult = getImportResult(files, ['pytest']); - assert(importResult.isImportFound, `Import not found: ${importResult.importFailureInfo?.join('\n')}`); - }); - }); -} + const moduleImportInfo = getModuleNameForImport(files); -function getImportResult( - files: { path: string; content: string }[], - nameParts: string[], - setup?: (c: ConfigOptions) => void -) { - const { importResolver, uri, configOptions } = setupImportResolver(files, setup); - - const importResult = importResolver.resolveImport(uri, configOptions.findExecEnvironment(uri), { - leadingDots: 0, - nameParts: nameParts, - importedSymbols: new Set(), + assert.strictEqual(moduleImportInfo.importType, ImportType.Local); + assert(!moduleImportInfo.isThirdPartyPyTypedPresent); + assert(!moduleImportInfo.isLocalTypingsFile); + }); }); - // Add the config venvpath to the import result so we can output it on failure. - if (!importResult.isImportFound) { - importResult.importFailureInfo = importResult.importFailureInfo ?? []; - importResult.importFailureInfo.push(`venvPath: ${configOptions.venvPath}`); + if (usingTrueVenv()) { + describe('Import tests that have to run with a venv', () => { + test('venv can find imports', () => { + const tempFile = new RealTempFile(); + const files = [ + { + path: combinePaths('/', 'file1.py'), + content: 'import pytest', + }, + ]; + + const importResult = getImportResult(files, ['pytest']); + assert(importResult.isImportFound, `Import not found: ${importResult.importFailureInfo?.join('\n')}`); + + tempFile.dispose(); + }); + }); } - return importResult; -} + function getImportResult( + files: { path: string; content: string }[], + nameParts: string[], + setup?: (c: ConfigOptions) => void + ) { + const { importResolver, uri, configOptions } = setupImportResolver(files, setup); -function getModuleNameForImport(files: { path: string; content: string }[], setup?: (c: ConfigOptions) => void) { - const { importResolver, uri, configOptions } = setupImportResolver(files, setup); - - const moduleImportInfo = importResolver.getModuleNameForImport( - uri, - configOptions.findExecEnvironment(uri), - undefined, - /* detectPyTyped */ true - ); + const importResult = importResolver.resolveImport(uri, configOptions.findExecEnvironment(uri), { + leadingDots: 0, + nameParts: nameParts, + importedSymbols: new Set(), + }); - return moduleImportInfo; -} + // Add the config venvpath to the import result so we can output it on failure. + if (!importResult.isImportFound) { + importResult.importFailureInfo = importResult.importFailureInfo ?? []; + importResult.importFailureInfo.push(`venvPath: ${configOptions.venvPath}`); + } -function setupImportResolver(files: { path: string; content: string }[], setup?: (c: ConfigOptions) => void) { - const defaultHostFactory = (sp: ServiceProvider) => - new TestAccessHost(sp.fs().getModulePath(), [UriEx.file(libraryRoot)]); - const defaultSetup = - setup ?? - ((c) => { - /* empty */ - }); - const defaultSpFactory = (files: { path: string; content: string }[]) => createServiceProviderFromFiles(files); - - // Use environment variables to determine how to create a host and how to modify the config options. - // These are set in the CI to test imports with different options. - let hostFactory: (sp: ServiceProvider) => Host = defaultHostFactory; - let configModifier = defaultSetup; - let spFactory = defaultSpFactory; - - if (process.env.CI_IMPORT_TEST_VENVPATH) { - configModifier = (c: ConfigOptions) => { - defaultSetup(c); - c.venvPath = UriEx.file( - process.env.CI_IMPORT_TEST_VENVPATH!, - /* isCaseSensitive */ true, - /* checkRelative */ true - ); - c.venv = process.env.CI_IMPORT_TEST_VENV; - }; - spFactory = (files: { path: string; content: string }[]) => createServiceProviderWithCombinedFs(files); - } else if (process.env.CI_IMPORT_TEST_PYTHONPATH) { - configModifier = (c: ConfigOptions) => { - defaultSetup(c); - c.pythonPath = UriEx.file( - process.env.CI_IMPORT_TEST_PYTHONPATH!, - /* isCaseSensitive */ true, - /* checkRelative */ true - ); - }; - hostFactory = (sp: ServiceProvider) => new TruePythonTestAccessHost(sp); - spFactory = (files: { path: string; content: string }[]) => createServiceProviderWithCombinedFs(files); + return importResult; } - const sp = spFactory(files); - const configOptions = new ConfigOptions(UriEx.file('/')); - configModifier(configOptions); + function getModuleNameForImport(files: { path: string; content: string }[], setup?: (c: ConfigOptions) => void) { + const { importResolver, uri, configOptions } = setupImportResolver(files, setup); - const file = files.length > 0 ? files[files.length - 1].path : combinePaths('src', 'file.py'); - if (files.length === 0) { - files.push({ - path: file, - content: '# not used', - }); + const moduleImportInfo = importResolver.getModuleNameForImport( + uri, + configOptions.findExecEnvironment(uri), + undefined, + /* detectPyTyped */ true + ); + + return moduleImportInfo; } - const uri = UriEx.file(file); - const importResolver = new ImportResolver(sp, configOptions, hostFactory(sp)); + function setupImportResolver(files: { path: string; content: string }[], setup?: (c: ConfigOptions) => void) { + const defaultHostFactory = (sp: ServiceProvider) => + new TestAccessHost(sp.fs().getModulePath(), [UriEx.file(libraryRoot)]); + const defaultSetup = + setup ?? + ((c) => { + /* empty */ + }); + const defaultSpFactory = (files: { path: string; content: string }[]) => createServiceProviderFromFiles(files); + + // Use environment variables to determine how to create a host and how to modify the config options. + // These are set in the CI to test imports with different options. + let hostFactory: (sp: ServiceProvider) => Host = defaultHostFactory; + let configModifier = defaultSetup; + let spFactory = defaultSpFactory; + + if (process.env.CI_IMPORT_TEST_VENVPATH) { + configModifier = (c: ConfigOptions) => { + defaultSetup(c); + c.venvPath = UriEx.file( + process.env.CI_IMPORT_TEST_VENVPATH!, + /* isCaseSensitive */ true, + /* checkRelative */ true + ); + c.venv = process.env.CI_IMPORT_TEST_VENV; + }; + spFactory = (files: { path: string; content: string }[]) => createServiceProviderWithCombinedFs(files); + } else if (process.env.CI_IMPORT_TEST_PYTHONPATH) { + configModifier = (c: ConfigOptions) => { + defaultSetup(c); + c.pythonPath = UriEx.file( + process.env.CI_IMPORT_TEST_PYTHONPATH!, + /* isCaseSensitive */ true, + /* checkRelative */ true + ); + }; + hostFactory = (sp: ServiceProvider) => { + return new TruePythonTestAccessHost(sp, tempFile); + }; + spFactory = (files: { path: string; content: string }[]) => createServiceProviderWithCombinedFs(files); + } - return { importResolver, uri, configOptions }; -} + const sp = spFactory(files); + const configOptions = new ConfigOptions(UriEx.file('/')); + configModifier(configOptions); + + const file = files.length > 0 ? files[files.length - 1].path : combinePaths('src', 'file.py'); + if (files.length === 0) { + files.push({ + path: file, + content: '# not used', + }); + } + + const uri = UriEx.file(file); + const importResolver = new ImportResolver(sp, configOptions, hostFactory(sp)); + + return { importResolver, uri, configOptions }; + } +}); function createTestFileSystem(files: { path: string; content: string }[]): TestFileSystem { const fs = new TestFileSystem(/* ignoreCase */ false, { cwd: normalizeSlashes('/') }); @@ -896,11 +909,12 @@ function createServiceProviderWithCombinedFs(files: { path: string; content: str } class TruePythonTestAccessHost extends FullAccessHost { - constructor(sp: ServiceProvider) { - // Make sure the service provide in use is using a real file system and real temporary file provider. + constructor(sp: ServiceProvider, tempFile: RealTempFile) { const clone = sp.clone(); - clone.add(ServiceKeys.fs, createFromRealFileSystem(sp.get(ServiceKeys.caseSensitivityDetector))); - clone.add(ServiceKeys.tempFile, new RealTempFile()); + + // Make sure the service provide in use is using a real file system and real temporary file provider. + clone.add(ServiceKeys.tempFile, tempFile); + clone.add(ServiceKeys.fs, createFromRealFileSystem(tempFile)); super(clone); } } diff --git a/packages/pyright-internal/src/tests/sourceFile.test.ts b/packages/pyright-internal/src/tests/sourceFile.test.ts index f3fcdc43048b..d3bbe138fa52 100644 --- a/packages/pyright-internal/src/tests/sourceFile.test.ts +++ b/packages/pyright-internal/src/tests/sourceFile.test.ts @@ -31,6 +31,7 @@ test('Empty', () => { const importResolver = new ImportResolver(sp, configOptions, new FullAccessHost(sp)); sourceFile.parse(configOptions, importResolver); + serviceProvider.dispose(); }); test('Empty Open file', () => { diff --git a/packages/pyright-internal/src/tests/testUtils.ts b/packages/pyright-internal/src/tests/testUtils.ts index e816f4470dc1..d785a22e167d 100644 --- a/packages/pyright-internal/src/tests/testUtils.ts +++ b/packages/pyright-internal/src/tests/testUtils.ts @@ -117,6 +117,8 @@ export function typeAnalyzeSampleFiles( const results = getAnalysisResults(program, fileUris, configOptions); program.dispose(); + serviceProvider.dispose(); + return results; } diff --git a/packages/pyright-internal/src/tests/uri.test.ts b/packages/pyright-internal/src/tests/uri.test.ts index 28a49ea5ea71..6fc106163011 100644 --- a/packages/pyright-internal/src/tests/uri.test.ts +++ b/packages/pyright-internal/src/tests/uri.test.ts @@ -942,6 +942,7 @@ test('Realcase', () => { assert.strictEqual(p, real.getFilePath()); } } + tempFile.dispose(); }); test('Realcase use cwd implicitly', () => { @@ -957,6 +958,7 @@ test('Realcase use cwd implicitly', () => { const fspaths = fsentries.map((entry) => fs.realCasePath(uri.combinePaths(entry)).getFilePath()); assert.deepStrictEqual(lowerCaseDrive(paths), fspaths); + tempFile.dispose(); }); test('Web URIs dont exist', () => { @@ -966,6 +968,7 @@ test('Web URIs dont exist', () => { assert(!fs.existsSync(uri)); const stat = fs.statSync(uri); assert(!stat.isFile()); + tempFile.dispose(); }); test('constant uri test', () => { diff --git a/packages/pyright-internal/src/tests/zipfs.test.ts b/packages/pyright-internal/src/tests/zipfs.test.ts index aafa1ab5f698..758f1b8ab4b7 100644 --- a/packages/pyright-internal/src/tests/zipfs.test.ts +++ b/packages/pyright-internal/src/tests/zipfs.test.ts @@ -91,6 +91,8 @@ function runTests(p: string): void { assert.strictEqual(fs.isInZip(zipRoot.combinePaths('EGG-INFO', 'top_level.txt')), true); assert.strictEqual(fs.isInZip(Uri.file(module.filename, tempFile)), false); }); + + tempFile.dispose(); } describe('zip', () => runTests('./samples/zipfs/basic.zip')); @@ -111,6 +113,8 @@ function runBadTests(p: string): void { test('isInZip', () => { assert.strictEqual(fs.isInZip(zipRoot.combinePaths('EGG-INFO', 'top_level.txt')), false); }); + + tempFile.dispose(); } describe('corrupt zip', () => runBadTests('./samples/zipfs/bad.zip')); diff --git a/packages/pyright-internal/src/workspaceFactory.ts b/packages/pyright-internal/src/workspaceFactory.ts index f7534ff84ad5..d320d2409698 100644 --- a/packages/pyright-internal/src/workspaceFactory.ts +++ b/packages/pyright-internal/src/workspaceFactory.ts @@ -98,6 +98,8 @@ export function renameWorkspace(workspace: Workspace, name: string) { workspace.service.setServiceName(name); } +export type CreateServiceFunction = (name: string, workspaceRoot: Uri, kinds: string[]) => AnalyzerService; + export class WorkspaceFactory { private _defaultWorkspacePath = ''; private _map = new Map(); @@ -105,7 +107,7 @@ export class WorkspaceFactory { constructor( private readonly _console: ConsoleInterface, - private readonly _createService: (name: string, rootPath: Uri, kinds: string[]) => AnalyzerService, + private readonly _createService: CreateServiceFunction, private readonly _onWorkspaceCreated: (workspace: AllWorkspace) => void, private readonly _onWorkspaceRemoved: (workspace: AllWorkspace) => void, private readonly _serviceProvider: ServiceProvider