Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions ThirdPartyNotices-Distribution.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Microsoft Python extension for Visual Studio Code incorporates components from t
34. vscode-extension-telemetry (https://github.com/Microsoft/vscode-extension-telemetry)
35. vscode-languageclient (https://github.com/Microsoft/vscode-languageserver-node)
36. vscode-languageserver (https://github.com/Microsoft/vscode-languageserver-node/)
37. importmagic (https://github.com/alecthomas/importmagic/)


%% Arch NOTICES AND INFORMATION BEGIN HERE
Expand Down Expand Up @@ -1511,3 +1512,31 @@ THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI

=========================================
END OF vscode-languageserver NOTICES AND INFORMATION

%% importmagic NOTICES AND INFORMATION BEGIN HERE
=========================================
Copyright (c) 2014, 2015, Alec Thomas
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
=========================================
END OF importmagic NOTICES AND INFORMATION
32 changes: 30 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,11 @@
"onCommand:python.startREPL",
"onCommand:python.goToPythonObject",
"onCommand:python.setLinter",
"onCommand:python.enableLinting",
"onCommand:python.createTerminal"
"onCommand:python.enableLinting",
"onCommand:python.createTerminal",
"onCommand:importMagic.resolveImport",
"onCommand:importMagic.insertImport",
"onCommand:importMagic.findSymbols"
],
"main": "./out/client/extension",
"contributes": {
Expand All @@ -94,6 +97,11 @@
"command": "jupyter.runSelectionLine",
"key": "ctrl+alt+enter",
"when": "editorFocus && !replaceActive && !searchViewletVisible && !findWidgetVisible"
},
{
"command": "importMagic.findSymbols",
"key": "ctrl+shift+space",
"when": "editorLangId == python"
}
],
"commands": [
Expand All @@ -102,6 +110,21 @@
"title": "%python.command.python.sortImports.title%",
"category": "Python Refactor"
},
{
"command": "importMagic.resolveImport",
"title": "%python.command.importMagic.resolveImport.title%",
"category": "Python Refactor"
},
{
"command": "importMagic.insertImport",
"title": "%python.command.importMagic.insertImport.title%",
"category": "Python Refactor"
},
{
"command": "importMagic.findSymbols",
"title": "%python.command.importMagic.findSymbols.title%",
"category": "Python Refactor"
},
{
"command": "python.startREPL",
"title": "%python.command.python.startREPL.title%",
Expand Down Expand Up @@ -253,6 +276,11 @@
"group": "Refactor",
"when": "editorLangId == python"
},
{
"command": "importMagic.resolveImport",
"group": "Refactor",
"when": "editorLangId == python"
},
{
"command": "python.execSelectionInTerminal",
"group": "Python",
Expand Down
5 changes: 4 additions & 1 deletion package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,8 @@
"python.snippet.launch.attach.label": "Python: Attach",
"python.snippet.launch.attach.description": "Attach the debugger for remote debugging",
"python.snippet.launch.scrapy.label": "Python: Scrapy",
"python.snippet.launch.scrapy.description": "Scrapy with Integrated Terminal/Console"
"python.snippet.launch.scrapy.description": "Scrapy with Integrated Terminal/Console",
"python.command.importMagic.resolveImport.title": "Resolve Imports",
"python.command.importMagic.insertImport.title": "Insert Import",
"python.command.importMagic.findSymbols.title": "Find Symbols"
}
236 changes: 236 additions & 0 deletions pythonFiles/importMagic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
import io
import os
import re
import sys
import json
import time
import traceback

try:
import importmagic
except:
json_message = {'error': True, 'message': 'Importmagic not installed',
'traceback': '', 'type': 'ModuleNotFoundError'}
sys.stderr.write(json.dumps(json_message))
sys.stderr.write('\n')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about:

from __future__ import print_function

print(json.dumps(json_message), file=sys.stderr)

sys.stderr.flush()


ITEMS_LIMIT = 50
INDEX_FILE_TTL = 300


class ImportMagicDaemon(object):
def __init__(self):
self._input = io.open(sys.stdin.fileno(), encoding='utf-8')
self._index = None

def method_build_index(self, **kwargs):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these named method_*?

workspace_path = kwargs.get('workspacePath')
extra_paths = kwargs.get('extraPaths', [])
skip_test_folders = kwargs.get('skipTestFolders')
force_rebuild = kwargs.get('forceRebuild')

if not workspace_path:
raise ValueError('Empty workspacePath')

paths = list(set(extra_paths + [workspace_path] + sys.path))

index_file = os.path.join(workspace_path, '.vscode', 'importmagic')
index_file_ts = None

try:
index_file_ts = os.path.getmtime(index_file)
except Exception:
pass

if force_rebuild or not index_file_ts or time.time() - index_file_ts > INDEX_FILE_TTL:
if skip_test_folders:
blacklist_re = importmagic.index.DEFAULT_BLACKLIST_RE
else:
blacklist_re = re.compile(r'^$')

self._index = importmagic.SymbolIndex(blacklist_re=blacklist_re)
self._index.build_index(paths)

try:
with open(index_file, 'w') as fd:
self._index.serialize(fd)
except Exception:
pass
else:
# Read old index
with open(index_file, 'r') as fd:
self._index = importmagic.SymbolIndex.deserialize(fd)

return dict(success=True)

def method_get_unresolved(self, **kwargs):
source_file = kwargs.get('sourceFile')

if not source_file:
raise ValueError('Empty sourceFile')

if not self._index:
raise Exception('First run build_index() method')

with open(source_file, 'r') as fd:
python_source = fd.read()
scope = importmagic.Scope.from_source(python_source)

unresolved, unreferenced = \
scope.find_unresolved_and_unreferenced_symbols()

return dict(unresolved=list(unresolved), unreferenced=list(unreferenced))

def method_import_suggestions(self, **kwargs):
source_file = kwargs.get('sourceFile')
unresolved_name = kwargs.get('unresolvedName')

if not source_file:
raise ValueError('Empty sourceFile')

if not unresolved_name:
raise ValueError('Empty unresolvedName')

if not self._index:
raise Exception('First run build_index() method')

with open(source_file, 'r') as fd:
python_source = fd.read()
scope = importmagic.Scope.from_source(python_source)

_unresolved, _unreferenced = \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leading underscore typically denotes unused variables declared only for unpacking:

foo, _dummy = function_that_returns_a_tuple()

scope.find_unresolved_and_unreferenced_symbols()

# Sometimes unresolved may contain "sys.path".
# Split this cases for find "sys.path", "sys" and "path"
unresolved = set()
for item1 in _unresolved:
for item2 in item1.split('.'):
unresolved.add(item2)

if unresolved_name not in unresolved:
raise Exception('Import this expression is not necessary')

candidates = []
for score, module, variable in self._index.symbol_scores(unresolved_name):
candidates.append(dict(score=score, module=module,
variable=variable))
if len(candidates) >= ITEMS_LIMIT:
break

return dict(candidates=candidates)

def method_insert_import(self, **kwargs):
source_file = kwargs.get('sourceFile')

module = kwargs.get('module')
variable = kwargs.get('variable')

if not source_file:
raise ValueError('Empty sourceFile')

if not module:
raise ValueError('Empty module')

if not self._index:
raise Exception('First run inibuild_indext() method')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no inibuild_indext.


with open(source_file, 'r') as fd:
python_source = fd.read()

imports = importmagic.Imports(self._index, python_source)
imports.set_style(multiline='backslash', max_columns=79) # TODO: Get style from VS Code

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will likely become a blocker for merging this change. You could import isort and use its configuration parser to determine proper settings for the file. This would make sure whatever this tool does is not immediately flagged by an isort checker.


if variable is None:
imports.add_import(module)
else:
imports.add_import_from(module, variable)
start, end, text = imports.get_update()

return dict(fromLine=start, endLine=end, text=text)

def method_get_symbols(self, **kwargs):
find = kwargs.get('text', '').replace('_', '').lower()

if not self._index:
raise Exception('First run build_index() method')

results = {}

def scan_tree(scope):
for key, subscope in scope._tree.items():
if type(subscope) is not float:
get_result(key, scope, subscope.score)
scan_tree(subscope)
else:
get_result(key, scope, subscope)

def get_result(key, scope, key_score):
k = key.replace('_', '').lower()
if not k.startswith(find):
return

score = -(key_score + (100-scope.depth())*10)
if k not in results or results[k]['_score'] > score:
if scope.path():
module = scope.path()
variable = key
else:
module = key
variable = None

results[k] = dict(key=key, module=module, variable=variable,
depth=scope.depth(), _score=score, score=key_score)


scan_tree(self._index)

return dict(items=sorted(results.values(), key=lambda x: x['_score'])[:ITEMS_LIMIT])

def _process_request(self, request):
method_name = request.get('method')

result = None
method = getattr(self, 'method_%s' % method_name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be much more readable with a simple if/elif/elif switch. Dynamically constructing method names and using getattr to access code means that you will never be able to find where a particular function is used.

if not method:
raise ValueError('Invalid method name')

result = method(**request)

if not result or type(result) != dict:
raise ValueError('Method must return dict')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like something that should be covered by tests rather than a runtime check.


return result

def _error_response(self, response):
sys.stderr.write(json.dumps(response))
sys.stderr.write('\n')
sys.stderr.flush()

def _success_response(self, response):
sys.stdout.write(json.dumps(response))
sys.stdout.write('\n')
sys.stdout.flush()

def watch(self):
while True:
try:
request = json.loads(self._input.readline())
request_id = request.get('requestId')

if not request_id:
raise ValueError('Empty request id')

response = self._process_request(request)
json_message = dict(id=request_id, **response)
self._success_response(json_message)
except:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unqualified except is bad. It will capture unexpected things, like SystemExit or KeyboardInterrupt.

exc_type, exc_value, exc_tb = sys.exc_info()
tb_info = traceback.extract_tb(exc_tb)
json_message = dict(error=True, id=request_id, message=str(exc_value), traceback=str(tb_info), type=str(exc_type))
self._error_response(json_message)

if __name__ == '__main__':
ImportMagicDaemon().watch()
11 changes: 11 additions & 0 deletions pythonFiles/importmagic/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Python Import Magic - automagically add, remove and manage imports

This module just exports the main API of importmagic.
"""

__author__ = 'Alec Thomas <alec@swapoff.org>'
__version__ = '0.1.7'

from importmagic.importer import Import, Imports, get_update, update_imports
from importmagic.index import SymbolIndex
from importmagic.symbols import Scope
Loading