mirror of
https://github.com/microsoft/pyright.git
synced 2024-08-17 03:40:44 +03:00
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:
parent
8faf8e2da2
commit
4b194b0431
@ -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).
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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.
|
||||
|
@ -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[] = [];
|
||||
|
26
server/src/common/extensibility.ts
Normal file
26
server/src/common/extensibility.ts
Normal 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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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`
|
||||
}
|
||||
);
|
@ -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`
|
||||
}
|
||||
);
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -0,0 +1,2 @@
|
||||
{
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"exclude": ["excluded"]
|
||||
}
|
Loading…
Reference in New Issue
Block a user