added auto venv detection, LS extension points, auto detection on library changes and more tests added. (#574)

auto venv detection makes sure pyright automatically exlude virtual environment from user codes
LS extension points lets plug in such as intellicode to extend what shows up in completion list
auto detection on library let pyright to refresh automatically when new packages are installed/removed.

more test coverage on code fix, new features and etc
This commit is contained in:
Heejae Chang 2020-03-20 12:17:25 -07:00 committed by GitHub
parent 8faf8e2da2
commit 4b194b0431
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 361 additions and 37 deletions

View File

@ -8,7 +8,7 @@ Relative paths specified within the config file are relative to the config file
**include** [array of paths, optional]: Paths of directories or files that should be included. If no paths are specified, pyright defaults to the directory that contains the config file. Paths may contain wildcard characters ** (a directory or multiple levels of directories), * (a sequence of zero or more characters), or ? (a single character). If no include paths are specified, the root path for the workspace is assumed.
**exclude** [array of paths, optional]: Paths of directories or files that should not be included. These override the includes directories, allowing specific subdirectories to be ignored. Note that files in the exclude paths may still be included in the analysis if they are referenced (imported) by source files that are not excluded. Paths may contain wildcard characters ** (a directory or multiple levels of directories), * (a sequence of zero or more characters), or ? (a single character). If no exclude paths are specified, Pyright automatically excludes the following: `**/node_modules`, `**/__pycache__`, `.venv` and `.git`.
**exclude** [array of paths, optional]: Paths of directories or files that should not be included. These override the includes directories, allowing specific subdirectories to be ignored. Note that files in the exclude paths may still be included in the analysis if they are referenced (imported) by source files that are not excluded. Paths may contain wildcard characters ** (a directory or multiple levels of directories), * (a sequence of zero or more characters), or ? (a single character). If no exclude paths are specified, Pyright automatically excludes the following: `**/node_modules`, `**/__pycache__`, `.git` and any virtual environment directories.
**ignore** [array of paths, optional]: Paths of directories or files whose diagnostic output (errors and warnings) should be suppressed even if they are an included file or within the transitive closure of an included file. Paths may contain wildcard characters ** (a directory or multiple levels of directories), * (a sequence of zero or more characters), or ? (a single character).

View File

@ -22,6 +22,7 @@ import { assert } from '../common/debug';
import { Diagnostic } from '../common/diagnostic';
import { FileDiagnostics } from '../common/diagnosticSink';
import { FileEditAction, TextEditAction } from '../common/editAction';
import { LanguageServiceExtension } from '../common/extensibility';
import {
combinePaths,
getDirectoryPath,
@ -95,7 +96,8 @@ export class Program {
constructor(
initialImportResolver: ImportResolver,
initialConfigOptions: ConfigOptions,
console?: ConsoleInterface
console?: ConsoleInterface,
private _extension?: LanguageServiceExtension
) {
this._console = console || new StandardConsole();
this._evaluator = createTypeEvaluator(this._lookUpImport);
@ -840,7 +842,7 @@ export class Program {
this._bindFile(sourceFileInfo, token);
return sourceFileInfo.sourceFile.getCompletionsForPosition(
let completionList = sourceFileInfo.sourceFile.getCompletionsForPosition(
position,
workspacePath,
this._configOptions,
@ -850,6 +852,20 @@ export class Program {
() => this._buildModuleSymbolsMap(sourceFileInfo),
token
);
if (completionList && this._extension?.completionListExtension) {
const tree = sourceFileInfo.sourceFile.getParseResults()?.parseTree;
if (tree) {
completionList = this._extension.completionListExtension.updateCompletionList(
completionList,
tree,
position,
this._configOptions
);
}
}
return completionList;
}
resolveCompletionItem(filePath: string, completionItem: CompletionItem, token: CancellationToken) {

View File

@ -13,6 +13,7 @@ import { ConfigOptions } from '../common/configOptions';
import * as pathConsts from '../common/pathConsts';
import {
combinePaths,
containsPath,
ensureTrailingDirectorySeparator,
getDirectoryPath,
getFileSystemEntries,
@ -21,7 +22,7 @@ import {
} from '../common/pathUtils';
import { VirtualFileSystem } from '../common/vfs';
const cachedSearchPaths = new Map<string, string[]>();
const cachedSearchPaths = new Map<string, PythonPathResult>();
export function getTypeShedFallbackPath(fs: VirtualFileSystem) {
let moduleDirectory = fs.getModulePath();
@ -54,7 +55,9 @@ export function findPythonSearchPaths(
fs: VirtualFileSystem,
configOptions: ConfigOptions,
venv: string | undefined,
importFailureInfo: string[]
importFailureInfo: string[],
includeWatchPathsOnly?: boolean | undefined,
workspaceRoot?: string | undefined
): string[] | undefined {
importFailureInfo.push('Finding python search paths');
@ -114,14 +117,28 @@ export function findPythonSearchPaths(
}
// Fall back on the python interpreter.
return getPythonPathFromPythonInterpreter(fs, configOptions.pythonPath, importFailureInfo);
const pathResult = getPythonPathFromPythonInterpreter(fs, configOptions.pythonPath, importFailureInfo);
if (includeWatchPathsOnly && workspaceRoot) {
const paths = pathResult.paths.filter(
p => !containsPath(workspaceRoot, p, true) || containsPath(pathResult.prefix, p, true)
);
return paths;
}
return pathResult.paths;
}
interface PythonPathResult {
paths: string[];
prefix: string;
}
export function getPythonPathFromPythonInterpreter(
fs: VirtualFileSystem,
interpreterPath: string | undefined,
importFailureInfo: string[]
): string[] {
): PythonPathResult {
const searchKey = interpreterPath || '';
// If we've seen this request before, return the cached results.
@ -130,7 +147,10 @@ export function getPythonPathFromPythonInterpreter(
return cachedPath;
}
let pythonPaths: string[] = [];
const result: PythonPathResult = {
paths: [],
prefix: ''
};
try {
// Set the working directory to a known location within
@ -141,7 +161,10 @@ export function getPythonPathFromPythonInterpreter(
fs.chdir(moduleDirectory);
}
const commandLineArgs: string[] = ['-c', 'import sys, json; json.dump(sys.path, sys.stdout)'];
const commandLineArgs: string[] = [
'-c',
'import sys, json; json.dump(dict(path=sys.path, prefix=sys.prefix), sys.stdout)'
];
let execOutput: string;
if (interpreterPath) {
@ -154,22 +177,24 @@ export function getPythonPathFromPythonInterpreter(
// Parse the execOutput. It should be a JSON-encoded array of paths.
try {
const execSplit: string[] = JSON.parse(execOutput);
for (let execSplitEntry of execSplit) {
const execSplit = JSON.parse(execOutput);
for (let execSplitEntry of execSplit.path) {
execSplitEntry = execSplitEntry.trim();
if (execSplitEntry) {
const normalizedPath = normalizePath(execSplitEntry);
// Make sure the path exists and is a directory. We don't currently
// support zip files and other formats.
if (fs.existsSync(normalizedPath) && isDirectory(fs, normalizedPath)) {
pythonPaths.push(normalizedPath);
result.paths.push(normalizedPath);
} else {
importFailureInfo.push(`Skipping '${normalizedPath}' because it is not a valid directory`);
}
}
}
if (pythonPaths.length === 0) {
result.prefix = execSplit.prefix;
if (result.paths.length === 0) {
importFailureInfo.push(`Found no valid directories`);
}
} catch (err) {
@ -177,13 +202,14 @@ export function getPythonPathFromPythonInterpreter(
throw err;
}
} catch {
pythonPaths = [];
result.paths = [];
result.prefix = '';
}
cachedSearchPaths.set(searchKey, pythonPaths);
importFailureInfo.push(`Received ${pythonPaths.length} paths from interpreter`);
pythonPaths.forEach(path => {
cachedSearchPaths.set(searchKey, result);
importFailureInfo.push(`Received ${result.paths.length} paths from interpreter`);
result.paths.forEach(path => {
importFailureInfo.push(` ${path}`);
});
return pythonPaths;
return result;
}

View File

@ -23,6 +23,7 @@ import { assert } from '../common/debug';
import { Diagnostic } from '../common/diagnostic';
import { FileDiagnostics } from '../common/diagnosticSink';
import { FileEditAction, TextEditAction } from '../common/editAction';
import { LanguageServiceExtension } from '../common/extensibility';
import {
combinePaths,
FileSpec,
@ -73,10 +74,13 @@ export class AnalyzerService {
private _console: ConsoleInterface;
private _sourceFileWatcher: FileWatcher | undefined;
private _reloadConfigTimer: any;
private _libraryReanalysisTimer: any;
private _configFilePath: string | undefined;
private _configFileWatcher: FileWatcher | undefined;
private _libraryFileWatcher: FileWatcher | undefined;
private _onCompletionCallback: AnalysisCompleteCallback | undefined;
private _watchForSourceChanges = false;
private _watchForLibraryChanges = false;
private _verboseOutput = false;
private _maxAnalysisTime?: MaxAnalysisTime;
private _analyzeTimer: any;
@ -88,14 +92,15 @@ export class AnalyzerService {
fs: VirtualFileSystem,
console?: ConsoleInterface,
importResolverFactory?: ImportResolverFactory,
configOptions?: ConfigOptions
configOptions?: ConfigOptions,
extension?: LanguageServiceExtension
) {
this._instanceName = instanceName;
this._console = console || new StandardConsole();
this._configOptions = configOptions ?? new ConfigOptions(process.cwd());
this._importResolverFactory = importResolverFactory || AnalyzerService.createImportResolver;
this._importResolver = this._importResolverFactory(fs, this._configOptions);
this._program = new Program(this._importResolver, this._configOptions, this._console);
this._program = new Program(this._importResolver, this._configOptions, this._console, extension);
this._executionRootPath = '';
this._typeStubTargetImportName = undefined;
}
@ -113,8 +118,10 @@ export class AnalyzerService {
dispose() {
this._removeSourceFileWatchers();
this._removeConfigFileWatcher();
this._removeLibraryFileWatcher();
this._clearReloadConfigTimer();
this._clearReanalysisTimer();
this._clearLibraryReanalysisTimer();
}
static createImportResolver(fs: VirtualFileSystem, options: ConfigOptions): ImportResolver {
@ -130,7 +137,8 @@ export class AnalyzerService {
}
setOptions(commandLineOptions: CommandLineOptions): void {
this._watchForSourceChanges = !!commandLineOptions.watch;
this._watchForSourceChanges = !!commandLineOptions.watchForSourceChanges;
this._watchForLibraryChanges = !!commandLineOptions.watchForLibraryChanges;
this._verboseOutput = !!commandLineOptions.verboseOutput;
this._configOptions = this._getConfigOptions(commandLineOptions);
this._program.setConfigOptions(this._configOptions);
@ -324,7 +332,7 @@ export class AnalyzerService {
}
const configOptions = new ConfigOptions(projectRoot);
const defaultExcludes = ['**/node_modules', '**/__pycache__', '.venv', '.git'];
const defaultExcludes = ['**/node_modules', '**/__pycache__', '.git'];
if (commandLineOptions.fileSpecs.length > 0) {
commandLineOptions.fileSpecs.forEach(fileSpec => {
@ -368,6 +376,10 @@ export class AnalyzerService {
this._console.log(`Auto-excluding ${exclude}`);
configOptions.exclude.push(getFileSpec(configFileDir, exclude));
});
if (configOptions.autoExcludeVenv === undefined) {
configOptions.autoExcludeVenv = true;
}
}
// If the user has defined execution environments, then we ignore
@ -377,10 +389,13 @@ export class AnalyzerService {
}
}
this._updateConfigFileWatcher();
this._updateLibraryFileWatcher();
} else {
if (commandLineOptions.autoSearchPaths) {
configOptions.addExecEnvironmentForAutoSearchPaths(this._fs);
}
configOptions.autoExcludeVenv = true;
}
const reportDuplicateSetting = (settingName: string) => {
@ -470,7 +485,7 @@ export class AnalyzerService {
this._fs,
configOptions.pythonPath,
importFailureInfo
);
).paths;
if (pythonPaths.length === 0) {
if (configOptions.verboseOutput) {
this._console.log(`No search paths found for configured python interpreter.`);
@ -754,9 +769,17 @@ export class AnalyzerService {
private _matchFiles(include: FileSpec[], exclude: FileSpec[]): string[] {
const includeFileRegex = /\.pyi?$/;
const envMarkers = [['bin', 'activate'], ['Scripts', 'activate'], ['pyvenv.cfg']];
const results: string[] = [];
const visitDirectory = (absolutePath: string, includeRegExp: RegExp) => {
if (this._configOptions.autoExcludeVenv) {
if (envMarkers.some(f => this._fs.existsSync(combinePaths(absolutePath, ...f)))) {
this._console.log(`Auto-excluding ${absolutePath}`);
return;
}
}
const { files, directories } = getFileSystemEntries(this._fs, absolutePath);
for (const file of files) {
@ -854,6 +877,80 @@ export class AnalyzerService {
}
}
private _removeLibraryFileWatcher() {
if (this._libraryFileWatcher) {
this._libraryFileWatcher.close();
this._libraryFileWatcher = undefined;
}
}
private _updateLibraryFileWatcher() {
this._removeLibraryFileWatcher();
// Invalidate import resolver because it could have cached
// imports that are no longer valid because library has
// been installed or uninstalled.
this._importResolver.invalidateCache();
if (!this._watchForLibraryChanges) {
return;
}
// watch the library paths for package install/uninstall
const importFailureInfo: string[] = [];
const watchList = findPythonSearchPaths(
this._fs,
this._configOptions,
undefined,
importFailureInfo,
true,
this._executionRootPath
);
if (watchList && watchList.length > 0) {
try {
if (this._verboseOutput) {
this._console.log(`Adding fs watcher for library directories:\n ${watchList.join('\n')}`);
}
// Use fs.watch instead of chokidar, to avoid issue where chokidar locks up files
// when the virtual environment is located under the workspace folder (which breaks pip uninstall)
// Not sure why that happens with chokidar, if the watch path is outside the workspace it is fine.
this._libraryFileWatcher = this._fs.createLowLevelFileSystemWatcher(watchList, true, (event, path) => {
if (this._verboseOutput) {
this._console.log(`Received fs event '${event}' for path '${path}'`);
}
this._scheduleLibraryAnalysis();
});
} catch {
this._console.log(`Exception caught when installing fs watcher for:\n ${watchList.join('\n')}`);
}
}
}
private _clearLibraryReanalysisTimer() {
if (this._libraryReanalysisTimer) {
clearTimeout(this._libraryReanalysisTimer);
this._libraryReanalysisTimer = undefined;
}
}
private _scheduleLibraryAnalysis() {
this._clearLibraryReanalysisTimer();
// Wait for a little while, since library changes
// tend to happen in big batches when packages
// are installed or uninstalled.
this._libraryReanalysisTimer = setTimeout(() => {
this._clearLibraryReanalysisTimer();
// Invalidate import resolver, mark all files dirty unconditionally and reanalyze
this.invalidateAndForceReanalysis();
this._scheduleReanalysis(false);
}, 100);
}
private _removeConfigFileWatcher() {
if (this._configFileWatcher) {
this._configFileWatcher.close();
@ -914,6 +1011,7 @@ export class AnalyzerService {
this._importResolver = this._importResolverFactory(this._fs, this._configOptions);
this._program.setImportResolver(this._importResolver);
this._updateLibraryFileWatcher();
this._updateSourceFileWatchers();
this._updateTrackedFileList(true);
this._scheduleReanalysis(false);

View File

@ -21,8 +21,11 @@ export class CommandLineOptions {
// are included.
fileSpecs: string[] = [];
// Watch for changes?
watch?: boolean;
// Watch for changes in workspace source files.
watchForSourceChanges?: boolean;
// Watch for changes in environment library/search paths.
watchForLibraryChanges?: boolean;
// Path of config file. This option cannot be combined with
// file specs.

View File

@ -342,6 +342,14 @@ export class ConfigOptions {
// within those directories are included.
exclude: FileSpec[] = [];
// Automatically detect virtual environment folders and exclude them.
// This property is for internal use and not exposed externally
// as a config setting.
// It is used to store whether the user has specified directories in
// the exclude setting, which is later modified to include a default set.
// This setting is true when user has not specified any exclude.
autoExcludeVenv?: boolean;
// A list of file specs whose errors and warnings should be ignored even
// if they are included in the transitive closure of included files.
ignore: FileSpec[] = [];

View File

@ -0,0 +1,26 @@
/*
* completions.ts
*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
* Defines language service completion list extensibility.
*/
import { CompletionList, Position } from 'vscode-languageserver';
import { ModuleNode } from '../parser/parseNodes';
import { ConfigOptions } from './configOptions';
export interface LanguageServiceExtension {
completionListExtension: CompletionListExtension;
}
export interface CompletionListExtension {
updateCompletionList(
sourceList: CompletionList,
ast: ModuleNode,
position: Position,
options: ConfigOptions
): CompletionList;
}

View File

@ -50,6 +50,11 @@ export interface VirtualFileSystem {
unlinkSync(path: string): void;
realpathSync(path: string): string;
getModulePath(): string;
createLowLevelFileSystemWatcher(
paths: string[],
recursive?: boolean,
listener?: (event: string, filename: string) => void
): FileWatcher;
createFileSystemWatcher(paths: string[], event: 'all', listener: Listener): FileWatcher;
}
@ -64,6 +69,14 @@ export function createFromRealFileSystem(console?: ConsoleInterface): VirtualFil
const _isMacintosh = process.platform === 'darwin';
const _isLinux = process.platform === 'linux';
class LowLevelWatcher implements FileWatcher {
constructor(private paths: string[]) {}
close(): void {
this.paths.forEach(p => fs.unwatchFile(p));
}
}
class FileSystem implements VirtualFileSystem {
constructor(private _console: ConsoleInterface) {}
@ -105,6 +118,18 @@ class FileSystem implements VirtualFileSystem {
return (global as any).__rootDirectory;
}
createLowLevelFileSystemWatcher(
paths: string[],
recursive?: boolean,
listener?: (event: string, filename: string) => void
): FileWatcher {
paths.forEach(p => {
fs.watch(p, { recursive: recursive }, listener);
});
return new LowLevelWatcher(paths);
}
createFileSystemWatcher(paths: string[], event: 'all', listener: Listener): FileWatcher {
return this._createBaseFileSystemWatcher(paths).on(event, listener);
}

View File

@ -38,6 +38,7 @@ import { AnalysisResults, AnalyzerService } from './analyzer/service';
import { ConfigOptions } from './common/configOptions';
import { ConsoleInterface } from './common/console';
import { Diagnostic as AnalyzerDiagnostic, DiagnosticCategory } from './common/diagnostic';
import { LanguageServiceExtension } from './common/extensibility';
import { convertPathToUri, convertUriToPath } from './common/pathUtils';
import { Position } from './common/textRange';
import { createFromRealFileSystem, VirtualFileSystem } from './common/vfs';
@ -54,6 +55,7 @@ export interface ServerSettings {
useLibraryCodeForTypes?: boolean;
disableLanguageServices?: boolean;
autoSearchPaths?: boolean;
watchForLibraryChanges?: boolean;
}
export interface WorkspaceServiceInstance {
@ -96,7 +98,7 @@ export abstract class LanguageServerBase implements LanguageServerInterface {
// File system abstraction.
fs: VirtualFileSystem;
constructor(private _productName: string, rootDirectory: string) {
constructor(private _productName: string, rootDirectory: string, private _extension?: LanguageServiceExtension) {
this._connection.console.log(`${_productName} language server starting`);
// virtual file system to be used. initialized to real file system by default. but can't be overwritten
this.fs = createFromRealFileSystem(this._connection.console);
@ -149,7 +151,14 @@ export abstract class LanguageServerBase implements LanguageServerInterface {
// program within a workspace.
createAnalyzerService(name: string): AnalyzerService {
this._connection.console.log(`Starting service instance "${name}"`);
const service = new AnalyzerService(name, this.fs, this._connection.console, this.createImportResolver);
const service = new AnalyzerService(
name,
this.fs,
this._connection.console,
this.createImportResolver.bind(this),
undefined,
this._extension
);
// Don't allow the analysis engine to go too long without
// reporting results. This will keep it responsive.

View File

@ -39,12 +39,13 @@ function _getCommandLineOptions(
const commandLineOptions = new CommandLineOptions(workspaceRootPath, true);
commandLineOptions.checkOnlyOpenFiles = serverSettings.openFilesOnly;
commandLineOptions.useLibraryCodeForTypes = serverSettings.useLibraryCodeForTypes;
commandLineOptions.watchForLibraryChanges = serverSettings.watchForLibraryChanges;
// Disable watching of source files in the VS Code extension if we're
// analyzing only open files. The file system watcher code has caused
// lots of problems across multiple platforms. It provides little or
// no benefit when we're in "openFilesOnly" mode.
commandLineOptions.watch = !commandLineOptions.checkOnlyOpenFiles;
commandLineOptions.watchForSourceChanges = !commandLineOptions.checkOnlyOpenFiles;
if (serverSettings.venvPath) {
commandLineOptions.venvPath = combinePaths(

View File

@ -162,7 +162,7 @@ function processArgs() {
options.useLibraryCodeForTypes = !!args.lib;
const watch = args.watch !== undefined;
options.watch = watch;
options.watchForSourceChanges = watch;
const output = args.outputjson ? new NullConsole() : undefined;
const service = new AnalyzerService('<default>', createFromRealFileSystem(output), output);

View File

@ -12,7 +12,7 @@ import { convertUriToPath, getDirectoryPath, normalizeSlashes } from './common/p
import { LanguageServerBase, ServerSettings, WorkspaceServiceInstance } from './languageServerBase';
import { CodeActionProvider } from './languageService/codeActionProvider';
class Server extends LanguageServerBase {
class PyrightServer extends LanguageServerBase {
private _controller: CommandController;
constructor() {
@ -73,4 +73,4 @@ class Server extends LanguageServerBase {
}
}
export const server = new Server();
export const server = new PyrightServer();

View File

@ -13,7 +13,7 @@ import { AnalyzerService } from '../analyzer/service';
import { CommandLineOptions } from '../common/commandLineOptions';
import { ConfigOptions, ExecutionEnvironment } from '../common/configOptions';
import { NullConsole } from '../common/console';
import { combinePaths, normalizePath, normalizeSlashes } from '../common/pathUtils';
import { combinePaths, getBaseFileName, normalizePath, normalizeSlashes } from '../common/pathUtils';
import { createFromRealFileSystem } from '../common/vfs';
test('FindFilesWithConfigFile', () => {
@ -40,6 +40,42 @@ test('FindFilesWithConfigFile', () => {
assert.equal(fileList.length, 2);
});
test('FindFilesVirtualEnvAutoDetectExclude', () => {
const cwd = normalizePath(combinePaths(process.cwd(), '../server'));
const service = new AnalyzerService('<default>', createFromRealFileSystem(), new NullConsole());
const commandLineOptions = new CommandLineOptions(cwd, 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 => getBaseFileName(p)).sort();
assert.deepEqual(fileNames, ['sample1.py', 'sample2.py', 'sample3.py']);
});
test('FindFilesVirtualEnvAutoDetectInclude', () => {
const cwd = normalizePath(combinePaths(process.cwd(), '../server'));
const service = new AnalyzerService('<default>', createFromRealFileSystem(), new NullConsole());
const commandLineOptions = new CommandLineOptions(cwd, 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 => getBaseFileName(p)).sort();
assert.deepEqual(fileNames, ['library1.py', 'sample1.py', 'sample2.py', 'sample3.py']);
});
test('FileSpecNotAnArray', () => {
const cwd = normalizePath(combinePaths(process.cwd(), '../server'));
const nullConsole = new NullConsole();

View File

@ -0,0 +1,20 @@
/// <reference path="fourslash.ts" />
// @asynctest: true
// @filename: quickActionOrganizeImportTest1.py
//// import time
//// import os
//// import sys
helper.verifyCommand(
{
title: 'Quick action order imports 1',
command: Consts.Commands.orderImports,
arguments: ['quickActionOrganizeImportTest1.py']
},
{
['quickActionOrganizeImportTest1.py']: `import os
import sys
import time`
}
);

View File

@ -0,0 +1,24 @@
/// <reference path="fourslash.ts" />
// @asynctest: true
// @filename: quickActionOrganizeImportTest2.py
//// import time
//// import sys
//// a = 100
//// print(a)
//// import math
//// import os
helper.verifyCommand(
{
title: 'Quick action order imports',
command: Consts.Commands.orderImports,
arguments: ['quickActionOrganizeImportTest2.py']
},
{
['quickActionOrganizeImportTest2.py']: `import math
import os
import sys
import time`
}
);

View File

@ -9,7 +9,7 @@
import * as assert from 'assert';
import Char from 'typescript-char';
import { CancellationToken, Command, Diagnostic, MarkupContent } from 'vscode-languageserver';
import { CancellationToken, Command, Diagnostic, MarkupContent, TextEdit } from 'vscode-languageserver';
import { ImportResolver, ImportResolverFactory } from '../../../analyzer/importResolver';
import { Program } from '../../../analyzer/program';
@ -328,8 +328,7 @@ export class TestState {
fileToOpen.fileName = normalizeSlashes(fileToOpen.fileName);
this.activeFile = fileToOpen;
// Let the host know that this file is now open
// this.languageServiceAdapterHost.openFile(fileToOpen.fileName, content);
this.program.setFileOpened(this.activeFile.fileName, 1, fileToOpen.content);
}
printCurrentFileState(showWhitespace: boolean, makeCaretVisible: boolean) {
@ -561,8 +560,24 @@ export class TestState {
this._analyze();
const controller = new CommandController(new TestLanguageService(this.workspace, this.console, this.fs));
await controller.execute({ command: command.command, arguments: command.arguments }, CancellationToken.None);
await this._verifyFiles(files);
const commandResult = await controller.execute(
{ command: command.command, arguments: command.arguments },
CancellationToken.None
);
if (command.command === 'pyright.createtypestub') {
await this._verifyFiles(files);
} else if (command.command === 'pyright.organizeimports') {
//organize imports command can only be used on 1 file at a time, so there is no looping over "commandResult" or "files"
const actualText = (commandResult as TextEdit[])[0].newText;
const expectedText: string = Object.values(files)[0];
if (actualText != expectedText) {
this._raiseError(
`doesn't contain expected result: ${stringify(expectedText)}, actual: ${stringify(actualText)}`
);
}
}
this.markTestDone();
}

View File

@ -313,6 +313,18 @@ export class FileSystem {
};
}
createLowLevelFileSystemWatcher(
paths: string[],
recursive?: boolean,
listener?: (event: string, filename: string) => void
): FileWatcher {
return {
close: () => {
/* left empty */
}
};
}
getModulePath(): string {
return MODULE_PATH;
}

View File

@ -0,0 +1,3 @@
{
"exclude": ["excluded"]
}