Fix windows bug, add PEP 604 printing, fix unused variable action, add stubPath and diagnosticMode (#673)

This commit is contained in:
Jake Bailey 2020-05-15 13:01:55 -07:00 committed by GitHub
parent 37c3abf734
commit 1d56d6765f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 241 additions and 95 deletions

View File

@ -81,6 +81,25 @@
"description": "Automatically add common search paths like 'src'?",
"scope": "resource"
},
"python.analysis.stubPath": {
"type": "string",
"default": "",
"description": "Path to directory containing custom type stub files.",
"scope": "resource"
},
"python.analysis.diagnosticMode": {
"type": "string",
"default": "openFilesOnly",
"enum": [
"openFilesOnly",
"workspace"
],
"enumDescriptions": [
"Analyzes and reports errors on only open files.",
"Analyzes and reports errors on all files in the workspace."
],
"scope": "resource"
},
"python.pythonPath": {
"type": "string",
"default": "python",
@ -105,12 +124,6 @@
"description": "Disables the “Organize Imports” command.",
"scope": "resource"
},
"pyright.openFilesOnly": {
"type": "boolean",
"default": true,
"description": "Report errors only for currently-open files.",
"scope": "resource"
},
"pyright.useLibraryCodeForTypes": {
"type": "boolean",
"default": false,

View File

@ -82,8 +82,8 @@
"default": "",
"pattern": "^(.*)$"
},
"typingsPath": {
"$id": "#/properties/typingsPath",
"stubPath": {
"$id": "#/properties/stubPath",
"type": "string",
"title": "Path to directory containing custom type stub files",
"default": "",

View File

@ -16,7 +16,7 @@ Relative paths specified within the config file are relative to the config file
**typeshedPath** [path, optional]: Path to a directory that contains typeshed type stub files. Pyright ships with an internal copy of some typeshed type stubs (those that cover the Python stdlib packages). If you want to use a full copy of the typeshed type stubs (including those for third-party packages), you can clone the [typeshed github repo](https://github.com/python/typeshed) to a local directory and reference the location with this path.
**typingsPath** [path, optional]: Path to a directory that contains custom type stubs. Each package's type stub file(s) are expected to be in its own subdirectory. The default value of this setting is "./typings".
**stubPath** [path, optional]: Path to a directory that contains custom type stubs. Each package's type stub file(s) are expected to be in its own subdirectory. The default value of this setting is "./typings". (typingsPath is now deprecated)
**venvPath** [path, optional]: Path to a directory containing one or more subdirectories, each of which contains a virtual environment. Each execution environment (see below for details) can refer to a different virtual environment. When used in conjunction with a **venv** setting (see below), pyright will search for imports in the virtual environments site-packages directory rather than the paths specified in PYTHONPATH.
@ -165,7 +165,7 @@ The following is an example of a pyright config file:
"src/oldstuff"
],
"typingsPath": "src/typestubs",
"stubPath": "src/typestubs",
"venvPath": "/home/foo/.venvs",
"reportTypeshedErrors": false,

View File

@ -6,8 +6,6 @@ The Pyright VS Code extension honors the following settings.
**pyright.disableOrganizeImports** [boolean]: Disables the “Organize Imports” command. This is useful if you are using another extension that provides similar functionality and you dont want the two extensions to fight each other.
**pyright.openFilesOnly** [boolean]: Determines whether pyright analyzes (and reports errors for) all files in the workspace, as indicated by the config file. If this option is set to true, pyright analyzes only open files.
**pyright.typeCheckingMode** ["off", "basic", "strict"]: Determines the default type-checking level used by pyright. This can be overridden in the configuration file.
**pyright.useLibraryCodeForTypes** [boolean]: Determines whether pyright reads, parses and analyzes library code to extract type information in the absence of type stub files. This can add significant overhead and may result in poor-quality type information. The default value for this option is false.
@ -22,4 +20,7 @@ The Pyright VS Code extension honors the following settings.
**python.venvPath** [path]: Path to folder with subdirectories that contain virtual environments.
**python.analysis.stubPath** [path]: Path to directory containing custom type stub files.
**python.analysis.diagnosticMode** ["openFilesOnly", "workspace"]: Determines whether pyright analyzes (and reports errors for) all files in the workspace, as indicated by the config file. If this option is set to "openFilesOnly", pyright analyzes only open files.

View File

@ -19,7 +19,7 @@ If youre serious about static type checking for your Python source base, it
## Generating Type Stubs
If you use only a few classes, methods or functions within a library, writing a type stub file by hand is feasible. For large libraries, this can become tedious and error-prone. Pyright can generate “draft” versions of type stub files for you.
To generate a type stub file from within VS Code, enable the reportMissingTypeStubs” setting in your pyrightconfig.json file or by adding a comment `# pyright: reportMissingTypeStubs=true` to individual source files. Make sure you have the target library installed in the python environment that pyright is configured to use for import resolution. Optionally specify a “typingsPath” in your pyrightconfig.json file. This is where pyright will generate your type stub files. By default, the typingsPath is set to "./typings".
To generate a type stub file from within VS Code, enable the reportMissingTypeStubs” setting in your pyrightconfig.json file or by adding a comment `# pyright: reportMissingTypeStubs=true` to individual source files. Make sure you have the target library installed in the python environment that pyright is configured to use for import resolution. Optionally specify a “stubPath” in your pyrightconfig.json file. This is where pyright will generate your type stub files. By default, the stubPath is set to "./typings".
### Generating Type Stubs in VS Code
If “reportMissingTypeStubs” is enabled, pyright will highlight any imports that have no type stub. Hover over the error message, and you will see a “Quick Fix” link. Clicking on this link will reveal a popup menu item titled “Create Type Stub For XXX”. The example below shows a missing typestub for the `django` library.

View File

@ -140,15 +140,15 @@ export class BackgroundAnalysisProgram {
async writeTypeStub(
targetImportPath: string,
targetIsSingleFile: boolean,
typingsPath: string,
stubPath: string,
token: CancellationToken
): Promise<any> {
if (this._backgroundAnalysis) {
return this._backgroundAnalysis.writeTypeStub(targetImportPath, targetIsSingleFile, typingsPath, token);
return this._backgroundAnalysis.writeTypeStub(targetImportPath, targetIsSingleFile, stubPath, token);
}
analyzeProgram(this._program, undefined, this._configOptions, this._onAnalysisCompletion, this._console, token);
return this._program.writeTypeStub(targetImportPath, targetIsSingleFile, typingsPath, token);
return this._program.writeTypeStub(targetImportPath, targetIsSingleFile, stubPath, token);
}
invalidateAndForceReanalysis() {

View File

@ -12,6 +12,7 @@
* cannot (or should not be) performed lazily.
*/
import { Commands } from '../commands/commands';
import { DiagnosticLevel } from '../common/configOptions';
import { assert } from '../common/debug';
import { Diagnostic, DiagnosticAddendum } from '../common/diagnostic';
@ -963,7 +964,8 @@ export class Checker extends ParseTreeWalker {
TextRange.extend(textRange, nameParts[nameParts.length - 1]);
this._fileInfo.diagnosticSink.addUnusedCodeWithTextRange(
`"${multipartName}" is not accessed`,
textRange
textRange,
{ action: Commands.unusedImport }
);
this._evaluator.addDiagnosticForTextRange(
@ -1033,7 +1035,12 @@ export class Checker extends ParseTreeWalker {
}
if (nameNode && rule !== undefined && message) {
this._fileInfo.diagnosticSink.addUnusedCodeWithTextRange(`"${nameNode.value}" is not accessed`, nameNode);
const action = rule === DiagnosticRule.reportUnusedImport ? { action: Commands.unusedImport } : undefined;
this._fileInfo.diagnosticSink.addUnusedCodeWithTextRange(
`"${nameNode.value}" is not accessed`,
nameNode,
action
);
this._evaluator.addDiagnostic(diagnosticLevel, rule, message, nameNode);
}
}

View File

@ -193,11 +193,11 @@ export class ImportResolver {
}
if (allowPyi) {
// Check for a typings file.
if (this._configOptions.typingsPath) {
importFailureInfo.push(`Looking in typingsPath '${this._configOptions.typingsPath}'`);
// Check for a stub file.
if (this._configOptions.stubPath) {
importFailureInfo.push(`Looking in stubPath '${this._configOptions.stubPath}'`);
const typingsImport = this.resolveAbsoluteImport(
this._configOptions.typingsPath,
this._configOptions.stubPath,
moduleDescriptor,
importName,
importFailureInfo
@ -327,9 +327,9 @@ export class ImportResolver {
}
// Check for a typings file.
if (this._configOptions.typingsPath) {
if (this._configOptions.stubPath) {
this._getCompletionSuggestionsAbsolute(
this._configOptions.typingsPath,
this._configOptions.stubPath,
moduleDescriptor,
suggestions,
similarityLimit
@ -383,8 +383,8 @@ export class ImportResolver {
}
// Check for a typings file.
if (this._configOptions.typingsPath) {
const candidateModuleName = this._getModuleNameFromPath(this._configOptions.typingsPath, filePath);
if (this._configOptions.stubPath) {
const candidateModuleName = this._getModuleNameFromPath(this._configOptions.stubPath, filePath);
// Does this candidate look better than the previous best module name?
// We'll always try to use the shortest version.

View File

@ -8,6 +8,9 @@
* import statements in a python source file.
*/
import { CancellationToken } from 'vscode-languageserver';
import { throwIfCancellationRequested } from '../common/cancellationUtils';
import { TextEditAction } from '../common/editAction';
import { convertOffsetToPosition } from '../common/positionUtils';
import { Position } from '../common/textRange';
@ -378,8 +381,10 @@ function _formatModuleName(node: ModuleNameNode): string {
return moduleName;
}
export function getContainingImportStatement(node: ParseNode | undefined) {
export function getContainingImportStatement(node: ParseNode | undefined, token: CancellationToken) {
while (node) {
throwIfCancellationRequested(token);
if (node.nodeType === ParseNodeType.Import || node.nodeType === ParseNodeType.ImportFrom) {
break;
}

View File

@ -109,16 +109,16 @@ export class Program {
private _extension?: LanguageServiceExtension
) {
this._console = console || new StandardConsole();
this._evaluator = createTypeEvaluator(this._lookUpImport, Program._getPrintTypeFlags(initialConfigOptions));
this._importResolver = initialImportResolver;
this._configOptions = initialConfigOptions;
this._createNewEvaluator();
}
setConfigOptions(configOptions: ConfigOptions) {
this._configOptions = configOptions;
// Create a new evaluator with the updated config options.
this._evaluator = createTypeEvaluator(this._lookUpImport, Program._getPrintTypeFlags(this._configOptions));
this._createNewEvaluator();
}
setImportResolver(importResolver: ImportResolver) {
@ -432,12 +432,7 @@ export class Program {
}
}
writeTypeStub(
targetImportPath: string,
targetIsSingleFile: boolean,
typingsPath: string,
token: CancellationToken
) {
writeTypeStub(targetImportPath: string, targetIsSingleFile: boolean, stubPath: string, token: CancellationToken) {
for (const sourceFileInfo of this._sourceFileList) {
throwIfCancellationRequested(token);
@ -447,7 +442,7 @@ export class Program {
// not any files that the target module happened to import.
const relativePath = getRelativePath(filePath, targetImportPath);
if (relativePath !== undefined) {
let typeStubPath = normalizePath(combinePaths(typingsPath, relativePath));
let typeStubPath = normalizePath(combinePaths(stubPath, relativePath));
// If the target is a single file implementation, as opposed to
// a package in a directory, transform the name of the type stub
@ -461,7 +456,7 @@ export class Program {
const typeStubDir = getDirectoryPath(typeStubPath);
try {
makeDirectories(this._fs, typeStubDir, typingsPath);
makeDirectories(this._fs, typeStubDir, stubPath);
} catch (e) {
const errMsg = `Could not create directory for '${typeStubDir}'`;
throw new Error(errMsg);
@ -488,6 +483,10 @@ export class Program {
flags |= PrintTypeFlags.OmitTypeArgumentsIfAny;
}
if (configOptions.diagnosticRuleSet.pep604Printing) {
flags |= PrintTypeFlags.PEP604;
}
return flags;
}

View File

@ -458,9 +458,17 @@ export class AnalyzerService {
configOptions.checkOnlyOpenFiles = !!commandLineOptions.checkOnlyOpenFiles;
configOptions.useLibraryCodeForTypes = !!commandLineOptions.useLibraryCodeForTypes;
// If there was no typings path specified, use a default path.
if (configOptions.typingsPath === undefined) {
configOptions.typingsPath = normalizePath(combinePaths(configOptions.projectRoot, 'typings'));
// If there was no stub path specified, use a default path.
if (commandLineOptions.stubPath) {
if (!configOptions.stubPath) {
configOptions.stubPath = commandLineOptions.stubPath;
} else {
reportDuplicateSetting('stubPath');
}
} else {
if (!configOptions.stubPath) {
configOptions.stubPath = normalizePath(combinePaths(configOptions.projectRoot, 'typings'));
}
}
// Do some sanity checks on the specified settings and report missing
@ -544,9 +552,9 @@ export class AnalyzerService {
}
}
if (configOptions.typingsPath) {
if (!this._fs.existsSync(configOptions.typingsPath) || !isDirectory(this._fs, configOptions.typingsPath)) {
this._console.log(`typingsPath ${configOptions.typingsPath} is not a valid directory.`);
if (configOptions.stubPath) {
if (!this._fs.existsSync(configOptions.stubPath) || !isDirectory(this._fs, configOptions.stubPath)) {
this._console.log(`stubPath ${configOptions.stubPath} is not a valid directory.`);
}
}
@ -604,13 +612,13 @@ export class AnalyzerService {
}
private _getTypeStubFolder() {
const typingsPath = this._configOptions.typingsPath;
const stubPath = this._configOptions.stubPath;
if (!this._typeStubTargetPath || !this._typeStubTargetImportName) {
const errMsg = `Import '${this._typeStubTargetImportName}'` + ` could not be resolved`;
this._console.error(errMsg);
throw new Error(errMsg);
}
if (!typingsPath) {
if (!stubPath) {
// We should never get here because we always generate a
// default typings path if none was specified.
const errMsg = 'No typings path was specified';
@ -627,16 +635,16 @@ export class AnalyzerService {
}
try {
// Generate a new typings directory if necessary.
if (!this._fs.existsSync(typingsPath)) {
this._fs.mkdirSync(typingsPath);
if (!this._fs.existsSync(stubPath)) {
this._fs.mkdirSync(stubPath);
}
} catch (e) {
const errMsg = `Could not create typings directory '${typingsPath}'`;
const errMsg = `Could not create typings directory '${stubPath}'`;
this._console.error(errMsg);
throw new Error(errMsg);
}
// Generate a typings subdirectory.
const typingsSubdirPath = combinePaths(typingsPath, typeStubInputTargetParts[0]);
const typingsSubdirPath = combinePaths(stubPath, typeStubInputTargetParts[0]);
try {
// Generate a new typings subdirectory if necessary.
if (!this._fs.existsSync(typingsSubdirPath)) {

View File

@ -299,6 +299,9 @@ export const enum PrintTypeFlags {
// Omit type arguments for generic classes if they are "Any".
OmitTypeArgumentsIfAny = 1 << 1,
// Print Union and Optional in PEP 604 format.
PEP604 = 1 << 2,
}
interface ParamAssignmentInfo {
@ -12229,8 +12232,16 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
});
const returnType = getFunctionEffectiveReturnType(type);
const returnTypeString =
recursionCount < maxTypeRecursionCount ? printType(returnType, recursionCount + 1) : '';
let returnTypeString = recursionCount < maxTypeRecursionCount ? printType(returnType, recursionCount + 1) : '';
if (
printTypeFlags & PrintTypeFlags.PEP604 &&
returnType.category === TypeCategory.Union &&
recursionCount > 0
) {
returnTypeString = `(${returnTypeString})`;
}
return [paramTypeStrings, returnTypeString];
}
@ -12293,6 +12304,11 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
if (subtypes.find((t) => t.category === TypeCategory.None) !== undefined) {
const optionalType = printType(removeNoneFromUnion(unionType), recursionCount + 1);
if (printTypeFlags & PrintTypeFlags.PEP604) {
return optionalType + ' | None';
}
return 'Optional[' + optionalType + ']';
}
@ -12327,6 +12343,10 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
return subtypeStrings[0];
}
if (printTypeFlags & PrintTypeFlags.PEP604) {
return subtypeStrings.join(' | ');
}
return `Union[${subtypeStrings.join(', ')}]`;
}

View File

@ -127,13 +127,13 @@ export class TypeStubWriter extends ParseTreeWalker {
private _trackedImportFrom = new Map<string, TrackedImportFrom>();
private _accessedImportedSymbols = new Map<string, boolean>();
constructor(private _typingsPath: string, private _sourceFile: SourceFile, private _evaluator: TypeEvaluator) {
constructor(private _stubPath: string, private _sourceFile: SourceFile, private _evaluator: TypeEvaluator) {
super();
// As a heuristic, we'll include all of the import statements
// in "__init__.pyi" files even if they're not locally referenced
// because these are often used as ways to re-export symbols.
if (this._typingsPath.endsWith('__init__.pyi')) {
if (this._stubPath.endsWith('__init__.pyi')) {
this._includeAllImports = true;
}
}
@ -662,6 +662,6 @@ export class TypeStubWriter extends ParseTreeWalker {
finalText += this._printTrackedImports();
finalText += this._typeStubText;
this._sourceFile.fileSystem.writeFileSync(this._typingsPath, finalText, 'utf8');
this._sourceFile.fileSystem.writeFileSync(this._stubPath, finalText, 'utf8');
}
}

View File

@ -152,7 +152,7 @@ export class BackgroundAnalysisBase {
async writeTypeStub(
targetImportPath: string,
targetIsSingleFile: boolean,
typingsPath: string,
stubPath: string,
token: CancellationToken
): Promise<any> {
throwIfCancellationRequested(token);
@ -163,7 +163,7 @@ export class BackgroundAnalysisBase {
const cancellationId = getCancellationTokenId(token);
this._enqueueRequest({
requestType: 'writeTypeStub',
data: { targetImportPath, targetIsSingleFile, typingsPath, cancellationId },
data: { targetImportPath, targetIsSingleFile, stubPath, cancellationId },
port: port2,
});
@ -253,7 +253,7 @@ export class BackgroundAnalysisRunnerBase {
case 'writeTypeStub': {
run(() => {
const { targetImportPath, targetIsSingleFile, typingsPath, cancellationId } = msg.data;
const { targetImportPath, targetIsSingleFile, stubPath, cancellationId } = msg.data;
const token = getCancellationTokenFromId(cancellationId);
analyzeProgram(
@ -264,7 +264,7 @@ export class BackgroundAnalysisRunnerBase {
this._getConsole(),
token
);
this._program.writeTypeStub(targetImportPath, targetIsSingleFile, typingsPath, token);
this._program.writeTypeStub(targetImportPath, targetIsSingleFile, stubPath, token);
}, msg.port!);
break;
}
@ -392,7 +392,7 @@ function createConfigOptionsFrom(jsonObject: any): ConfigOptions {
configOptions.pythonPath = jsonObject.pythonPath;
configOptions.typeshedPath = jsonObject.typeshedPath;
configOptions.typingsPath = jsonObject.typingsPath;
configOptions.stubPath = jsonObject.stubPath;
configOptions.autoExcludeVenv = jsonObject.autoExcludeVenv;
configOptions.verboseOutput = jsonObject.verboseOutput;
configOptions.checkOnlyOpenFiles = jsonObject.checkOnlyOpenFiles;

View File

@ -12,4 +12,5 @@ export const enum Commands {
restartServer = 'pyright.restartserver',
orderImports = 'pyright.organizeimports',
addMissingOptionalToParam = 'pyright.addoptionalforparam',
unusedImport = 'pyright.unusedImport',
}

View File

@ -68,12 +68,6 @@ export class CreateTypeStubCommand implements ServerCommand {
// Creates a service instance that's used for creating type
// stubs for a specified target library.
private async _createTypeStubService(callingFile?: string): Promise<AnalyzerService> {
return this._createAnalyzerService(callingFile);
}
private async _createAnalyzerService(callingFile: string | undefined) {
this._ls.console.log('Starting type stub service instance');
if (callingFile) {
// this should let us to inherit all execution env of the calling file
// if it is invoked from IDE through code action

View File

@ -40,6 +40,9 @@ export class CommandLineOptions {
// Path of typeshed stubs.
typeshedPath?: string;
// Path of typing folder
stubPath?: string;
// Absolute execution root (current working directory).
executionRoot: string;

View File

@ -51,6 +51,9 @@ export interface DiagnosticRuleSet {
// when printed if all arguments are Unknown or Any?
omitTypeArgsIfAny: boolean;
// Should Union and Optional types be printed in PEP 604 format?
pep604Printing: boolean;
// Use strict inference rules for list expressions?
strictListInference: boolean;
@ -254,6 +257,7 @@ export function getStrictDiagnosticRuleSet(): DiagnosticRuleSet {
const diagSettings: DiagnosticRuleSet = {
printUnknownAsAny: false,
omitTypeArgsIfAny: false,
pep604Printing: true,
strictListInference: true,
strictDictionaryInference: true,
strictParameterNoneValue: true,
@ -305,6 +309,7 @@ export function getNoTypeCheckingDiagnosticRuleSet(): DiagnosticRuleSet {
const diagSettings: DiagnosticRuleSet = {
printUnknownAsAny: true,
omitTypeArgsIfAny: true,
pep604Printing: true,
strictListInference: false,
strictDictionaryInference: false,
strictParameterNoneValue: false,
@ -356,6 +361,7 @@ export function getDefaultDiagnosticRuleSet(): DiagnosticRuleSet {
const diagSettings: DiagnosticRuleSet = {
printUnknownAsAny: false,
omitTypeArgsIfAny: false,
pep604Printing: true,
strictListInference: false,
strictDictionaryInference: false,
strictParameterNoneValue: false,
@ -422,7 +428,7 @@ export class ConfigOptions {
typeshedPath?: string;
// Path to custom typings (stub) modules.
typingsPath?: string;
stubPath?: string;
// A list of file specs to include in the analysis. Can contain
// directories, in which case all "*.py" files within those directories
@ -648,6 +654,7 @@ export class ConfigOptions {
this.diagnosticRuleSet = {
printUnknownAsAny: defaultSettings.printUnknownAsAny,
omitTypeArgsIfAny: defaultSettings.omitTypeArgsIfAny,
pep604Printing: defaultSettings.pep604Printing,
// Use strict inference rules for list expressions?
strictListInference: this._convertBoolean(
@ -1002,13 +1009,24 @@ export class ConfigOptions {
}
}
// Read the "typingsPath" setting.
this.typingsPath = undefined;
// Read the "stubPath" setting.
this.stubPath = undefined;
// Keep this for backward compatibility
if (configObj.typingsPath !== undefined) {
if (typeof configObj.typingsPath !== 'string') {
console.log(`Config "typingsPath" field must contain a string.`);
} else {
this.typingsPath = normalizePath(combinePaths(this.projectRoot, configObj.typingsPath));
console.log(`Config "typingsPath" is now deprecated. Please, use stubPath instead.`);
this.stubPath = normalizePath(combinePaths(this.projectRoot, configObj.typingsPath));
}
}
if (configObj.stubPath !== undefined) {
if (typeof configObj.stubPath !== 'string') {
console.log(`Config "stubPath" field must contain a string.`);
} else {
this.stubPath = normalizePath(combinePaths(this.projectRoot, configObj.stubPath));
}
}

View File

@ -7,7 +7,7 @@
* Class that represents errors and warnings.
*/
import { Diagnostic, DiagnosticCategory } from './diagnostic';
import { Diagnostic, DiagnosticAction, DiagnosticCategory } from './diagnostic';
import { convertOffsetsToRange } from './positionUtils';
import { Range, TextRange } from './textRange';
import { TextRangeCollection } from './textRangeCollection';
@ -43,8 +43,12 @@ export class DiagnosticSink {
return this.addDiagnostic(new Diagnostic(DiagnosticCategory.Warning, message, range));
}
addUnusedCode(message: string, range: Range) {
return this.addDiagnostic(new Diagnostic(DiagnosticCategory.UnusedCode, message, range));
addUnusedCode(message: string, range: Range, action?: DiagnosticAction) {
const diag = new Diagnostic(DiagnosticCategory.UnusedCode, message, range);
if (action) {
diag.addAction(action);
}
return this.addDiagnostic(diag);
}
addDiagnostic(diag: Diagnostic) {
@ -91,7 +95,11 @@ export class TextRangeDiagnosticSink extends DiagnosticSink {
return this.addWarning(message, convertOffsetsToRange(range.start, range.start + range.length, this._lines));
}
addUnusedCodeWithTextRange(message: string, range: TextRange) {
return this.addUnusedCode(message, convertOffsetsToRange(range.start, range.start + range.length, this._lines));
addUnusedCodeWithTextRange(message: string, range: TextRange, action?: DiagnosticAction) {
return this.addUnusedCode(
message,
convertOffsetsToRange(range.start, range.start + range.length, this._lines),
action
);
}
}

View File

@ -511,8 +511,8 @@ export function getFileExtension(fileName: string, multiDotExtension = false) {
return path.extname(fileName);
}
const lastDividerIndex = fileName.lastIndexOf(path.sep);
const firstDotIndex = fileName.indexOf('.', lastDividerIndex + 1);
fileName = getFileName(fileName);
const firstDotIndex = fileName.indexOf('.');
return fileName.substr(firstDotIndex);
}

View File

@ -75,6 +75,7 @@ export interface ServerSettings {
venvPath?: string;
pythonPath?: string;
typeshedPath?: string;
stubPath?: string;
openFilesOnly?: boolean;
typeCheckingMode?: string;
useLibraryCodeForTypes?: boolean;
@ -216,6 +217,10 @@ export abstract class LanguageServerBase implements LanguageServerInterface {
return undefined;
}
protected isOpenFilesOnly(diagnosticMode: string): boolean {
return diagnosticMode !== 'workspace';
}
protected createImportResolver(fs: FileSystem, options: ConfigOptions): ImportResolver {
return new ImportResolver(fs, options);
}

View File

@ -73,7 +73,15 @@ function _getCommandLineOptions(
// Pyright supports only one typeshed path currently, whereas the
// official VS Code Python extension supports multiple typeshed paths.
// We'll use the first one specified and ignore the rest.
commandLineOptions.typeshedPath = _expandPathVariables(languageServiceRootPath, serverSettings.typeshedPath);
commandLineOptions.typeshedPath = normalizePath(
_expandPathVariables(languageServiceRootPath, serverSettings.typeshedPath)
);
}
if (serverSettings.stubPath) {
commandLineOptions.stubPath = normalizePath(
_expandPathVariables(languageServiceRootPath, serverSettings.stubPath)
);
}
if (typeStubTargetImportName) {

View File

@ -12,7 +12,7 @@ import { BackgroundAnalysis } from './backgroundAnalysis';
import { BackgroundAnalysisBase } from './backgroundAnalysisBase';
import { CommandController } from './commands/commandController';
import { getCancellationFolderName } from './common/cancellationUtils';
import { isDebugMode } from './common/core';
import { isDebugMode, isString } from './common/core';
import { convertUriToPath, getDirectoryPath, normalizeSlashes } from './common/pathUtils';
import { LanguageServerBase, ServerSettings, WorkspaceServiceInstance } from './languageServerBase';
import { CodeActionProvider } from './languageService/codeActionProvider';
@ -45,6 +45,16 @@ class PyrightServer extends LanguageServerBase {
if (typeshedPaths && isArray(typeshedPaths) && typeshedPaths.length > 0) {
serverSettings.typeshedPath = normalizeSlashes(typeshedPaths[0]);
}
const stubPath = pythonAnalysisSection.stubPath;
if (stubPath && isString(stubPath)) {
serverSettings.stubPath = normalizeSlashes(stubPath);
}
if (pythonAnalysisSection.diagnosticMode !== undefined) {
serverSettings.openFilesOnly = this.isOpenFilesOnly(pythonAnalysisSection.diagnosticMode);
}
serverSettings.autoSearchPaths = !!pythonAnalysisSection.autoSearchPaths;
const extraPaths = pythonAnalysisSection.extraPaths;
@ -57,13 +67,17 @@ class PyrightServer extends LanguageServerBase {
const pyrightSection = await this.getConfiguration(workspace, 'pyright');
if (pyrightSection) {
serverSettings.openFilesOnly = !!pyrightSection.openFilesOnly;
if (pyrightSection.openFilesOnly !== undefined) {
serverSettings.openFilesOnly = !!pyrightSection.openFilesOnly;
}
serverSettings.useLibraryCodeForTypes = !!pyrightSection.useLibraryCodeForTypes;
serverSettings.disableLanguageServices = !!pyrightSection.disableLanguageServices;
serverSettings.disableOrganizeImports = !!pyrightSection.disableOrganizeImports;
serverSettings.typeCheckingMode = pyrightSection.typeCheckingMode;
} else {
serverSettings.openFilesOnly = true;
// Unless openFilesOnly is set explicitly, set it to true by default.
serverSettings.openFilesOnly = serverSettings.openFilesOnly ?? true;
serverSettings.useLibraryCodeForTypes = false;
serverSettings.disableLanguageServices = false;
serverSettings.disableOrganizeImports = false;

View File

@ -117,9 +117,12 @@ declare namespace _ {
moveCaretRight(count: number): void;
verifyDiagnostics(map?: { [marker: string]: { category: string; message: string } }): void;
verifyCodeActions(map: {
[marker: string]: { codeActions: { title: string; kind: string; command: Command }[] };
}): void;
verifyCodeActions(
map: {
[marker: string]: { codeActions: { title: string; kind: string; command: Command }[] };
},
verifyCodeActionCount?: boolean
): void;
verifyCommand(command: Command, files: { [filePath: string]: string }): void;
verifyInvokeCodeAction(
map: {

View File

@ -31,8 +31,8 @@ const xInitSignatures = [
const xComplicatedSignatures = [
{
label: '(a: int, b: int, c: int = 1234, d: Optional[str] = None, **kwargs) -> Union[int, str]',
parameters: ['a: int', 'b: int', 'c: int = 1234', 'd: Optional[str] = None', '**kwargs'],
label: '(a: int, b: int, c: int = 1234, d: str | None = None, **kwargs) -> int | str',
parameters: ['a: int', 'b: int', 'c: int = 1234', 'd: str | None = None', '**kwargs'],
},
];

View File

@ -575,23 +575,37 @@ export class TestState {
}
}
async verifyCodeActions(map: {
[marker: string]: { codeActions: { title: string; kind: string; command: Command }[] };
}): Promise<any> {
async verifyCodeActions(
map: {
[marker: string]: { codeActions: { title: string; kind: string; command: Command }[] };
},
verifyCodeActionCount?: boolean
): Promise<any> {
this._analyze();
for (const range of this.getRanges()) {
const name = this.getMarkerName(range.marker!);
for (const expected of map[name].codeActions) {
const actual = await this._getCodeActions(range);
if (!map[name]) {
continue;
}
const codeActions = await this._getCodeActions(range);
if (verifyCodeActionCount) {
if (codeActions.length !== map[name].codeActions.length) {
this._raiseError(
`doesn't contain expected result: ${stringify(map[name])}, actual: ${stringify(codeActions)}`
);
}
}
for (const expected of map[name].codeActions) {
const expectedCommand = {
title: expected.command.title,
command: expected.command.command,
arguments: convertToString(expected.command.arguments),
};
const matches = actual.filter((a) => {
const matches = codeActions.filter((a) => {
const actualCommand = a.command
? {
title: a.command.title,
@ -609,7 +623,7 @@ export class TestState {
if (matches.length !== 1) {
this._raiseError(
`doesn't contain expected result: ${stringify(expected)}, actual: ${stringify(actual)}`
`doesn't contain expected result: ${stringify(expected)}, actual: ${stringify(codeActions)}`
);
}
}
@ -1035,8 +1049,8 @@ export class TestState {
configOptions.defaultVenv = vfs.MODULE_PATH;
// make sure we set typing path
if (configOptions.typingsPath === undefined) {
configOptions.typingsPath = normalizePath(combinePaths(vfs.MODULE_PATH, 'typings'));
if (configOptions.stubPath === undefined) {
configOptions.stubPath = normalizePath(combinePaths(vfs.MODULE_PATH, 'typings'));
}
return configOptions;

View File

@ -120,7 +120,32 @@ test('Configuration', () => {
assert.equal(state.configOptions.diagnosticRuleSet.reportMissingImports, 'error');
assert.equal(state.configOptions.diagnosticRuleSet.reportMissingModuleSource, 'warning');
assert.equal(state.configOptions.typingsPath, normalizeSlashes('/src/typestubs'));
assert.equal(state.configOptions.stubPath, normalizeSlashes('/src/typestubs'));
});
test('stubPath configuration', () => {
const code = `
// @filename: mspythonconfig.json
//// {
//// "stubPath": "src/typestubs"
//// }
`;
const state = parseAndGetTestState(code).state;
assert.equal(state.configOptions.stubPath, normalizeSlashes('/src/typestubs'));
});
test('Duplicated stubPath configuration', () => {
const code = `
// @filename: mspythonconfig.json
//// {
//// "typingsPath": "src/typestubs1",
//// "stubPath": "src/typestubs2"
//// }
`;
const state = parseAndGetTestState(code).state;
assert.equal(state.configOptions.stubPath, normalizeSlashes('/src/typestubs2'));
});
test('ProjectRoot', () => {