Added a bunch of verbose diagnostic information to help resolve import resolution issues.

This commit is contained in:
Eric Traut 2019-04-27 19:04:37 -07:00
parent 9077e7d392
commit 375e35e56d
11 changed files with 134 additions and 44 deletions

View File

@ -9,6 +9,7 @@ Pyright can be run as either a VS Code extension or as a node-based command-line
| --stats | Print detailed performance stats |
| -t, --typeshed-path DIRECTORY | Use typeshed type stubs at this location (1) |
| -v, --venv-path DIRECTORY | Directory that contains virtual environments (2) |
| --verbose | Emit verbose diagnostics |
| -w, --watch | Continue to run and watch for changes (3) |

View File

@ -11,7 +11,6 @@
import * as fs from 'fs';
import { ConfigOptions, ExecutionEnvironment } from '../common/configOptions';
import { ConsoleInterface } from '../common/console';
import { combinePaths, getDirectoryPath, getFileSystemEntries, isDirectory, isFile,
stripFileExtension, stripTrailingDirectorySeparator } from '../common/pathUtils';
import { is3x, versionToString } from '../common/pythonVersion';
@ -38,18 +37,20 @@ export class ImportResolver {
// Resolves the import and returns the path if it exists, otherwise
// returns undefined.
resolveImport(moduleDescriptor: ImportedModuleDescriptor, consoleInterface: ConsoleInterface): ImportResult {
let importName = this._formatImportName(moduleDescriptor);
resolveImport(moduleDescriptor: ImportedModuleDescriptor): ImportResult {
const importName = this._formatImportName(moduleDescriptor);
const importFailureInfo: string[] = [];
// Find the site packages for the configured virtual environment.
if (this._cachedPythonSearchPaths === undefined) {
this._cachedPythonSearchPaths = PythonPathUtils.findPythonSearchPaths(
this._configOptions, this._executionEnvironment, consoleInterface);
this._configOptions, this._executionEnvironment, importFailureInfo);
}
// First check for a typeshed file.
if (moduleDescriptor.leadingDots === 0 && moduleDescriptor.nameParts.length > 0) {
let builtInImport = this._findTypeshedPath(moduleDescriptor, importName, true);
let builtInImport = this._findTypeshedPath(moduleDescriptor, importName,
true, importFailureInfo);
if (builtInImport) {
builtInImport.isTypeshedFile = true;
return builtInImport;
@ -58,7 +59,8 @@ export class ImportResolver {
// Is it a relative import?
if (moduleDescriptor.leadingDots > 0) {
let relativeImport = this._resolveRelativeImport(moduleDescriptor, importName);
let relativeImport = this._resolveRelativeImport(moduleDescriptor,
importName, importFailureInfo);
if (relativeImport) {
return relativeImport;
}
@ -67,7 +69,7 @@ export class ImportResolver {
// Look for it in the root directory of the execution environment.
let localImport = this._resolveAbsoluteImport(
this._executionEnvironment.root, moduleDescriptor, importName);
this._executionEnvironment.root, moduleDescriptor, importName, importFailureInfo);
if (localImport && localImport.importFound) {
return localImport;
}
@ -75,7 +77,8 @@ export class ImportResolver {
for (let i = 0; i < this._executionEnvironment.extraPaths.length; i++) {
let extraPath = this._executionEnvironment.extraPaths[i];
localImport = this._resolveAbsoluteImport(extraPath, moduleDescriptor, importName);
localImport = this._resolveAbsoluteImport(extraPath, moduleDescriptor,
importName, importFailureInfo);
if (localImport && localImport.importFound) {
return localImport;
}
@ -89,14 +92,15 @@ export class ImportResolver {
// Check for a typings file.
if (this._configOptions.typingsPath) {
let typingsImport = this._resolveAbsoluteImport(
this._configOptions.typingsPath, moduleDescriptor, importName);
this._configOptions.typingsPath, moduleDescriptor, importName, importFailureInfo);
if (typingsImport && typingsImport.importFound) {
return typingsImport;
}
}
// Check for a typeshed file.
let typeshedImport = this._findTypeshedPath(moduleDescriptor, importName, false);
let typeshedImport = this._findTypeshedPath(moduleDescriptor, importName,
false, importFailureInfo);
if (typeshedImport) {
typeshedImport.isTypeshedFile = true;
return typeshedImport;
@ -108,7 +112,7 @@ export class ImportResolver {
// Allow partial resolution because some third-party packages
// use tricks to populate their package namespaces.
let thirdPartyImport = this._resolveAbsoluteImport(
searchPath, moduleDescriptor, importName, true);
searchPath, moduleDescriptor, importName, importFailureInfo, true);
if (thirdPartyImport) {
thirdPartyImport.importType = ImportType.ThirdParty;
return thirdPartyImport;
@ -126,6 +130,7 @@ export class ImportResolver {
return {
importName,
importFound: false,
importFailureInfo,
resolvedPaths: [],
importType: ImportType.Local,
isNamespacePackage: false,
@ -135,7 +140,9 @@ export class ImportResolver {
}
private _findTypeshedPath(moduleDescriptor: ImportedModuleDescriptor, importName: string,
isStdLib: boolean): ImportResult | undefined {
isStdLib: boolean, importFailureInfo: string[]): ImportResult | undefined {
importFailureInfo.push('Looking for typeshed path');
let typeshedPath = '';
@ -144,12 +151,14 @@ export class ImportResolver {
if (this._configOptions.typeshedPath) {
const possibleTypeshedPath = this._configOptions.typeshedPath;
if (fs.existsSync(possibleTypeshedPath) && isDirectory(possibleTypeshedPath)) {
importFailureInfo.push(`Found typeshed path '${ typeshedPath }'`);
typeshedPath = possibleTypeshedPath;
}
} else if (this._cachedPythonSearchPaths) {
for (let searchPath of this._cachedPythonSearchPaths) {
const possibleTypeshedPath = combinePaths(searchPath, 'typeshed');
if (fs.existsSync(possibleTypeshedPath) && isDirectory(possibleTypeshedPath)) {
importFailureInfo.push(`Found typeshed path '${ typeshedPath }'`);
typeshedPath = possibleTypeshedPath;
break;
}
@ -159,17 +168,20 @@ export class ImportResolver {
// If typeshed directory wasn't found in other locations, use the fallback.
if (!typeshedPath) {
typeshedPath = PythonPathUtils.getTypeShedFallbackPath() || '';
importFailureInfo.push(`Using typeshed fallback path '${ typeshedPath }'`);
}
typeshedPath = PythonPathUtils.getTypeshedSubdirectory(typeshedPath, isStdLib);
if (!fs.existsSync(typeshedPath) || !isDirectory(typeshedPath)) {
importFailureInfo.push(`Typeshed path '${ typeshedPath }' is not a valid directory`);
return undefined;
}
// We currently support only 3.x.
let pythonVersion = this._executionEnvironment.pythonVersion;
if (!is3x(pythonVersion)) {
importFailureInfo.push(`Python version < 3.0 not supported`);
return undefined;
}
@ -181,7 +193,8 @@ export class ImportResolver {
minorVersion === 0 ? '3' : '2and3';
let testPath = combinePaths(typeshedPath, pythonVersionString);
if (fs.existsSync(testPath)) {
let importInfo = this._resolveAbsoluteImport(testPath, moduleDescriptor, importName);
let importInfo = this._resolveAbsoluteImport(testPath, moduleDescriptor,
importName, importFailureInfo);
if (importInfo && importInfo.importFound) {
if (isStdLib) {
importInfo.importType = ImportType.BuiltIn;
@ -197,11 +210,14 @@ export class ImportResolver {
minorVersion--;
}
importFailureInfo.push(`Typeshed path not found`);
return undefined;
}
private _resolveRelativeImport(moduleDescriptor: ImportedModuleDescriptor,
importName: string): ImportResult | undefined {
importName: string, importFailureInfo: string[]): ImportResult | undefined {
importFailureInfo.push('Attempting to resolve relative import');
// Determine which search path this file is part of.
let curDir = getDirectoryPath(this._sourceFilePath);
@ -210,13 +226,16 @@ export class ImportResolver {
}
// Now try to match the module parts from the current directory location.
return this._resolveAbsoluteImport(curDir, moduleDescriptor, importName);
return this._resolveAbsoluteImport(curDir, moduleDescriptor,
importName, importFailureInfo);
}
// Follows import resolution algorithm defined in PEP-420:
// https://www.python.org/dev/peps/pep-0420/
private _resolveAbsoluteImport(rootPath: string, moduleDescriptor: ImportedModuleDescriptor,
importName: string, allowPartial = false): ImportResult | undefined {
importName: string, importFailureInfo: string[], allowPartial = false): ImportResult | undefined {
importFailureInfo.push(`Attempting to resolve using root path '${ rootPath }'`);
// Starting at the specified path, walk the file system to find the
// specified module.
@ -228,14 +247,18 @@ export class ImportResolver {
// Handle the "from . import XXX" case.
if (moduleDescriptor.nameParts.length === 0) {
let pyFilePath = combinePaths(dirPath, '__init__.py');
let pyiFilePath = pyFilePath + 'i';
const pyFilePath = combinePaths(dirPath, '__init__.py');
const pyiFilePath = pyFilePath + 'i';
if (fs.existsSync(pyiFilePath) && isFile(pyiFilePath)) {
importFailureInfo.push(`Resolved import with file '${ pyiFilePath }'`);
resolvedPaths.push(pyiFilePath);
isStubFile = true;
} else if (fs.existsSync(pyFilePath) && isFile(pyFilePath)) {
importFailureInfo.push(`Resolved import with file '${ pyFilePath }'`);
resolvedPaths.push(pyFilePath);
} else {
importFailureInfo.push(`Partially resolved import with directory '${ dirPath }'`);
resolvedPaths.push(dirPath);
isNamespacePackage = true;
}
@ -246,27 +269,38 @@ export class ImportResolver {
for (let i = 0; i < moduleDescriptor.nameParts.length; i++) {
dirPath = combinePaths(dirPath, moduleDescriptor.nameParts[i]);
if (!fs.existsSync(dirPath) || !isDirectory(dirPath)) {
importFailureInfo.push(`Could not find directory '${ dirPath }'`);
// We weren't able to find the subdirectory. See if we can
// find a ".py" or ".pyi" file with this name.
let pyFilePath = stripTrailingDirectorySeparator(dirPath) + '.py';
let pyiFilePath = pyFilePath + 'i';
const pyFilePath = stripTrailingDirectorySeparator(dirPath) + '.py';
const pyiFilePath = pyFilePath + 'i';
if (fs.existsSync(pyiFilePath) && isFile(pyiFilePath)) {
importFailureInfo.push(`Resolved import with file '${ pyiFilePath }'`);
resolvedPaths.push(pyiFilePath);
isStubFile = true;
} else if (fs.existsSync(pyFilePath) && isFile(pyFilePath)) {
importFailureInfo.push(`Resolved import with file '${ pyFilePath }'`);
resolvedPaths.push(pyFilePath);
} else {
importFailureInfo.push(`Did not find file '${ pyiFilePath }' or '${ pyFilePath }'`);
}
break;
}
let pyFilePath = combinePaths(dirPath, '__init__.py');
let pyiFilePath = pyFilePath + 'i';
const pyFilePath = combinePaths(dirPath, '__init__.py');
const pyiFilePath = pyFilePath + 'i';
if (fs.existsSync(pyiFilePath) && isFile(pyiFilePath)) {
importFailureInfo.push(`Resolved import with file '${ pyiFilePath }'`);
resolvedPaths.push(pyiFilePath);
isStubFile = true;
} else if (fs.existsSync(pyFilePath) && isFile(pyFilePath)) {
importFailureInfo.push(`Resolved import with file '${ pyFilePath }'`);
resolvedPaths.push(pyFilePath);
} else {
importFailureInfo.push(`Partially resolved import with directory '${ dirPath }'`);
resolvedPaths.push(dirPath);
if (i === moduleDescriptor.nameParts.length - 1) {
isNamespacePackage = true;
@ -295,6 +329,7 @@ export class ImportResolver {
return {
importName,
importFound,
importFailureInfo,
importType: ImportType.Local,
resolvedPaths,
searchPath: rootPath,

View File

@ -26,6 +26,10 @@ export interface ImportResult {
// True if import was resolved to a module or file.
importFound: boolean;
// If importFound is false, may contain strings that help
// diagnose the import resolution failure.
importFailureInfo?: string[];
// Type of import (built-in, local, third-party).
importType: ImportType;

View File

@ -310,7 +310,7 @@ export class Program {
}
if (fileToParse.sourceFile.parse(options)) {
this._updateSourceFileImports(fileToParse);
this._updateSourceFileImports(fileToParse, options);
}
}
@ -715,15 +715,17 @@ export class Program {
return false;
}
private _updateSourceFileImports(sourceFileInfo: SourceFileInfo): SourceFileInfo[] {
let filesAdded: SourceFileInfo[] = [];
private _updateSourceFileImports(sourceFileInfo: SourceFileInfo,
options: ConfigOptions): SourceFileInfo[] {
const filesAdded: SourceFileInfo[] = [];
// Get the new list of imports and see if it changed from the last
// list of imports for this file.
let imports = sourceFileInfo.sourceFile.getImports();
const imports = sourceFileInfo.sourceFile.getImports();
// Create a map of unique imports, since imports can appear more than once.
let newImportPathMap: { [name: string]: boolean } = {};
const newImportPathMap: { [name: string]: boolean } = {};
imports.forEach(importResult => {
if (importResult.importFound) {
// Don't explore any third-party files unless they're type stub files.
@ -731,7 +733,7 @@ export class Program {
// Namespace packages have no __init__.py file, so the resolved
// path points to a directory.
if (!importResult.isNamespacePackage && importResult.resolvedPaths.length > 0) {
let filePath = importResult.resolvedPaths[
const filePath = importResult.resolvedPaths[
importResult.resolvedPaths.length - 1];
newImportPathMap[filePath] = !!importResult.isTypeshedFile;
}
@ -740,12 +742,22 @@ export class Program {
newImportPathMap[implicitImport.path] = !!importResult.isTypeshedFile;
});
}
} else if (options.verboseOutput) {
if (!sourceFileInfo.isTypeshedFile || options.reportTypeshedErrors) {
this._console.log(`Could not import '${ importResult.importName }' ` +
`in file '${ sourceFileInfo.sourceFile.getFilePath() }'`);
if (importResult.importFailureInfo) {
importResult.importFailureInfo.forEach(diag => {
this._console.log(` ${ diag }`);
});
}
}
}
});
let updatedImportMap: { [name: string]: SourceFileInfo } = {};
const updatedImportMap: { [name: string]: SourceFileInfo } = {};
sourceFileInfo.imports.forEach(importInfo => {
let oldFilePath = importInfo.sourceFile.getFilePath();
const oldFilePath = importInfo.sourceFile.getFilePath();
// A previous import was removed.
if (newImportPathMap[oldFilePath] === undefined) {

View File

@ -37,7 +37,9 @@ export class PythonPathUtils {
}
static findPythonSearchPaths(configOptions: ConfigOptions, execEnv: ExecutionEnvironment | undefined,
consoleInterface: ConsoleInterface): string[] | undefined {
importFailureInfo: string[]): string[] | undefined {
importFailureInfo.push('Finding python search paths');
let venvPath: string | undefined;
if (execEnv && execEnv.venv) {
@ -54,7 +56,10 @@ export class PythonPathUtils {
let libPath = combinePaths(venvPath, 'lib');
let sitePackagesPath = combinePaths(libPath, 'site-packages');
if (fs.existsSync(sitePackagesPath)) {
importFailureInfo.push(`Found path '${ sitePackagesPath }'`);
return [sitePackagesPath];
} else {
importFailureInfo.push(`Did not find '${ sitePackagesPath }', so looking for python subdirectory`);
}
// We didn't find a site-packages directory directly in the lib
@ -65,18 +70,21 @@ export class PythonPathUtils {
if (dirName.startsWith('python')) {
let dirPath = combinePaths(libPath, dirName, 'site-packages');
if (fs.existsSync(dirPath)) {
importFailureInfo.push(`Found path '${ dirPath }'`);
return [dirPath];
}
}
}
}
importFailureInfo.push(`Did not find site-packages. Falling back on python interpreter.`);
// Fall back on the python interpreter.
return this.getPythonPathFromPythonInterpreter(configOptions.pythonPath, consoleInterface);
return this.getPythonPathFromPythonInterpreter(configOptions.pythonPath, importFailureInfo);
}
static getPythonPathFromPythonInterpreter(interpreterPath: string | undefined,
consoleInterface: ConsoleInterface): string[] {
importFailureInfo: string[]): string[] {
const searchKey = interpreterPath || '';
@ -92,9 +100,11 @@ export class PythonPathUtils {
let execOutput: string;
if (interpreterPath) {
importFailureInfo.push(`Executing interpreter at '${ interpreterPath }'`);
execOutput = child_process.execFileSync(
interpreterPath, commandLineArgs, { encoding: 'utf8' });
} else {
importFailureInfo.push(`Executing python interpreter`);
execOutput = child_process.execFileSync(
'python', commandLineArgs, { encoding: 'utf8' });
}
@ -124,18 +134,20 @@ export class PythonPathUtils {
}
if (pythonPaths.length === 0) {
consoleInterface.error(`Attempted to get python import paths from python interpreter but ` +
`found no valid directories`);
importFailureInfo.push(`Found no valid directories`);
}
} else {
consoleInterface.error(`Attempted to get python import paths from python interpreter but ` +
`could not parse output: '${ execOutput }'`);
importFailureInfo.push(`Could not parse output: '${ execOutput }'`);
}
} catch {
pythonPaths = [];
}
cachedSearchPaths[searchKey] = pythonPaths;
importFailureInfo.push(`Received ${ pythonPaths.length } paths from interpreter`);
pythonPaths.forEach(path => {
importFailureInfo.push(` ${ path }`);
});
return pythonPaths;
}

View File

@ -232,6 +232,10 @@ export class AnalyzerService {
}
}
if (commandLineOptions.verboseOutput) {
configOptions.verboseOutput = true;
}
// Do some sanity checks on the specified settings and report missing
// or inconsistent information.
if (configOptions.venvPath) {
@ -245,14 +249,26 @@ export class AnalyzerService {
`venvPath must be used in conjunction with venv setting, which was omitted.`);
} else {
const fullVenvPath = combinePaths(configOptions.venvPath, configOptions.defaultVenv);
if (!fs.existsSync(fullVenvPath) || !isDirectory(fullVenvPath)) {
this._console.log(
`venv ${ configOptions.defaultVenv } subdirectory not found ` +
`in venv path ${ configOptions.venvPath }.`);
} else if (PythonPathUtils.findPythonSearchPaths(configOptions, undefined, this._console) === undefined) {
this._console.log(
`site-packages directory cannot be located for venvPath ` +
`${ configOptions.venvPath } and venv ${ configOptions.defaultVenv }.`);
} else {
const importFailureInfo: string[] = [];
if (PythonPathUtils.findPythonSearchPaths(configOptions, undefined,
importFailureInfo) === undefined) {
this._console.log(
`site-packages directory cannot be located for venvPath ` +
`${ configOptions.venvPath } and venv ${ configOptions.defaultVenv }.`);
if (configOptions.verboseOutput) {
importFailureInfo.forEach(diag => {
this._console.log(` ${ diag }`);
});
}
}
}
}
} else {

View File

@ -597,7 +597,7 @@ export class SourceFile {
leadingDots: 0,
nameParts: ['builtins'],
importedSymbols: undefined
}, this._console);
});
// Avoid importing builtins from the builtins.pyi file itself.
if (builtinsImportResult.resolvedPaths.length === 0 ||
@ -612,7 +612,7 @@ export class SourceFile {
leadingDots: 0,
nameParts: ['typing'],
importedSymbols: undefined
}, this._console);
});
// Avoid importing typing from the typing.pyi file itself.
let typingModulePath: string | undefined;
@ -627,7 +627,7 @@ export class SourceFile {
leadingDots: moduleImport.leadingDots,
nameParts: moduleImport.nameParts,
importedSymbols: moduleImport.importedSymbols
}, this._console);
});
imports.push(importResult);
// Associate the import results with the module import

View File

@ -40,6 +40,9 @@ export class CommandLineOptions {
// Absolute execution root (current working directory).
executionRoot: string;
// Emit verbose information to console?
verboseOutput?: boolean;
// Indicates that the settings came from VS Code rather than
// from the command-line. Useful for providing clearer error
// messages.

View File

@ -73,6 +73,9 @@ export class ConfigOptions {
// if they are included in the transitive closure of included files.
ignore: string[] = [];
// Emit verbose information to console?
verboseOutput: boolean;
//---------------------------------------------------------------
// Diagnostics Settings

View File

@ -38,6 +38,7 @@ function processArgs() {
{ name: 'stats' },
{ name: 'typeshed-path', alias: 't', type: String },
{ name: 'venv-path', alias: 'v', type: String },
{ name: 'verbose', type: Boolean },
{ name: 'watch', alias: 'w', type: Boolean }
];
@ -75,6 +76,8 @@ function processArgs() {
options.typeshedPath = combinePaths(process.cwd(), normalizePath(args['typeshed-path']));
}
options.verboseOutput = args.verbose !== undefined;
let watch = args.watch !== undefined;
options.watch = watch;

View File

@ -169,6 +169,7 @@ _connection.onCompletion(params => {
function updateOptionsAndRestartService(settings?: Settings) {
let commandLineOptions = new CommandLineOptions(_rootPath, true);
commandLineOptions.watch = true;
commandLineOptions.verboseOutput = true;
if (settings && settings.python) {
if (settings.python.venvPath) {