diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000000..4f92a467d0ef --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Tab indentation +[*] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +insert_final_newline = true + +# The indent size used in the `package.json` file cannot be changed +# https://github.com/npm/npm/pull/3180#issuecomment-16336516 +[{.travis.yml,npm-shrinkwrap.json,package.json}] +indent_style = space +indent_size = 4 diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 000000000000..7c2542a3c660 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,15 @@ + +{ + "env": { + "node": true, + "es6": true + }, + "rules": { + "no-console": 0, + "no-cond-assign": 0, + "no-unused-vars": 1, + "no-extra-semi": "warn", + "semi": "warn" + }, + "extends": "eslint:recommended" +} diff --git a/.gitignore b/.gitignore index dd0be90ebb16..a5334358937a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,10 @@ out node_modules *.pyc -.vscode/.ropeproject/** \ No newline at end of file +**/.vscode/.ropeproject/** +**/testFiles/**/.cache/** +*.noseids +.vscode-test +__pycache__ +npm-debug.log +**/.mypy_cache/** diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index a49472051702..000000000000 --- a/.gitmodules +++ /dev/null @@ -1,4 +0,0 @@ -[submodule "docs"] - path = docs - url = https://github.com/DonJayamanne/pythonVSCodeDocs.git - branch = help diff --git a/.travis.yml b/.travis.yml index cc5e0ec69aca..4631c982bbdb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,24 +1,37 @@ language: python -sudo: required +cache: pip +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - gcc-4.9 + - g++-4.9 + - gcc-4.9-multilib + - g++-4.9-multilib + - libgtk2.0-0 + - libx11-dev + - libxkbfile-dev + - libsecret-1-dev + - python-dev matrix: include: # # Use the built in venv for linux builds - # - os: linux - # sudo: required - # python: 2.7 - os: linux - sudo: required - python: 3.5 - # # Use generic language for osx + python: 2.7 + - os: linux + python: 3.6 + # # # Use generic language for osx # - os: osx # language: generic # env: PYTHON=2.7.10 # # Use generic language for osx - - os: osx - language: generic - env: PYTHON=3.5.1 -# Perform the manual steps on osx to install python3 and activate venv + # - os: osx + # language: generic + # env: PYTHON=3.6.1 +# Perform the manual steps on osx to install python 2.7.1 and 3.6.1 and set it as the global interpreter. +# This way when the node unit tests will pick the right version of python (from global) before_install: | if [ $TRAVIS_OS_NAME == "linux" ]; then export CXX="g++-4.9" CC="gcc-4.9" DISPLAY=:99.0; @@ -28,30 +41,18 @@ before_install: | git submodule update --init --recursive git clone https://github.com/creationix/nvm.git ./.nvm source ./.nvm/nvm.sh - nvm install 6.6.0 - nvm use 6.6.0 + nvm install 7.2.1 + nvm use 7.2.1 npm config set python `which python` if [ "$TRAVIS_OS_NAME" == "osx" ]; then - brew update; - brew install openssl readline - brew outdated pyenv || brew upgrade pyenv - brew install pyenv-virtualenv - brew install pyenv-virtualenvwrapper - pyenv install $PYTHON - pyenv virtualenv $PYTHON MYVERSION - source ~/.pyenv/versions/MYVERSION/bin/activate - python --version - python -c 'import sys;print(sys.version)' - python -c 'import sys;print(sys.executable)' + pyenv install $PYTHON + pyenv global $PYTHON fi + export TRAVIS_PYTHON_PATH=`which python` install: -# we have this here so we can see where python is installed and hardcode in our tests -# else when running npm test, the python version picked is completely different :( - - python --version - - python -c 'import sys;print(sys.executable)' - - pip install -r requirements.txt + - pip install --upgrade -r requirements.txt - npm install - npm run vscode:prepublish script: - - npm test --silent \ No newline at end of file + - npm test --silent diff --git a/.vscode/launch.json b/.vscode/launch.json index 47a3d47700d6..3cc9f6c671c6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,66 +1,79 @@ // A launch configuration that compiles the extension and then opens it inside a new window { - "version": "0.1.0", - "configurations": [ - { - "name": "Launch Extension", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": [ - "--extensionDevelopmentPath=${workspaceRoot}" - ], - "stopOnEntry": false, - "sourceMaps": true, - "outDir": "${workspaceRoot}/out", - "preLaunchTask": "npm" - }, - { - "name": "Launch Extension as debugServer", // https://code.visualstudio.com/docs/extensions/example-debuggers - "type": "node", - "request": "launch", - "runtimeArgs": [ - "--harmony" - ], - "program": "${workspaceRoot}/out/client/debugger/Main.js", - "stopOnEntry": false, - "args": [ - "--server=4711" - ], - "sourceMaps": true, - "outDir": "${workspaceRoot}/out/client", - "cwd": "${workspaceRoot}" - }, - { - "name": "Launch Tests", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": [ - "${workspaceRoot}/src/test", - "--extensionDevelopmentPath=${workspaceRoot}", - "--extensionTestsPath=${workspaceRoot}/out/test" - ], - "stopOnEntry": false, - "sourceMaps": true, - "outDir": "${workspaceRoot}/out/test", - "preLaunchTask": "npm" - }, - { - "name": "Python", - "type": "python", - "request": "launch", - "stopOnEntry": true, - "pythonPath": "python", - "program": "${file}", - "console": "integratedTerminal", - "args": [], - "debugOptions": [ - "WaitOnAbnormalExit", - "WaitOnNormalExit", - "RedirectOutput" - ], - "cwd": "${workspaceRoot}" - } - ] -} \ No newline at end of file + "version": "0.1.0", + "configurations": [ + { + "name": "Launch Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceRoot}" + ], + "stopOnEntry": false, + "sourceMaps": true, + "outFiles": [ + "${workspaceRoot}/out/**/*.js" + ], + "preLaunchTask": "compile" + }, + { + "name": "Launch Extension as debugServer", // https://code.visualstudio.com/docs/extensions/example-debuggers + "type": "node", + "request": "launch", + "program": "${workspaceRoot}/out/client/debugger/Main.js", + "stopOnEntry": false, + "args": [ + "--server=4711" + ], + "sourceMaps": true, + "outFiles": [ + "${workspaceRoot}/out/client/**/*.js" + ], + "cwd": "${workspaceRoot}" + }, + { + "name": "Launch Tests", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "${workspaceRoot}/src/test", + "--extensionDevelopmentPath=${workspaceRoot}", + "--extensionTestsPath=${workspaceRoot}/out/test" + ], + "stopOnEntry": false, + "sourceMaps": true, + "outFiles": [ + "${workspaceRoot}/out/**/*.js" + ], + "preLaunchTask": "compile" + }, + { + "name": "Launch Multiroot Tests", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "${workspaceRoot}/src/testMultiRootWkspc/multi.code-workspace", + "--extensionDevelopmentPath=${workspaceRoot}", + "--extensionTestsPath=${workspaceRoot}/out/test" + ], + "stopOnEntry": false, + "sourceMaps": true, + "outFiles": [ + "${workspaceRoot}/out/**/*.js" + ], + "preLaunchTask": "compile" + } + ], + "compounds": [ + { + "name": "Extension + Debugger", + "configurations": [ + "Launch Extension", + "Launch Extension as debugServer" + ] + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 9f53d84c3d2d..1884ba9a0255 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,16 +1,22 @@ // Place your settings in this file to overwrite default and user settings. { "files.exclude": { - "out": false, // set this to true to hide the "out" folder with the compiled JS files + "out": true, // set this to true to hide the "out" folder with the compiled JS files "**/*.pyc": true, - "**/__pycache__": true + "**/__pycache__": true, + "node_modules": true, + ".vscode-test": true, + "**/.mypy_cache/**": true, + "**/.ropeproject/**": true }, "search.exclude": { "out": true // set this to false to include "out" folder in search results }, "typescript.tsdk": "./node_modules/typescript/lib", // we want to use the TS server from our node_modules folder to control its version - "tslint.enable": true, + "tslint.enable": true, "python.linting.enabled": false, - "python.formatting.formatOnSave": false, - "python.unitTest.promptToConfigure": false -} \ No newline at end of file + "python.formatting.formatOnSave": false, + "python.unitTest.promptToConfigure": false, + "python.workspaceSymbols.enabled": false, + "python.formatting.provider": "yapf" +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index d31b15910eed..bfcb80a350f7 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -5,26 +5,66 @@ // ${fileDirname}: the current opened file's dirname // ${fileExtname}: the current opened file's extension // ${cwd}: the current working directory of the spawned process - // A task runner that calls a custom npm script that compiles the extension. { - "version": "0.1.0", - - // we want to run npm - "command": "npm", - - // the command is a shell script - "isShellCommand": true, - - // show the output window only if unrecognized errors occur. - "showOutput": "silent", - - // we run the custom script "compile" as defined in package.json - "args": ["run", "compile", "--loglevel", "silent"], - - // The tsc compiler is started in watching mode - "isWatching": true, - - // use the standard tsc in watch mode problem matcher to find compile problems in the output. - "problemMatcher": "$tsc-watch" -} \ No newline at end of file + "version": "2.0.0", + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "shared" + }, + "tasks": [ + { + "label": "compile", + "type": "npm", + "script": "compile", + "group": "build", + "isBackground": true, + "problemMatcher": [ + "$tsc", + { + "base": "$tslint5", + "fileLocation": "relative" + } + ] + }, + { + "type": "npm", + "script": "lint", + "problemMatcher": { + "base": "$tslint5", + "fileLocation": "relative" + } + }, + { + "type": "npm", + "script": "watch", + "isBackground": true, + "problemMatcher": "$tsc-watch" + }, + { + "type": "gulp", + "task": "hygiene", + "problemMatcher": [ + "$tsc", + { + "base": "$tslint5", + "fileLocation": "relative" + } + ] + }, + { + "label": "lint-staged", + "type": "npm", + "script": "lint-staged", + "problemMatcher": [ + "$tsc", + { + "base": "$tslint5", + "fileLocation": "relative" + } + ] + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f4453597250..db5d606bd4c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,186 @@ +### Version 0.8.0 (9 November 2017) +* Add support for multi-root workspaces [#1228](https://github.com/DonJayamanne/pythonVSCode/issues/1228), [#1302](https://github.com/DonJayamanne/pythonVSCode/pull/1302), [#1328](https://github.com/DonJayamanne/pythonVSCode/issues/1328), [#1357](https://github.com/DonJayamanne/pythonVSCode/pull/1357) +* Add code snippet for ```ipdb``` [#1141](https://github.com/DonJayamanne/pythonVSCode/pull/1141) +* Add ability to resolving environment variables in path to ```mypy``` [#1195](https://github.com/DonJayamanne/pythonVSCode/issues/1195) +* Add ability to disable a linter globally and disable prompts to install linters [#1207](https://github.com/DonJayamanne/pythonVSCode/issues/1207) +* Auto-selecting an interpreter from a virtual environment if only one is found in the root directory of the project [#1216](https://github.com/DonJayamanne/pythonVSCode/issues/1216) +* Add support for specifying the working directory for unit tests [#1155](https://github.com/DonJayamanne/pythonVSCode/issues/1155), [#1185](https://github.com/DonJayamanne/pythonVSCode/issues/1185) +* Add syntax highlighting of pip requirements files [#1247](https://github.com/DonJayamanne/pythonVSCode/pull/1247) +* Add ability to select an interpreter even when a workspace is not open [#1260](https://github.com/DonJayamanne/pythonVSCode/issues/1260), [#1263](https://github.com/DonJayamanne/pythonVSCode/pull/1263) +* Display a code lens to change the selected interpreter to the one specified in the shebang line [#1257](https://github.com/DonJayamanne/pythonVSCode/pull/1257), [#1263](https://github.com/DonJayamanne/pythonVSCode/pull/1263), [#1267](https://github.com/DonJayamanne/pythonVSCode/pull/1267), [#1280](https://github.com/DonJayamanne/pythonVSCode/issues/1280), [#1261](https://github.com/DonJayamanne/pythonVSCode/issues/1261), [#1290](https://github.com/DonJayamanne/pythonVSCode/pull/1290) +* Expand list of interpreters displayed for selection [#1147](https://github.com/DonJayamanne/pythonVSCode/issues/1147), [#1148](https://github.com/DonJayamanne/pythonVSCode/issues/1148), [#1224](https://github.com/DonJayamanne/pythonVSCode/pull/1224), [#1240](https://github.com/DonJayamanne/pythonVSCode/pull/1240) +* Display details of current or selected interpreter in statusbar [#1147](https://github.com/DonJayamanne/pythonVSCode/issues/1147), [#1217](https://github.com/DonJayamanne/pythonVSCode/issues/1217) +* Ensure paths in workspace symbols are not prefixed with ```.vscode``` [#816](https://github.com/DonJayamanne/pythonVSCode/issues/816), [#1066](https://github.com/DonJayamanne/pythonVSCode/pull/1066), [#829](https://github.com/DonJayamanne/pythonVSCode/issues/829) +* Ensure paths in ```PYTHONPATH``` environment variable are delimited using the OS-specific path delimiter [#832](https://github.com/DonJayamanne/pythonVSCode/issues/832) +* Ensure ```Rope``` is not packaged with the extension [#1208](https://github.com/DonJayamanne/pythonVSCode/issues/1208), [#1207](https://github.com/DonJayamanne/pythonVSCode/issues/1207), [#1243](https://github.com/DonJayamanne/pythonVSCode/pull/1243), [#1229](https://github.com/DonJayamanne/pythonVSCode/issues/1229) +* Ensure ctags are rebuilt as expected upon file save [#624](https://github.com/DonJayamanne/pythonVSCode/issues/1212) +* Ensure right test method is executed when two test methods exist with the same name in different classes [#1203](https://github.com/DonJayamanne/pythonVSCode/issues/1203) +* Ensure unit tests run successfully on Travis for both Python 2.7 and 3.6 [#1255](https://github.com/DonJayamanne/pythonVSCode/pull/1255), [#1241](https://github.com/DonJayamanne/pythonVSCode/issues/1241), [#1315](https://github.com/DonJayamanne/pythonVSCode/issues/1315) +* Fix building of ctags when a path contains a space [#1064](https://github.com/DonJayamanne/pythonVSCode/issues/1064), [#1144](https://github.com/DonJayamanne/pythonVSCode/issues/1144),, [#1213](https://github.com/DonJayamanne/pythonVSCode/pull/1213) +* Fix autocompletion in unsaved Python files [#1194](https://github.com/DonJayamanne/pythonVSCode/issues/1194) +* Fix running of test methods in nose [#597](https://github.com/DonJayamanne/pythonVSCode/issues/597), [#1225](https://github.com/DonJayamanne/pythonVSCode/pull/1225) +* Fix to disable linting of diff windows [#1221](https://github.com/DonJayamanne/pythonVSCode/issues/1221), [#1244](https://github.com/DonJayamanne/pythonVSCode/pull/1244) +* Fix docstring formatting [#1188](https://github.com/DonJayamanne/pythonVSCode/issues/1188) +* Fix to ensure language features can run in parallel without interference with one another [#1314](https://github.com/DonJayamanne/pythonVSCode/issues/1314), [#1318](https://github.com/DonJayamanne/pythonVSCode/pull/1318) +* Fix to ensure unit tests can be debugged more than once per run [#948](https://github.com/DonJayamanne/pythonVSCode/issues/948), [#1353](https://github.com/DonJayamanne/pythonVSCode/pull/1353) +* Fix to ensure parameterized unit tests can be debugged [#1284](https://github.com/DonJayamanne/pythonVSCode/issues/1284), [#1299](https://github.com/DonJayamanne/pythonVSCode/pull/1299) +* Fix issue that causes debugger to freeze/hang [#1041](https://github.com/DonJayamanne/pythonVSCode/issues/1041), [#1354](https://github.com/DonJayamanne/pythonVSCode/pull/1354) +* Fix to support unicode characters in Python tests [#1282](https://github.com/DonJayamanne/pythonVSCode/issues/1282), [#1291](https://github.com/DonJayamanne/pythonVSCode/pull/1291) +* Changes as a result of VS Code API changes [#1270](https://github.com/DonJayamanne/pythonVSCode/issues/1270), [#1288](https://github.com/DonJayamanne/pythonVSCode/pull/1288), [#1372](https://github.com/DonJayamanne/pythonVSCode/issues/1372), [#1300](https://github.com/DonJayamanne/pythonVSCode/pull/1300), [#1298](https://github.com/DonJayamanne/pythonVSCode/issues/1298) +* Updates to Readme [#1212](https://github.com/DonJayamanne/pythonVSCode/issues/1212), [#1222](https://github.com/DonJayamanne/pythonVSCode/issues/1222) +* Fix executing a command under PowerShell [#1098](https://github.com/DonJayamanne/pythonVSCode/issues/1098) + + +### Version 0.7.0 (3 August 2017) +* Displaying internal documentation [#1008](https://github.com/DonJayamanne/pythonVSCode/issues/1008), [#10860](https://github.com/DonJayamanne/pythonVSCode/issues/10860) +* Fixes to 'async with' snippet [#1108](https://github.com/DonJayamanne/pythonVSCode/pull/1108), [#996](https://github.com/DonJayamanne/pythonVSCode/issues/996) +* Add support for environment variable in unit tests [#1074](https://github.com/DonJayamanne/pythonVSCode/issues/1074) +* Fixes to unit test code lenses not being displayed [#1115](https://github.com/DonJayamanne/pythonVSCode/issues/1115) +* Fix to empty brackets being added [#1110](https://github.com/DonJayamanne/pythonVSCode/issues/1110), [#1031](https://github.com/DonJayamanne/pythonVSCode/issues/1031) +* Fix debugging of Django applications [#819](https://github.com/DonJayamanne/pythonVSCode/issues/819), [#999](https://github.com/DonJayamanne/pythonVSCode/issues/999) +* Update isort to the latest version [#1134](https://github.com/DonJayamanne/pythonVSCode/issues/1134), [#1135](https://github.com/DonJayamanne/pythonVSCode/pull/1135) +* Fix issue causing intellisense and similar functionality to stop working [#1072](https://github.com/DonJayamanne/pythonVSCode/issues/1072), [#1118](https://github.com/DonJayamanne/pythonVSCode/pull/1118), [#1089](https://github.com/DonJayamanne/pythonVSCode/issues/1089) +* Bunch of unit tests and code cleanup +* Resolve issue where navigation to decorated function goes to decorator [#742](https://github.com/DonJayamanne/pythonVSCode/issues/742) +* Go to symbol in workspace leads to nonexisting files [#816](https://github.com/DonJayamanne/pythonVSCode/issues/816), [#829](https://github.com/DonJayamanne/pythonVSCode/issues/829) + +### Version 0.6.9 (22 July 2017) +* Fix to enure custom linter paths are respected [#1106](https://github.com/DonJayamanne/pythonVSCode/issues/1106) + +### Version 0.6.8 (20 July 2017) +* Add new editor menu 'Run Current Unit Test File' [#1061](https://github.com/DonJayamanne/pythonVSCode/issues/1061) +* Changed 'mypy-lang' to mypy [#930](https://github.com/DonJayamanne/pythonVSCode/issues/930), [#998](https://github.com/DonJayamanne/pythonVSCode/issues/998), [#505](https://github.com/DonJayamanne/pythonVSCode/issues/505) +* Using "Python -m" to launch linters [#716](https://github.com/DonJayamanne/pythonVSCode/issues/716), [#923](https://github.com/DonJayamanne/pythonVSCode/issues/923), [#1059](https://github.com/DonJayamanne/pythonVSCode/issues/1059) +* Add PEP 526 AutoCompletion [#1102](https://github.com/DonJayamanne/pythonVSCode/pull/1102), [#1101](https://github.com/DonJayamanne/pythonVSCode/issues/1101) +* Resolved issues in Go To and Peek Definitions [#1085](https://github.com/DonJayamanne/pythonVSCode/pull/1085), [#961](https://github.com/DonJayamanne/pythonVSCode/issues/961), [#870](https://github.com/DonJayamanne/pythonVSCode/issues/870) + +### Version 0.6.7 (02 July 2017) +* Updated icon from jpg to png (transparent background) + +### Version 0.6.6 (02 July 2017) +* Provide details of error with solution for changes to syntax in launch.json [#1047](https://github.com/DonJayamanne/pythonVSCode/issues/1047), [#1025](https://github.com/DonJayamanne/pythonVSCode/issues/1025) +* Provide a warning about known issues with having pyenv.cfg whilst debugging [#913](https://github.com/DonJayamanne/pythonVSCode/issues/913) +* Create .vscode directory if not found [#1043](https://github.com/DonJayamanne/pythonVSCode/issues/1043) +* Highlighted text due to linter errors is off by one column [#965](https://github.com/DonJayamanne/pythonVSCode/issues/965), [#970](https://github.com/DonJayamanne/pythonVSCode/pull/970) +* Added preminary support for WSL Bash and Cygwin [#1049](https://github.com/DonJayamanne/pythonVSCode/pull/1049) +* Ability to configure the linter severity levels [#941](https://github.com/DonJayamanne/pythonVSCode/pull/941), [#895](https://github.com/DonJayamanne/pythonVSCode/issues/895) +* Fixes to unit tests [#1051](https://github.com/DonJayamanne/pythonVSCode/pull/1051), [#1050](https://github.com/DonJayamanne/pythonVSCode/pull/1050) +* Outdent lines following `contibue`, `break` and `return` [#1050](https://github.com/DonJayamanne/pythonVSCode/pull/1050) +* Change location of cache for Jedi files [#1035](https://github.com/DonJayamanne/pythonVSCode/pull/1035) +* Fixes to the way directories are searched for Python interpreters [#569](https://github.com/DonJayamanne/pythonVSCode/issues/569), [#1040](https://github.com/DonJayamanne/pythonVSCode/pull/1040) +* Handle outputs from Python packages that interfere with the way autocompletion is handled [#602](https://github.com/DonJayamanne/pythonVSCode/issues/602) + +### Version 0.6.5 (13 June 2017) +* Fix error in launch.json [#1006](https://github.com/DonJayamanne/pythonVSCode/issues/1006) +* Detect current workspace interpreter when selecting interpreter [#1006](https://github.com/DonJayamanne/pythonVSCode/issues/979) +* Disable output buffering when debugging [#1005](https://github.com/DonJayamanne/pythonVSCode/issues/1005) +* Updated snippets to use correct placeholder syntax [#976](https://github.com/DonJayamanne/pythonVSCode/pull/976) +* Fix hover and auto complete unit tests [#1012](https://github.com/DonJayamanne/pythonVSCode/pull/1012) +* Fix hover definition variable test for Python 3.5 [#1013](https://github.com/DonJayamanne/pythonVSCode/pull/1013) +* Better formatting of docstring [#821](https://github.com/DonJayamanne/pythonVSCode/pull/821), [#919](https://github.com/DonJayamanne/pythonVSCode/pull/919) +* Supporting more paths when searching for Python interpreters [#569](https://github.com/DonJayamanne/pythonVSCode/issues/569) +* Increase buffer output (to support detection large number of tests) [#927](https://github.com/DonJayamanne/pythonVSCode/issues/927) + +### Version 0.6.4 (4 May 2017) +* Fix dates in changelog [#899](https://github.com/DonJayamanne/pythonVSCode/pull/899) +* Using charriage return or line feeds to split a document into multiple lines [#917](https://github.com/DonJayamanne/pythonVSCode/pull/917), [#821](https://github.com/DonJayamanne/pythonVSCode/issues/821) +* Doc string not being displayed [#888](https://github.com/DonJayamanne/pythonVSCode/issues/888) +* Supporting paths that begin with the ~/ [#909](https://github.com/DonJayamanne/pythonVSCode/issues/909) +* Supporting more paths when searching for Python interpreters [#569](https://github.com/DonJayamanne/pythonVSCode/issues/569) +* Supporting ~/ paths when providing the path to ctag file [#910](https://github.com/DonJayamanne/pythonVSCode/issues/910) +* Disable linting of python files opened in diff viewer [#896](https://github.com/DonJayamanne/pythonVSCode/issues/896) +* Added a new command ```Go to Python Object``` [#928](https://github.com/DonJayamanne/pythonVSCode/issues/928) +* Restored the menu item to rediscover tests [#863](https://github.com/DonJayamanne/pythonVSCode/issues/863) +* Changes to rediscover tests when test files are altered and saved [#863](https://github.com/DonJayamanne/pythonVSCode/issues/863) + +### Version 0.6.3 (19 April 2017) +* Fix debugger issue [#893](https://github.com/DonJayamanne/pythonVSCode/issues/893) +* Improvements to debugging unit tests (check if string starts with, instead of comparing equality) [#797](https://github.com/DonJayamanne/pythonVSCode/issues/797) + +### Version 0.6.2 (13 April 2017) +* Fix incorrect indenting [#880](https://github.com/DonJayamanne/pythonVSCode/issues/880) + +#### Thanks +* [Yuwei Ba](https://github.com/ibigbug) + +### Version 0.6.1 (10 April 2017) +* Add support for new variable syntax in upcoming VS Code release [#774](https://github.com/DonJayamanne/pythonVSCode/issues/774), [#855](https://github.com/DonJayamanne/pythonVSCode/issues/855), [#873](https://github.com/DonJayamanne/pythonVSCode/issues/873), [#823](https://github.com/DonJayamanne/pythonVSCode/issues/823) +* Resolve issues in code refactoring [#802](https://github.com/DonJayamanne/pythonVSCode/issues/802), [#824](https://github.com/DonJayamanne/pythonVSCode/issues/824), [#825](https://github.com/DonJayamanne/pythonVSCode/pull/825) +* Changes to labels in Python Interpreter lookup [#815](https://github.com/DonJayamanne/pythonVSCode/pull/815) +* Resolve Typos [#852](https://github.com/DonJayamanne/pythonVSCode/issues/852) +* Use fully qualitified Python Path when installing dependencies [#866](https://github.com/DonJayamanne/pythonVSCode/issues/866) +* Commands for running tests from a file [#502](https://github.com/DonJayamanne/pythonVSCode/pull/502) +* Fix Sorting of imports when path contains spaces [#811](https://github.com/DonJayamanne/pythonVSCode/issues/811) +* Fixing occasional failure of linters [#793](https://github.com/DonJayamanne/pythonVSCode/issues/793), [#833](https://github.com/DonJayamanne/pythonVSCode/issues/838), [#860](https://github.com/DonJayamanne/pythonVSCode/issues/860) +* Added ability to pre-load some modules to improve autocompletion [#581](https://github.com/DonJayamanne/pythonVSCode/issues/581) + +#### Thanks +* [Ashwin Mathews](https://github.com/ajmathews) +* [Alexander Ioannidis](https://github.com/slint) +* [Andreas Schlapsi](https://github.com/aschlapsi) + +### Version 0.6.0 (10 March 2017) +* Moved Jupyter functionality into a separate extension [Jupyter]() +* Updated readme [#779](https://github.com/DonJayamanne/pythonVSCode/issues/779) +* Changing default arguments of ```mypy``` [#658](https://github.com/DonJayamanne/pythonVSCode/issues/658) +* Added ability to disable formatting [#559](https://github.com/DonJayamanne/pythonVSCode/issues/559) +* Fixing ability to run a Python file in a terminal [#784](https://github.com/DonJayamanne/pythonVSCode/issues/784) +* Added support for Proxy settings when installing Python packages using Pip [#778](https://github.com/DonJayamanne/pythonVSCode/issues/778) + +### Version 0.5.9 (3 March 2017) +* Fixed navigating to definitions [#711](https://github.com/DonJayamanne/pythonVSCode/issues/711) +* Support auto detecting binaries from Python Path [#716](https://github.com/DonJayamanne/pythonVSCode/issues/716) +* Setting PYTHONPATH environment variable [#686](https://github.com/DonJayamanne/pythonVSCode/issues/686) +* Improving Linter performance, killing redundant processes [4a8319e](https://github.com/DonJayamanne/pythonVSCode/commit/4a8319e0859f2d49165c9a08fe147a647d03ece9) +* Changed default path of the CATAS file to `.vscode/tags` [#722](https://github.com/DonJayamanne/pythonVSCode/issues/722) +* Add parsing severity level for flake8 and pep8 linters [#709](https://github.com/DonJayamanne/pythonVSCode/pull/709) +* Fix to restore function descriptions (intellisense) [#727](https://github.com/DonJayamanne/pythonVSCode/issues/727) +* Added default configuration for debugging Pyramid [#287](https://github.com/DonJayamanne/pythonVSCode/pull/287) +* Feature request: Run current line in Terminal [#738](https://github.com/DonJayamanne/pythonVSCode/issues/738) +* Miscellaneous improvements to hover provider [6a7a3f3](https://github.com/DonJayamanne/pythonVSCode/commit/6a7a3f32ab8add830d13399fec6f0cdd14cd66fc), [6268306](https://github.com/DonJayamanne/pythonVSCode/commit/62683064d01cfc2b76d9be45587280798a96460b) +* Fixes to rename refactor (due to 'LF' EOL in Windows) [#748](https://github.com/DonJayamanne/pythonVSCode/pull/748) +* Fixes to ctag file being generated in home folder when no workspace is opened [#753](https://github.com/DonJayamanne/pythonVSCode/issues/753) +* Fixes to ctag file being generated in home folder when no workspace is opened [#753](https://github.com/DonJayamanne/pythonVSCode/issues/753) +* Disabling auto-completion in single line comments [#74](https://github.com/DonJayamanne/pythonVSCode/issues/74) +* Fixes to debugging of modules [#518](https://github.com/DonJayamanne/pythonVSCode/issues/518) +* Displaying unit test status icons against unit test code lenses [#678](https://github.com/DonJayamanne/pythonVSCode/issues/678) +* Fix issue where causing 'python.python-debug.startSession' not found message to be displayed when debugging single file [#708](https://github.com/DonJayamanne/pythonVSCode/issues/708) +* Ability to include packages directory when generating tags file [#735](https://github.com/DonJayamanne/pythonVSCode/issues/735) +* Fix issue where running selected text in terminal does not work [#758](https://github.com/DonJayamanne/pythonVSCode/issues/758) +* Fix issue where disabling linter doesn't disable it (when no workspace is open) [#763](https://github.com/DonJayamanne/pythonVSCode/issues/763) +* Search additional directories for Python Interpreters (~/.virtualenvs, ~/Envs, ~/.pyenv) [#569](https://github.com/DonJayamanne/pythonVSCode/issues/569) +* Added ability to pre-load some modules to improve autocompletion [#581](https://github.com/DonJayamanne/pythonVSCode/issues/581) +* Removed invalid default value in launch.json file [#586](https://github.com/DonJayamanne/pythonVSCode/issues/586) +* Added ability to configure the pylint executable path [#766](https://github.com/DonJayamanne/pythonVSCode/issues/766) +* Fixed single file debugger to ensure the Python interpreter configured in python.PythonPath is being used [#769](https://github.com/DonJayamanne/pythonVSCode/issues/769) + +### Version 0.5.8 (3 February 2017) +* Fixed a bug in [debugging single files without a launch configuration](https://code.visualstudio.com/updates/v1_9#_debugging-without-a-launch-configuration) [#700](https://github.com/DonJayamanne/pythonVSCode/issues/700) +* Fixed error when starting REPL [#692](https://github.com/DonJayamanne/pythonVSCode/issues/692) + +### Version 0.5.7 (3 February 2017) +* Added support for [debugging single files without a launch configuration](https://code.visualstudio.com/updates/v1_9#_debugging-without-a-launch-configuration) +* Adding support for debug snippets [#660](https://github.com/DonJayamanne/pythonVSCode/issues/660) +* Ability to run a selected text in a Django shell [#652](https://github.com/DonJayamanne/pythonVSCode/issues/652) +* Adding support for the use of a customized 'isort' for sorting of imports [#632](https://github.com/DonJayamanne/pythonVSCode/pull/632) +* Debuger auto-detecting python interpreter from the path provided [#688](https://github.com/DonJayamanne/pythonVSCode/issues/688) +* Showing symbol type on hover [#657](https://github.com/DonJayamanne/pythonVSCode/pull/657) +* Fixes to running Python file when terminal uses Powershell [#651](https://github.com/DonJayamanne/pythonVSCode/issues/651) +* Fixes to linter issues when displaying Git diff view for Python files [#665](https://github.com/DonJayamanne/pythonVSCode/issues/665) +* Fixes to 'Go to definition' functionality [#662](https://github.com/DonJayamanne/pythonVSCode/issues/662) +* Fixes to Jupyter cells numbered larger than '10' [#681](https://github.com/DonJayamanne/pythonVSCode/issues/681) + +### Version 0.5.6 (16 January 2017) +* Added support for Python 3.6 [#646](https://github.com/DonJayamanne/pythonVSCode/issues/646), [#631](https://github.com/DonJayamanne/pythonVSCode/issues/631), [#619](https://github.com/DonJayamanne/pythonVSCode/issues/619), [#613](https://github.com/DonJayamanne/pythonVSCode/issues/613) +* Autodetect in python path in virtual environments [#353](https://github.com/DonJayamanne/pythonVSCode/issues/353) +* Add syntax highlighting of code samples in hover defintion [#555](https://github.com/DonJayamanne/pythonVSCode/issues/555) +* Launch REPL for currently selected interpreter [#560](https://github.com/DonJayamanne/pythonVSCode/issues/560) +* Fixes to debugging of modules [#589](https://github.com/DonJayamanne/pythonVSCode/issues/589) +* Reminder to install jedi and ctags in Quick Start [#642](https://github.com/DonJayamanne/pythonVSCode/pull/642) +* Improvements to Symbol Provider [#622](https://github.com/DonJayamanne/pythonVSCode/pull/622) +* Changes to disable unit test prompts for workspace [#559](https://github.com/DonJayamanne/pythonVSCode/issues/559) +* Minor fixes [#627](https://github.com/DonJayamanne/pythonVSCode/pull/627) + ### Version 0.5.5 (25 November 2016) * Fixes to debugging of unittests (nose and pytest) [#543](https://github.com/DonJayamanne/pythonVSCode/issues/543) * Fixes to debugging of Django [#546](https://github.com/DonJayamanne/pythonVSCode/issues/546) @@ -312,4 +495,5 @@ * Added ability to view global variables, arguments, add and remove break points ## Version 0.0.3 -* Added support for debugging using PDB \ No newline at end of file +* Added support for debugging using PDB + diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000000..18873b3ccf8a --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,6 @@ +This project has adopted the [Microsoft Open Source Code of +Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct +FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) +with any additional questions or comments. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b57d3166e19c..cff2b12e9b1a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,10 +1,10 @@ -##Contribution +## Contribution * Please feel free to fork and submit pull requests * Feature requests can be added [here](https://github.com/DonJayamanne/pythonVSCode/issues/183) ## Prerequisites 1. Node.js -2. Python v2.6 or later (required only for testing the extension and running unit tests) +2. Python 2.7 or later (required only for testing the extension and running unit tests) 3. Windows, OS X or Linux ## Setup @@ -20,23 +20,23 @@ Run the build Task from the [Command Palette](https://code.visualstudio.com/docs ### Errors and Warnings TypeScript errors and warnings will be displayed in VS Code in the Problems Panel (CTRL+SHIFT+M or โ‡งโŒ˜M) -###Validate your changes +### Validate your changes To test the changes you launch a development version of VS Code on the workspace vscode, which you are currently editing. Use the "Launch Extension" launch option. -###Unit Tests -Run the Unit Tests via the "Launch Test" launch option. +### Unit Tests +Run the Unit Tests via the "Launch Test" launch option. Currently unit tests only run on [Travis](https://travis-ci.org/DonJayamanne/pythonVSCode) -_Requirements_ -1. Ensure you have disabled breaking into 'Uncaught Exceptions' when running the Unit Tests -2. For the linters and formatters tests to pass successfully, you will need to have those corresponding Python libraries installed locally +_Requirements_ +1. Ensure you have disabled breaking into 'Uncaught Exceptions' when running the Unit Tests +2. For the linters and formatters tests to pass successfully, you will need to have those corresponding Python libraries installed locally -##Debugging the extension -###Standard Debugging +## Debugging the extension +### Standard Debugging Clone the repo into any directory and start debugging. -From there use the "Launch Extension" launch option. +From there use the "Launch Extension" launch option. -###Debugging the Python Extension Debugger -The easiest way to debug the Python Debugger (in my opinion) is to clone this git repo directory into [your](https://code.visualstudio.com/docs/extensions/install-extension#_your-extensions-folder) extensions directory. +### Debugging the Python Extension Debugger +The easiest way to debug the Python Debugger (in our opinion) is to clone this git repo directory into [your](https://code.visualstudio.com/docs/extensions/install-extension#_your-extensions-folder) extensions directory. From there use the ```Launch Extension as debugserver``` launch option. diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index dfcad4caa6fd..4a59941bce08 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -3,19 +3,6 @@ VS Code version: Python Extension version: Python Version: OS and version: -Your launch.json (if dealing with debugger issues): -```json -``` -Your settings.json: -```json -``` -## Logs -Output from ```Python``` output panel -``` -``` -Output from ```Console window``` (Help->Developer Tools menu) -``` -``` ## Actual behavior @@ -24,3 +11,11 @@ Output from ```Console window``` (Help->Developer Tools menu) ## Steps to reproduce: - - + +## Logs +Output from ```Python``` output panel +``` +``` +Output from ```Console window``` (Help->Developer Tools menu) +``` +``` diff --git a/LICENSE b/LICENSE index 67734473d340..8cb179cdb694 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ -The MIT License (MIT) +Copyright (c) Microsoft Corporation. All rights reserved. -Copyright (c) 2015 DonJayamanne +MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -12,7 +12,7 @@ furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER diff --git a/README.md b/README.md index fff9b235ab87..a62f748b12f2 100644 --- a/README.md +++ b/README.md @@ -1,161 +1,88 @@ -# Python +# Python extension for Visual Studio Code -An extension with rich support for the [Python language](https://www.python.org/), with features including the following and more: -* Linting ([Prospector](https://pypi.io/project/prospector/), [Pylint](https://pypi.io/project/pylint/), [pycodestyle](https://pypi.io/project/pycodestyle/)/Pep8, [Flake8](https://pypi.io/project/flake8/), [pylama](https://github.com/klen/pylama), [pydocstyle](https://pypi.io/project/pydocstyle/) with config files and plugins) -* Intellisense (autocompletion with support for PEP-0484) -* PySpark and Scientific tools (Jupyter/IPython) +A [Visual Studio Code](https://code.visualstudio.com/) [extension](https://marketplace.visualstudio.com/VSCode) with rich support for the [Python language](https://www.python.org/) (_including Python 3.6_), with features including the following and more: +* Linting ([Prospector](https://pypi.io/project/prospector/), [Pylint](https://pypi.io/project/pylint/), [pycodestyle](https://pypi.io/project/pycodestyle/), [Flake8](https://pypi.io/project/flake8/), [pylama](https://github.com/klen/pylama), [pydocstyle](https://pypi.io/project/pydocstyle/), [mypy](http://mypy-lang.org/) with config files and plugins) +* Intellisense (autocompletion with support for PEP 484 and PEP 526) * Auto indenting * Code formatting ([autopep8](https://pypi.io/project/autopep8/), [yapf](https://pypi.io/project/yapf/), with config files) -* Code refactoring ([Rename](https://github.com/DonJayamanne/pythonVSCode/wiki/Refactoring:-Rename), [Extract Variable](https://github.com/DonJayamanne/pythonVSCode/wiki/Refactoring:-Extract-Variable), [Extract Method](https://github.com/DonJayamanne/pythonVSCode/wiki/Refactoring:-Extract-Method), [Sort Imports](https://github.com/DonJayamanne/pythonVSCode/wiki/Refactoring:-Sort-Imports)) +* Code refactoring (Rename, Extract Variable, Extract Method,Sort Imports) * Viewing references, code navigation, view signature * Excellent debugging support (remote debugging over SSH, mutliple threads, django, flask) -* Running and debugging Unit tests ([unittest](https://docs.python.org/3/library/unittest.html#module-unittest), [pytest](https://pypi.io/project/pytest/), [nosetests](https://pypi.io/project/nose/), with config files) +* Running and debugging Unit tests ([unittest](https://docs.python.org/3/library/unittest.html#module-unittest), [pytest](https://pypi.io/project/pytest/), [nose](https://pypi.io/project/nose/), with config files) * Execute file or code in a python terminal * Local help file (offline documentation) * Snippets ## Quick Start * Install the extension -* If Python is in the current path - + You're ready to use it. -* If using a custom Python Version or a Virtual Environment, use the command [```Select Workspace Interpreter```](https://github.com/DonJayamanne/pythonVSCode/wiki/Miscellaneous#select-an-interpreter)) +* optionally install `ctags` for Workspace Symbols, from [here](http://ctags.sourceforge.net/), or using `brew install ctags` on macOS +* Select your Python interpreter + + If it's already in your path then you're set + + Otherwise, to select a different Python Interpreter/Version (or use a virtual environment), use the command `Select Workspace Interpreter`) -## [Documentation](https://github.com/DonJayamanne/pythonVSCode/wiki) -For further information and details continue through to the [documentation](https://github.com/DonJayamanne/pythonVSCode/wiki). +## [Documentation](https://code.visualstudio.com/docs/languages/python) +For further information and details continue through to the [documentation](https://code.visualstudio.com/docs/languages/python). -## [Issues, Feature Requests and Contributions](https://github.com/DonJayamanne/pythonVSCode/issues) -* Contributions are always welcome. Fork it, modify it and create a pull request. - + Details on contributing can be found [here](https://github.com/DonJayamanne/pythonVSCode/wiki/Contribution) -* Any and all feedback is appreciated and welcome. - + Please feel free to [add suggestions here](https://github.com/DonJayamanne/pythonVSCode/issues/183) +## Issues, Feature Requests, and Contributions +* If you come across a problem with the extension please [file an issue](https://github.com/microsoft/vscode-python) +* Contributions are always welcome! Please see our [contributing guide](https://github.com/Microsoft/vscode-python/blob/master/CONTRIBUTING.md) for more details +* Any and all feedback is appreciated and welcome! + - If someone has already [file an issue](https://github.com/Microsoft/vscode-python) that encompasses your feedback, please leave a ๐Ÿ‘/๐Ÿ‘Ž reaction on the issue + - Othewise please file a new issue ## Feature Details -* IDE Features - + Auto indenting - + Code navigation (Go to, Find all references) - + Code definition (Peek and hover definition, View Signature) - + Rename refactoring - + Sorting Import statements (use "Python: Sort Imports" command) -* [Intellisense and Autocomplete](https://github.com/DonJayamanne/pythonVSCode/wiki/Autocomplete-Intellisense) with support for PEP-0484 - + Ability to include custom module paths (e.g. include paths for libraries like Google App Engine, etc.) - + Use the `setting python.autoComplete.extraPaths = []` - + For instance getting autocomplete/intellisense for Google App Engine, add the following to your settings file: -```json -"python.autoComplete.extraPaths": [ - "C:/Program Files (x86)/Google/google_appengine", - "C:/Program Files (x86)/Google/google_appengine/lib" ] -``` -* [Scientific tools (Jupyter/IPython)](https://github.com/DonJayamanne/pythonVSCode/wiki/Jupyter-(IPython)) - + Executing blocks of code (cells) in a Jupyter Kernel - + Managing kernels (restarting, stopping, interrupting and selecting different kernels) - + Viewing interactive graphs, HTML, SVG, LaTeX output from Jupyter from within Visual Studio Code -* [Code formatting](https://github.com/DonJayamanne/pythonVSCode/wiki/Formatting) - + Auto formatting of code upon saving changes (default to 'Off') - + Use either yapf or autopep8 for code formatting (defaults to autopep8) -* [Linting](https://github.com/DonJayamanne/pythonVSCode/wiki/Linting) - + It can be turned off (default is to be turned on and use pylint) - + Multiple linters supported (along with support for configuration files for each linter) - + Supported linters include pylint, pep8, flake8, pydocstyle, prospector - + Paths to each of the linters can be optionally configured - + Custom plugins such as pylint plugin for Django can be easily used by modifying the settings as follows: -```json -"python.linting.pylintArgs": ["--load-plugins", "pylint_django"] -``` -* [Debugging](https://github.com/DonJayamanne/pythonVSCode/wiki/Debugging) - + Watch window - + Evaluate Expressions - + Step through code (Step in, Step out, Continue) - + Add/remove break points - + Local variables and arguments - + Multiple Threads and Web Applications (such as Flask, Django, with template debugging) - + Expanding values (viewing children, properties, etc) - + Conditional break points - + Remote debugging (over SSH) - + Google App Engine - + Debugging in the integrated or external terminal window - * Debugging as sudo -* [Unit Testing](https://github.com/DonJayamanne/pythonVSCode/wiki/UnitTests) - + Support for unittests, nosetests and pytest - + Test results are displayed in the "Python" output window - + Run failed tests, individual tests - + Debugging unittests +* IDE-like Features + + Automatic indenting + + Code navigation ("Go to", "Find all" references) + + Code definition (Peek and hover definition, View signatures) + + Rename refactoring + + Sorting import statements (use the `Python: Sort Imports` command) +* Intellisense and Autocomplete (including PEP 484 support) + + Ability to include custom module paths (e.g. include paths for libraries like Google App Engine, etc.; use the setting `python.autoComplete.extraPaths = []`) +* Code formatting + + Auto formatting of code upon saving changes (default to 'Off') + + Use either [yapf](https://pypi.io/project/yapf/) or [autopep8](https://pypi.io/project/autopep8/) for code formatting (defaults to autopep8) +* Linting + + Support for multiple linters with custom settings (default is [Pylint](https://pypi.io/project/pylint/), but [Prospector](https://pypi.io/project/prospector/), [pycodestyle](https://pypi.io/project/pycodestyle/), [Flake8](https://pypi.io/project/flake8/), [pylama](https://github.com/klen/pylama), [pydocstyle](https://pypi.io/project/pydocstyle/), and [mypy](http://mypy-lang.org/) are also supported) +* Debugging + + Watch window + + Evaluate Expressions + + Step through code ("Step in", "Step out", "Continue") + + Add/remove break points + + Local variables and arguments + + Multi-threaded applications + + Web applications (such as [Flask](http://flask.pocoo.org/) & [Django](https://www.djangoproject.com/), with template debugging) + + Expanding values (viewing children, properties, etc) + + Conditional break points + + Remote debugging (over SSH) + + Google App Engine + + Debugging in the integrated or external terminal window + + Debugging as sudo +* Unit Testing + + Support for [unittest](https://docs.python.org/3/library/unittest.html#module-unittest), [pytest](https://pypi.io/project/pytest/), and [nose](https://pypi.io/project/nose/) + + Ability to run all failed tests, individual tests + + Debugging unit tests * Snippets * Miscellaneous - + Running a file or selected text in python terminal + + Running a file or selected text in python terminal * Refactoring - + [Rename Refactorings](https://github.com/DonJayamanne/pythonVSCode/wiki/Refactoring:-Rename) - + [Extract Variable Refactorings](https://github.com/DonJayamanne/pythonVSCode/wiki/Refactoring:-Extract-Variable) - + [Extract Method Refactorings](https://github.com/DonJayamanne/pythonVSCode/wiki/Refactoring:-Extract-Method) - + [Sort Imports](https://github.com/DonJayamanne/pythonVSCode/wiki/Refactoring:-Sort-Imports) + + Rename Refactorings + + Extract Variable Refactorings + + Extract Method Refactorings + + Sort Imports -![Generate Features](https://raw.githubusercontent.com/DonJayamanne/pythonVSCodeDocs/master/images/general.gif) +![General Features](https://raw.githubusercontent.com/microsoft/vscode-python/master/images/general.gif) -![Debugging](https://raw.githubusercontent.com/DonJayamanne/pythonVSCodeDocs/master/images/debugDemo.gif) +![Debugging](https://raw.githubusercontent.com/microsoft/vscode-python/master/images/debugDemo.gif) -![Unit Tests](https://raw.githubusercontent.com/DonJayamanne/pythonVSCodeDocs/master/images/unittest.gif) +![Unit Tests](https://raw.githubusercontent.com/microsoft/vscode-python/master/images/unittest.gif) -![Scientific Tools](https://raw.githubusercontent.com/DonJayamanne/pythonVSCodeDocs/master/images/jupyter/examples.gif) -![Local Help](https://raw.githubusercontent.com/DonJayamanne/pythonVSCodeDocs/master/images/help.gif) - -## [Roadmap](https://donjayamanne.github.io/pythonVSCodeDocs/docs/roadmap/) - -## [Change Log](https://github.com/DonJayamanne/pythonVSCode/blob/master/CHANGELOG.md) -### Version 0.5.5 (25 November 2016) -* Fixes to debugging of unittests (nose and pytest) [#543](https://github.com/DonJayamanne/pythonVSCode/issues/543) -* Fixes to debugging of Django [#546](https://github.com/DonJayamanne/pythonVSCode/issues/546) - -### Version 0.5.4 (24 November 2016) -* Fixes to installing missing packages [#544](https://github.com/DonJayamanne/pythonVSCode/issues/544) -* Fixes to indentation of blocks of code [#432](https://github.com/DonJayamanne/pythonVSCode/issues/432) -* Fixes to debugging of unittests [#543](https://github.com/DonJayamanne/pythonVSCode/issues/543) -* Fixes to extension when a workspace (folder) isn't open [#542](https://github.com/DonJayamanne/pythonVSCode/issues/542) - -### Version 0.5.3 (23 November 2016) -* Added support for [PySpark](http://spark.apache.org/docs/0.9.0/python-programming-guide.html) [#539](https://github.com/DonJayamanne/pythonVSCode/pull/539), [#540](https://github.com/DonJayamanne/pythonVSCode/pull/540) -* Debugging unittests (UnitTest, pytest, nose) [#333](https://github.com/DonJayamanne/pythonVSCode/issues/333) -* Displaying progress for formatting [#327](https://github.com/DonJayamanne/pythonVSCode/issues/327) -* Prefixing new lines with '#' when new lines are added in the middle of a comment string [#365](https://github.com/DonJayamanne/pythonVSCode/issues/365) -* Debugging python modules [#518](https://github.com/DonJayamanne/pythonVSCode/issues/518), [#354](https://github.com/DonJayamanne/pythonVSCode/issues/354) - + Use new debug configuration ```Python Module``` -* Added support for workspace symbols using Exuberant CTags [#138](https://github.com/DonJayamanne/pythonVSCode/issues/138) - + New command ```Python: Build Workspace Symbols``` -* Auto indenting ```else:``` inside ```if``` and similar code blocks [#432](https://github.com/DonJayamanne/pythonVSCode/issues/432) - + Add the following setting in user ```settings.json``` -```jsong - "editor.formatOnType": true, -``` -* Added ability for linter to ignore paths or files [#501](https://github.com/DonJayamanne/pythonVSCode/issues/501) - + Add the following setting in ```settings.json``` -```json - "python.linting.ignorePatterns": [ - ".vscode/*.py", - "**/site-packages/**/*.py" - ], -``` -* Automatically adding brackets when autocompleting functions/methods [#425](https://github.com/DonJayamanne/pythonVSCode/issues/425) - + To enable this feature, turn on the setting ```"python.autoComplete.addBrackets": true``` -* Running nose tests with the arguments '--with-xunit' and '--xunit-file' [#517](https://github.com/DonJayamanne/pythonVSCode/issues/517) -* Added support for workspaceRootFolderName in settings.json [#525](https://github.com/DonJayamanne/pythonVSCode/pull/525), [#522](https://github.com/DonJayamanne/pythonVSCode/issues/522) -* Added support for workspaceRootFolderName in settings.json [#525](https://github.com/DonJayamanne/pythonVSCode/pull/525), [#522](https://github.com/DonJayamanne/pythonVSCode/issues/522) -* Fixes to running code in terminal [#515](https://github.com/DonJayamanne/pythonVSCode/issues/515) - -### Thanks -* [Thijs Damsma](https://github.com/tdamsma) -* [Siddhartha Gandhi](https://github.com/gandhis1) -* [Nobuhiro Nakamura](https://github.com/lefb766) -* [Water Zheng](https://github.com/zhengxiaoyao0716) -* [Andris Raugulis](https://github.com/arthepsy) -* [Igor Novozhilov](https://github.com/IgorNovozhilov) -* [Luca Mussi](https://github.com/splendido) -* [Shengyu Fu](https://github.com/shengyfu) -* [codebetter](https://github.com/skilliscode) - -## Source - -[GitHub](https://github.com/DonJayamanne/pythonVSCode) - - -## License - -[MIT](https://raw.githubusercontent.com/DonJayamanne/pythonVSCode/master/LICENSE) +## Data/Telemetry +The Microsoft Python Extension for Visual Studio Code collects usage +data and sends it to Microsoft to help improve our products and +services. Read our +[privacy statement](https://privacy.microsoft.com/privacystatement) to +learn more. This extension respects the `telemetry.enableTelemetry` +setting which you can learn more about at +https://code.visualstudio.com/docs/supporting/faq#_how-to-disable-telemetry-reporting. diff --git a/ThirdPartyNotices-Distribution.txt b/ThirdPartyNotices-Distribution.txt new file mode 100644 index 000000000000..dd1decf15fa6 --- /dev/null +++ b/ThirdPartyNotices-Distribution.txt @@ -0,0 +1,1577 @@ + +THIRD-PARTY SOFTWARE NOTICES AND INFORMATION +Do Not Translate or Localize + +The Microsoft Python extension for Visual Studio Code incorporates components from the projects listed below. This file provides information in Section A regarding components that are being relicensed to you by Microsoft under Microsoft's software licensing terms, and information in Section B regarding components that are being made available to you by Microsoft under their original licensing terms. The original copyright notices and the licenses under which Microsoft received such components are set forth below for informational purposes. Microsoft reserves all rights not expressly granted herein, whether by implication, estoppel or otherwise. + +Section A: Microsoft is offering you a license to use the following components under Microsoft's software licensing terms. + +1. +Go for Visual Studio Code (https://github.com/Microsoft/vscode-go) +2. anser (https://github.com/IonicaBizau/anser) + Includes:ansi_up +3. diff-match-patch (https://github.com/ForbesLindesay-Unmaintained/diff-match-patch) +4. fuzzy (https://github.com/mattyork/fuzzy) +5. Google Diff Match and Patch (https://github.com/GerHobbelt/google-diff-match-patch) +6. isort (https://github.com/timothycrosley/isort) +7. jedi (https://github.com/davidhalter/jedi) +8. line-by-line (https://github.com/Osterjour/line-by-line) +9. lodash (https://github.com/lodash/lodash) + Includes:Sizzle CSS Selector Engine + Includes:Webpack +10. minimatch (https://github.com/isaacs/minimatch) +11. named-js-regexp (https://github.com/edvinv/named-js-regexp) +12. node-copy-paste (https://github.com/xavi-/node-copy-paste) +13. node-fs-extra (https://github.com/jprichardson/node-fs-extra) +14. node-prepend-file (https://github.com/hemanth/node-prepend-file) +15. node-semver (https://github.com/npm/node-semver) +16. node-static (https://github.com/cloudhead/node-static) + Includes:ApacheBench +17. node-tmp (https://github.com/raszi/node-tmp) +18. node-tree-kill (https://github.com/pkrumins/node-tree-kill) +19. node-winreg (https://github.com/fresc81/node-winreg) +20. node-xml2js (https://github.com/Leonidas-from-XIV/node-xml2js) +21. omnisharp-vscode (https://github.com/OmniSharp/omnisharp-vscode) +22. pythonVSCode (https://github.com/DonJayamanne/pythonVSCode) +23. RxJS (https://github.com/Reactive-Extensions/RxJS) + Includes:Google Closure + Includes:jortSort +24. socket.io (https://github.com/socketio/socket.io) +25. Sphinx (http://sphinx-doc.org/) +26. transformime (https://github.com/nteract/transformime) +27. transformime-marked (https://github.com/nteract/transformime-marked) +28. uint64be (https://github.com/mafintosh/uint64be) +29. untildify (https://github.com/sindresorhus/untildify) +30. vscode-debugadapter (https://github.com/Microsoft/vscode-debugadapter-node/tree/master/adapter) +31. vscode-debugprotocol (https://github.com/Microsoft/vscode-debugadapter-node/tree/master/protocol) +32. vscode-extension-telemetry (https://github.com/Microsoft/vscode-extension-telemetry) +33. vscode-languageclient (https://github.com/Microsoft/vscode-languageserver-node) +34. vscode-languageserver (https://github.com/Microsoft/vscode-languageserver-node/) + + +%% +Go for Visual Studio Code NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF +Go for Visual Studio Code NOTICES AND INFORMATION + +%% anser NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2012-16 Ionica Bizau (http://ionicabizau.net) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +========================================= +Includes: ansi_up + +Copyright (c) 2011 Dru Nelson + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF anser NOTICES AND INFORMATION + +%% diff-match-patch NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright 2006 Google Inc. +http://code.google.com/p/google-diff-match-patch/ + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +========================================= +END OF diff-match-patch NOTICES AND INFORMATION + +%% fuzzy NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2012 Matt York + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +========================================= +END OF fuzzy NOTICES AND INFORMATION + +%% Google Diff Match and Patch NOTICES AND INFORMATION BEGIN HERE +========================================= + * Copyright 2006 Google Inc. + * http://code.google.com/p/google-diff-match-patch/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +========================================= +END OF Google Diff Match and Patch NOTICES AND INFORMATION + +%% isort NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2013 Timothy Edmund Crosley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF isort NOTICES AND INFORMATION + +%% jedi NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) <2013> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF jedi NOTICES AND INFORMATION + +%% line-by-line NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2012 Markus von der Wehd + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF line-by-line NOTICES AND INFORMATION + +%% lodash NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright JS Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. +========================================= +Includes: Sizzle CSS Selector Engine + +Copyright (c) 2009 John Resig + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +Includes: Webpack + +Copyright (c) JS Foundation and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +========================================= +END OF lodash NOTICES AND INFORMATION + +%% minimatch NOTICES AND INFORMATION BEGIN HERE +========================================= +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF minimatch NOTICES AND INFORMATION + +%% named-js-regexp NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License + +Copyright (c) 2015, @edvinv + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF named-js-regexp NOTICES AND INFORMATION + +%% node-copy-paste NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2015, Xavi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF node-copy-paste NOTICES AND INFORMATION + +%% node-fs-extra NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2011-2017 JP Richardson + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files +(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, + merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +========================================= +END OF node-fs-extra NOTICES AND INFORMATION + +%% node-prepend-file NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) Hemanth.HM (h3manth.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +========================================= +END OF node-prepend-file NOTICES AND INFORMATION + +%% node-semver NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +========================================= +END OF node-semver NOTICES AND INFORMATION + +%% node-static NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2010-14 Alexis Sellier + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +========================================= +Includes: ApacheBench + +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +========================================= +END OF node-static NOTICES AND INFORMATION + +%% node-tmp NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2014 KARASZI Istv๏ฟฝn + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +========================================= +END OF node-tmp NOTICES AND INFORMATION + +%% node-tree-kill NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2014 Peteris Krumins + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +========================================= +END OF node-tree-kill NOTICES AND INFORMATION + +%% node-winreg NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2016, Paul Bottin 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 node-winreg NOTICES AND INFORMATION + +%% node-xml2js NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright 2010, 2011, 2012, 2013. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + +========================================= +END OF node-xml2js NOTICES AND INFORMATION + +%% omnisharp-vscode NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, +modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT +OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF omnisharp-vscode NOTICES AND INFORMATION + +%% pythonVSCode NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2015 DonJayamanne + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF pythonVSCode NOTICES AND INFORMATION + +%% RxJS NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) Microsoft. All rights reserved. + +Microsoft Open Technologies would like to thank its contributors, a list +of whom are at http://rx.codeplex.com/wikipage?title=Contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); you +may not use this file except in compliance with the License. You may +obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied. See the License for the specific language governing permissions +and limitations under the License. + +========================================= +Includes: Google Closure + +// Copyright (c) 2006 The Closure Library Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +========================================= +Includes: jortSort + +The MIT License (MIT) + +Copyright (c) 2014 Jenn Schiffer + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +========================================= +END OF RxJS NOTICES AND INFORMATION + +%% socket.io NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014-2015 Automattic + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF socket.io NOTICES AND INFORMATION + +%% Sphinx NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2007-2017 by the Sphinx team (see AUTHORS file). +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 Sphinx NOTICES AND INFORMATION + +%% transformime NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2015, +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. + +* Neither the name of transformime nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +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 transformime NOTICES AND INFORMATION + +%% transformime-marked NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2015, +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. + +* Neither the name of transformime-marked nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +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 transformime-marked NOTICES AND INFORMATION + +%% uint64be NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2015 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +========================================= +END OF uint64be NOTICES AND INFORMATION + +%% untildify NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +========================================= +END OF untildify NOTICES AND INFORMATION + +%% vscode-debugadapter NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, +modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT +OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +========================================= +END OF vscode-debugadapter NOTICES AND INFORMATION + +%% vscode-debugprotocol NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, +modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT +OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +========================================= +END OF vscode-debugprotocol NOTICES AND INFORMATION + +%% vscode-extension-telemetry NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF vscode-extension-telemetry NOTICES AND INFORMATION + +%% vscode-languageclient NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +========================================= +END OF vscode-languageclient NOTICES AND INFORMATION + +%% vscode-languageserver NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +========================================= +END OF vscode-languageserver NOTICES AND INFORMATION + +Section B: The following components are offered subject to their original license terms. + +1. Files from the Python Project (https://www.python.org/) +2. Python documentation (https://docs.python.org/) +3. python-functools32 (https://github.com/MiCHiLU/python-functools32/blob/master/functools32/functools32.py) + + +%% Files from the Python Project NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017 Python Software Foundation; All Rights +Reserved" are retained in Python alone or in any derivative version prepared by +Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the Internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the Internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (C) 2006-2010 Python Software Foundation + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF Files from the Python Project NOTICES, INFORMATION, AND LICENSE + +%% Python documentation NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +Terms and conditions for accessing or otherwise using Python + +PSF LICENSE AGREEMENT FOR PYTHON 2.7.13 +1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and + the Individual or Organization ("Licensee") accessing and otherwise using Python + 2.7.13 software in source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby + grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, + analyze, test, perform and/or display publicly, prepare derivative works, + distribute, and otherwise use Python 2.7.13 alone or in any derivative + version, provided, however, that PSF's License Agreement and PSF's notice of + copyright, i.e., "Copyright ๏ฟฝ 2001-2017 Python Software Foundation; All Rights + Reserved" are retained in Python 2.7.13 alone or in any derivative version + prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on or + incorporates Python 2.7.13 or any part thereof, and wants to make the + derivative work available to others as provided herein, then Licensee hereby + agrees to include in any such work a brief summary of the changes made to Python + 2.7.13. + +4. PSF is making Python 2.7.13 available to Licensee on an "AS IS" basis. + PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF + EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR + WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE + USE OF PYTHON 2.7.13 WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON 2.7.13 + FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF + MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 2.7.13, OR ANY DERIVATIVE + THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material breach of + its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any relationship + of agency, partnership, or joint venture between PSF and Licensee. This License + Agreement does not grant permission to use PSF trademarks or trade name in a + trademark sense to endorse or promote products or services of Licensee, or any + third party. + +8. By copying, installing or otherwise using Python 2.7.13, Licensee agrees + to be bound by the terms and conditions of this License Agreement. +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an office at + 160 Saratoga Avenue, Santa Clara, CA 95051, and the Individual or Organization + ("Licensee") accessing and otherwise using this software in source or binary + form and its associated documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License Agreement, + BeOpen hereby grants Licensee a non-exclusive, royalty-free, world-wide license + to reproduce, analyze, test, perform and/or display publicly, prepare derivative + works, distribute, and otherwise use the Software alone or in any derivative + version, provided, however, that the BeOpen Python License is retained in the + Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" basis. + BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF + EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND DISCLAIMS ANY REPRESENTATION OR + WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE + USE OF THE SOFTWARE WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE SOFTWARE FOR + ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF USING, + MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY DERIVATIVE THEREOF, EVEN IF + ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material breach of + its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all respects + by the law of the State of California, excluding conflict of law provisions. + Nothing in this License Agreement shall be deemed to create any relationship of + agency, partnership, or joint venture between BeOpen and Licensee. This License + Agreement does not grant permission to use BeOpen trademarks or trade names in a + trademark sense to endorse or promote products or services of Licensee, or any + third party. As an exception, the "BeOpen Python" logos available at + http://www.pythonlabs.com/logos.html may be used according to the permissions + granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee agrees to be + bound by the terms and conditions of this License Agreement. +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +1. This LICENSE AGREEMENT is between the Corporation for National Research + Initiatives, having an office at 1895 Preston White Drive, Reston, VA 20191 + ("CNRI"), and the Individual or Organization ("Licensee") accessing and + otherwise using Python 1.6.1 software in source or binary form and its + associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI hereby + grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, + analyze, test, perform and/or display publicly, prepare derivative works, + distribute, and otherwise use Python 1.6.1 alone or in any derivative version, + provided, however, that CNRI's License Agreement and CNRI's notice of copyright, + i.e., "Copyright ๏ฟฝ 1995-2001 Corporation for National Research Initiatives; All + Rights Reserved" are retained in Python 1.6.1 alone or in any derivative version + prepared by Licensee. Alternately, in lieu of CNRI's License Agreement, + Licensee may substitute the following text (omitting the quotes): "Python 1.6.1 + is made available subject to the terms and conditions in CNRI's License + Agreement. This Agreement together with Python 1.6.1 may be located on the + Internet using the following unique, persistent identifier (known as a handle): + 1895.22/1013. This Agreement may also be obtained from a proxy server on the + Internet using the following URL: http://hdl.handle.net/1895.22/1013." + +3. In the event Licensee prepares a derivative work that is based on or + incorporates Python 1.6.1 or any part thereof, and wants to make the derivative + work available to others as provided herein, then Licensee hereby agrees to + include in any such work a brief summary of the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" basis. CNRI + MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, + BUT NOT LIMITATION, CNRI MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY + OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF + PYTHON 1.6.1 WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON 1.6.1 FOR + ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF + MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, OR ANY DERIVATIVE + THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material breach of + its terms and conditions. + +7. This License Agreement shall be governed by the federal intellectual property + law of the United States, including without limitation the federal copyright + law, and, to the extent such U.S. federal law does not apply, by the law of the + Commonwealth of Virginia, excluding Virginia's conflict of law provisions. + Notwithstanding the foregoing, with regard to derivative works based on Python + 1.6.1 that incorporate non-separable material that was previously distributed + under the GNU General Public License (GPL), the law of the Commonwealth of + Virginia shall govern this License Agreement only as to issues arising under or + with respect to Paragraphs 4, 5, and 7 of this License Agreement. Nothing in + this License Agreement shall be deemed to create any relationship of agency, + partnership, or joint venture between CNRI and Licensee. This License Agreement + does not grant permission to use CNRI trademarks or trade name in a trademark + sense to endorse or promote products or services of Licensee, or any third + party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, installing + or otherwise using Python 1.6.1, Licensee agrees to be bound by the terms and + conditions of this License Agreement. +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +Copyright ๏ฟฝ 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, The +Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, provided that +the above copyright notice appear in all copies and that both that copyright +notice and this permission notice appear in supporting documentation, and that +the name of Stichting Mathematisch Centrum or CWI not be used in advertising or +publicity pertaining to distribution of the software without specific, written +prior permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS +SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO +EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE FOR ANY SPECIAL, INDIRECT +OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS +SOFTWARE. +========================================= +END OF Python documentation NOTICES, INFORMATION, AND LICENSE + +%% python-functools32 NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012 Python Software Foundation; All Rights Reserved" are retained in Python +alone or in any derivative version prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the Internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the Internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, +The Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF python-functools32 NOTICES, INFORMATION, AND LICENSE + + diff --git a/ThirdPartyNotices-Repository.txt b/ThirdPartyNotices-Repository.txt new file mode 100644 index 000000000000..05090ab34b88 --- /dev/null +++ b/ThirdPartyNotices-Repository.txt @@ -0,0 +1,761 @@ + +THIRD-PARTY SOFTWARE NOTICES AND INFORMATION +Do Not Translate or Localize + +The Microsoft Python extension for Visual Studio Code incorporates third party material from the projects listed below. The original copyright notice and the license under which Microsoft received such third party material are set forth below. Microsoft reserves all other rights not expressly granted, whether by implication, estoppel or otherwise. + + +1. Go for Visual Studio Code (https://github.com/Microsoft/vscode-go) +2. Files from the Python Project (https://www.python.org/) +3. Google Diff Match and Patch (https://github.com/GerHobbelt/google-diff-match-patch) +4. isort (https://github.com/timothycrosley/isort) +5. jedi (https://github.com/davidhalter/jedi) +6. omnisharp-vscode (https://github.com/OmniSharp/omnisharp-vscode) +7. Python documentation (https://docs.python.org/) +8. python-functools32 (https://github.com/MiCHiLU/python-functools32/blob/master/functools32/functools32.py) +9. pythonVSCode (https://github.com/DonJayamanne/pythonVSCode) +10. Sphinx (http://sphinx-doc.org/) + + +%% +Go for Visual Studio Code NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF +Go for Visual Studio Code NOTICES, INFORMATION, AND LICENSE + +%% Files from the Python Project NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017 Python Software Foundation; All Rights +Reserved" are retained in Python alone or in any derivative version prepared by +Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the Internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the Internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (C) 2006-2010 Python Software Foundation + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF Files from the Python Project NOTICES, INFORMATION, AND LICENSE + +%% Google Diff Match and Patch NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= + * Copyright 2006 Google Inc. + * http://code.google.com/p/google-diff-match-patch/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +========================================= +END OF Google Diff Match and Patch NOTICES, INFORMATION, AND LICENSE + +%% isort NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2013 Timothy Edmund Crosley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF isort NOTICES, INFORMATION, AND LICENSE + +%% jedi NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) <2013> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF jedi NOTICES, INFORMATION, AND LICENSE + +%% omnisharp-vscode NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, +modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT +OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF omnisharp-vscode NOTICES, INFORMATION, AND LICENSE + +%% Python documentation NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +Terms and conditions for accessing or otherwise using Python +PSF LICENSE AGREEMENT FOR PYTHON 2.7.13 +1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and + the Individual or Organization ("Licensee") accessing and otherwise using Python + 2.7.13 software in source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby + grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, + analyze, test, perform and/or display publicly, prepare derivative works, + distribute, and otherwise use Python 2.7.13 alone or in any derivative + version, provided, however, that PSF's License Agreement and PSF's notice of + copyright, i.e., "Copyright ๏ฟฝ 2001-2017 Python Software Foundation; All Rights + Reserved" are retained in Python 2.7.13 alone or in any derivative version + prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on or + incorporates Python 2.7.13 or any part thereof, and wants to make the + derivative work available to others as provided herein, then Licensee hereby + agrees to include in any such work a brief summary of the changes made to Python + 2.7.13. + +4. PSF is making Python 2.7.13 available to Licensee on an "AS IS" basis. + PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF + EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR + WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE + USE OF PYTHON 2.7.13 WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON 2.7.13 + FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF + MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 2.7.13, OR ANY DERIVATIVE + THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material breach of + its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any relationship + of agency, partnership, or joint venture between PSF and Licensee. This License + Agreement does not grant permission to use PSF trademarks or trade name in a + trademark sense to endorse or promote products or services of Licensee, or any + third party. + +8. By copying, installing or otherwise using Python 2.7.13, Licensee agrees + to be bound by the terms and conditions of this License Agreement. + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an office at + 160 Saratoga Avenue, Santa Clara, CA 95051, and the Individual or Organization + ("Licensee") accessing and otherwise using this software in source or binary + form and its associated documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License Agreement, + BeOpen hereby grants Licensee a non-exclusive, royalty-free, world-wide license + to reproduce, analyze, test, perform and/or display publicly, prepare derivative + works, distribute, and otherwise use the Software alone or in any derivative + version, provided, however, that the BeOpen Python License is retained in the + Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" basis. + BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF + EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND DISCLAIMS ANY REPRESENTATION OR + WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE + USE OF THE SOFTWARE WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE SOFTWARE FOR + ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF USING, + MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY DERIVATIVE THEREOF, EVEN IF + ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material breach of + its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all respects + by the law of the State of California, excluding conflict of law provisions. + Nothing in this License Agreement shall be deemed to create any relationship of + agency, partnership, or joint venture between BeOpen and Licensee. This License + Agreement does not grant permission to use BeOpen trademarks or trade names in a + trademark sense to endorse or promote products or services of Licensee, or any + third party. As an exception, the "BeOpen Python" logos available at + http://www.pythonlabs.com/logos.html may be used according to the permissions + granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee agrees to be + bound by the terms and conditions of this License Agreement. + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +1. This LICENSE AGREEMENT is between the Corporation for National Research + Initiatives, having an office at 1895 Preston White Drive, Reston, VA 20191 + ("CNRI"), and the Individual or Organization ("Licensee") accessing and + otherwise using Python 1.6.1 software in source or binary form and its + associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI hereby + grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, + analyze, test, perform and/or display publicly, prepare derivative works, + distribute, and otherwise use Python 1.6.1 alone or in any derivative version, + provided, however, that CNRI's License Agreement and CNRI's notice of copyright, + i.e., "Copyright ๏ฟฝ 1995-2001 Corporation for National Research Initiatives; All + Rights Reserved" are retained in Python 1.6.1 alone or in any derivative version + prepared by Licensee. Alternately, in lieu of CNRI's License Agreement, + Licensee may substitute the following text (omitting the quotes): "Python 1.6.1 + is made available subject to the terms and conditions in CNRI's License + Agreement. This Agreement together with Python 1.6.1 may be located on the + Internet using the following unique, persistent identifier (known as a handle): + 1895.22/1013. This Agreement may also be obtained from a proxy server on the + Internet using the following URL: http://hdl.handle.net/1895.22/1013." + +3. In the event Licensee prepares a derivative work that is based on or + incorporates Python 1.6.1 or any part thereof, and wants to make the derivative + work available to others as provided herein, then Licensee hereby agrees to + include in any such work a brief summary of the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" basis. CNRI + MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, + BUT NOT LIMITATION, CNRI MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY + OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF + PYTHON 1.6.1 WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON 1.6.1 FOR + ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF + MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, OR ANY DERIVATIVE + THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material breach of + its terms and conditions. + +7. This License Agreement shall be governed by the federal intellectual property + law of the United States, including without limitation the federal copyright + law, and, to the extent such U.S. federal law does not apply, by the law of the + Commonwealth of Virginia, excluding Virginia's conflict of law provisions. + Notwithstanding the foregoing, with regard to derivative works based on Python + 1.6.1 that incorporate non-separable material that was previously distributed + under the GNU General Public License (GPL), the law of the Commonwealth of + Virginia shall govern this License Agreement only as to issues arising under or + with respect to Paragraphs 4, 5, and 7 of this License Agreement. Nothing in + this License Agreement shall be deemed to create any relationship of agency, + partnership, or joint venture between CNRI and Licensee. This License Agreement + does not grant permission to use CNRI trademarks or trade name in a trademark + sense to endorse or promote products or services of Licensee, or any third + party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, installing + or otherwise using Python 1.6.1, Licensee agrees to be bound by the terms and + conditions of this License Agreement. + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +Copyright ๏ฟฝ 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, The +Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, provided that +the above copyright notice appear in all copies and that both that copyright +notice and this permission notice appear in supporting documentation, and that +the name of Stichting Mathematisch Centrum or CWI not be used in advertising or +publicity pertaining to distribution of the software without specific, written +prior permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS +SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO +EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE FOR ANY SPECIAL, INDIRECT +OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS +SOFTWARE. +========================================= +END OF Python documentation NOTICES, INFORMATION, AND LICENSE + +%% python-functools32 NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012 Python Software Foundation; All Rights Reserved" are retained in Python +alone or in any derivative version prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the Internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the Internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, +The Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF python-functools32 NOTICES, INFORMATION, AND LICENSE + +%% pythonVSCode NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2015 DonJayamanne + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF pythonVSCode NOTICES, INFORMATION, AND LICENSE + +%% Sphinx NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +Copyright (c) 2007-2017 by the Sphinx team (see AUTHORS file). +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 Sphinx NOTICES, INFORMATION, AND LICENSE + + diff --git a/docs b/docs deleted file mode 160000 index f37284e16b70..000000000000 --- a/docs +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f37284e16b707ef9c43b126decde21929248a7dd diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 000000000000..f284ad9edc91 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,258 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +const gulp = require('gulp'); +const filter = require('gulp-filter'); +const es = require('event-stream'); +const tsfmt = require('typescript-formatter'); +const tslint = require('tslint'); +const relative = require('relative'); +const ts = require('gulp-typescript'); + +/** + * Hygiene works by creating cascading subsets of all our files and + * passing them through a sequence of checks. Here are the current subsets, + * named according to the checks performed on them. Each subset contains + * the following one, as described in mathematical notation: + * + * all โŠƒ eol โЇ indentation โŠƒ copyright โŠƒ typescript + */ + +const all = [ + 'src/**/*', + 'src/client/**/*', +]; + +const eolFilter = [ + '**', + '!.editorconfig', + '!.eslintrc', + '!.gitignore', + '!.gitmodules', + '!.jshintignore', + '!.jshintrc', + '!.npmrc', + '!.vscodeignore', + '!LICENSE', + '!webpack.config.js', + '!**/node_modules/**', + '!**/*.{svg,exe,png,bmp,scpt,bat,cmd,cur,ttf,woff,eot,txt,md,json,yml}', + '!out/**/*', + '!images/**/*', + '!.vscode/**/*', + '!pythonFiles/**/*', + '!resources/**/*', + '!snippets/**/*', + '!syntaxes/**/*', + '!**/typings/**/*', +]; + +const indentationFilter = [ + 'src/**/*.ts', + '!**/typings/**/*', +]; + +const tslintFilter = [ + 'src/**/*.ts', + 'test/**/*.ts', + '!webpack.config.js', + '!**/node_modules/**', + '!out/**/*', + '!images/**/*', + '!.vscode/**/*', + '!pythonFiles/**/*', + '!resources/**/*', + '!snippets/**/*', + '!syntaxes/**/*', + '!**/typings/**/*', +]; + +function reportFailures(failures) { + failures.forEach(failure => { + const name = failure.name || failure.fileName; + const position = failure.startPosition; + const line = position.lineAndCharacter ? position.lineAndCharacter.line : position.line; + const character = position.lineAndCharacter ? position.lineAndCharacter.character : position.character; + + // Output in format similar to tslint for the linter to pickup. + console.error(`ERROR: (${failure.ruleName}) ${relative(__dirname, name)}[${line + 1}, ${character + 1}]: ${failure.failure}`); + }); +} + +const hygiene = exports.hygiene = (some, options) => { + options = options || {}; + let errorCount = 0; + const eol = es.through(function (file) { + if (/\r\n?/g.test(file.contents.toString('utf8'))) { + console.error(file.relative + ': Bad EOL found'); + errorCount++; + } + + this.emit('data', file); + }); + + const indentation = es.through(function (file) { + file.contents + .toString('utf8') + .split(/\r\n|\r|\n/) + .forEach((line, i) => { + if (/^\s*$/.test(line)) { + // Empty or whitespace lines are OK. + } else if (/^(\s\s\s\s)+.*/.test(line)) { + // Good indent. + } else if (/^[\t]+.*/.test(line)) { + console.error(file.relative + '(' + (i + 1) + ',1): Bad whitespace indentation'); + errorCount++; + } + }); + + this.emit('data', file); + }); + + const formatting = es.map(function (file, cb) { + tsfmt.processString(file.path, file.contents.toString('utf8'), { + verify: true, + tsconfig: true, + tslint: true, + editorconfig: true, + tsfmt: true + }).then(result => { + if (result.error) { + console.error(result.message); + errorCount++; + } + cb(null, file); + + }, err => { + cb(err); + }); + }); + + const tsl = es.through(function (file) { + const configuration = tslint.Configuration.findConfiguration(null, '.'); + const options = { + formatter: 'json' + }; + const contents = file.contents.toString('utf8'); + const program = require('tslint').Linter.createProgram("./tsconfig.json"); + const linter = new tslint.Linter(options, program); + linter.lint(file.relative, contents, configuration.results); + const result = linter.getResult(); + if (result.failureCount > 0 || result.errorCount > 0) { + reportFailures(result.failures); + if (result.failureCount) { + errorCount += result.failureCount; + } + if (result.errorCount) { + errorCount += result.errorCount; + } + } + + this.emit('data', file); + }); + + const tsFiles = []; + const tscFilesTracker = es.through(function (file) { + tsFiles.push(file.path.replace(/\\/g, '/')); + tsFiles.push(file.path); + this.emit('data', file); + }); + + const tsc = function () { + function customReporter() { + return { + error: function (error) { + const fullFilename = error.fullFilename || ''; + const relativeFilename = error.relativeFilename || ''; + if (tsFiles.findIndex(file => fullFilename === file || relativeFilename === file) === -1) { + return; + } + errorCount += 1; + console.error(error.message); + }, + finish: function () { + // forget the summary. + } + }; + } + const tsProject = ts.createProject('tsconfig.json', { strict: true }); + const reporter = customReporter(); + return tsProject(reporter); + } + + const result = gulp.src(some || all, { + base: '.' + }) + .pipe(filter(f => !f.stat.isDirectory())) + .pipe(filter(eolFilter)) + .pipe(options.skipEOL ? es.through() : eol) + .pipe(filter(indentationFilter)) + .pipe(indentation); + + const typescript = result + .pipe(filter(tslintFilter)) + .pipe(formatting) + .pipe(tsl) + .pipe(tscFilesTracker) + .pipe(tsc()); + + return typescript + .pipe(es.through(null, function () { + if (errorCount > 0) { + this.emit('error', 'Hygiene failed with ' + errorCount + ' errors. Check \'gulpfile.js\'.'); + } else { + this.emit('end'); + } + })); +}; + +gulp.task('hygiene', () => hygiene()); + +// this allows us to run hygiene as a git pre-commit hook. +if (require.main === module) { + const cp = require('child_process'); + + process.on('unhandledRejection', (reason, p) => { + console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); + process.exit(1); + }); + + cp.exec('git config core.autocrlf', (err, out) => { + const skipEOL = out.trim() === 'true'; + + if (process.argv.length > 2) { + return hygiene(process.argv.slice(2), { + skipEOL: skipEOL + }).on('error', err => { + console.error(); + console.error(err); + process.exit(1); + }); + } + + cp.exec('git diff --cached --name-only', { + maxBuffer: 2000 * 1024 + }, (err, out) => { + if (err) { + console.error(); + console.error(err); + process.exit(1); + } + + const some = out + .split(/\r?\n/) + .filter(l => !!l); + hygiene(some, { + skipEOL: skipEOL + }).on('error', err => { + console.error(); + console.error(err); + process.exit(1); + }); + }); + }); +} diff --git a/icon.png b/icon.png new file mode 100644 index 000000000000..5ae724858d3b Binary files /dev/null and b/icon.png differ diff --git a/images/break.gif b/images/break.gif deleted file mode 100644 index a918ea0cd06e..000000000000 Binary files a/images/break.gif and /dev/null differ diff --git a/images/commands.png b/images/commands.png deleted file mode 100644 index d4a5f3a9ed7e..000000000000 Binary files a/images/commands.png and /dev/null differ diff --git a/images/debug/integratedDebugger.gif b/images/debug/integratedDebugger.gif deleted file mode 100644 index 585acdbff363..000000000000 Binary files a/images/debug/integratedDebugger.gif and /dev/null differ diff --git a/images/debugDemo.gif b/images/debugDemo.gif new file mode 100644 index 000000000000..48d7fed55691 Binary files /dev/null and b/images/debugDemo.gif differ diff --git a/images/debugGAE.gif b/images/debugGAE.gif deleted file mode 100644 index cf904fc9897c..000000000000 Binary files a/images/debugGAE.gif and /dev/null differ diff --git a/images/debugUserInput.gif b/images/debugUserInput.gif deleted file mode 100644 index cef9ba8cb802..000000000000 Binary files a/images/debugUserInput.gif and /dev/null differ diff --git a/images/flaskDebugging.gif b/images/flaskDebugging.gif deleted file mode 100644 index bfe120a9da80..000000000000 Binary files a/images/flaskDebugging.gif and /dev/null differ diff --git a/images/general.gif b/images/general.gif index d3e770bb97cf..50c95613d68d 100644 Binary files a/images/general.gif and b/images/general.gif differ diff --git a/images/goToDef.gif b/images/goToDef.gif deleted file mode 100644 index c9ed1d359195..000000000000 Binary files a/images/goToDef.gif and /dev/null differ diff --git a/images/icon.svg b/images/icon.svg deleted file mode 100644 index 6c6619d5b76f..000000000000 --- a/images/icon.svg +++ /dev/null @@ -1 +0,0 @@ -python-logo-generic \ No newline at end of file diff --git a/images/misc/commands.png b/images/misc/commands.png deleted file mode 100644 index de61208b1831..000000000000 Binary files a/images/misc/commands.png and /dev/null differ diff --git a/images/misc/interpreters.png b/images/misc/interpreters.png deleted file mode 100644 index c8dfd0c561c3..000000000000 Binary files a/images/misc/interpreters.png and /dev/null differ diff --git a/images/misc/runFile.png b/images/misc/runFile.png deleted file mode 100644 index 35f02b519c6f..000000000000 Binary files a/images/misc/runFile.png and /dev/null differ diff --git a/images/misc/runFileExplorer.png b/images/misc/runFileExplorer.png deleted file mode 100644 index b55d7714f5d9..000000000000 Binary files a/images/misc/runFileExplorer.png and /dev/null differ diff --git a/images/misc/runSelection.png b/images/misc/runSelection.png deleted file mode 100644 index fae4442a9f34..000000000000 Binary files a/images/misc/runSelection.png and /dev/null differ diff --git a/images/refactorExtractMethod.gif b/images/refactorExtractMethod.gif deleted file mode 100644 index 66675843d387..000000000000 Binary files a/images/refactorExtractMethod.gif and /dev/null differ diff --git a/images/refactorExtractMethodCmd.gif b/images/refactorExtractMethodCmd.gif deleted file mode 100644 index 63edb12601af..000000000000 Binary files a/images/refactorExtractMethodCmd.gif and /dev/null differ diff --git a/images/refactorExtractMethodQuickFix.gif b/images/refactorExtractMethodQuickFix.gif deleted file mode 100644 index 51c70109b3f4..000000000000 Binary files a/images/refactorExtractMethodQuickFix.gif and /dev/null differ diff --git a/images/refactorExtractVar.gif b/images/refactorExtractVar.gif deleted file mode 100644 index 6d0ef2cc3c22..000000000000 Binary files a/images/refactorExtractVar.gif and /dev/null differ diff --git a/images/refactorExtractVarCmd.gif b/images/refactorExtractVarCmd.gif deleted file mode 100644 index 44c7281cf8da..000000000000 Binary files a/images/refactorExtractVarCmd.gif and /dev/null differ diff --git a/images/refactorExtractVarQuickFix.gif b/images/refactorExtractVarQuickFix.gif deleted file mode 100644 index 212d6aad8c3a..000000000000 Binary files a/images/refactorExtractVarQuickFix.gif and /dev/null differ diff --git a/images/rename.gif b/images/rename.gif deleted file mode 100644 index 7df50c520fd4..000000000000 Binary files a/images/rename.gif and /dev/null differ diff --git a/images/sortImport.gif b/images/sortImport.gif deleted file mode 100644 index 9994e9ead707..000000000000 Binary files a/images/sortImport.gif and /dev/null differ diff --git a/images/sortImportCmd.gif b/images/sortImportCmd.gif deleted file mode 100644 index c084808bb0eb..000000000000 Binary files a/images/sortImportCmd.gif and /dev/null differ diff --git a/images/sortImportQuickFix.gif b/images/sortImportQuickFix.gif deleted file mode 100644 index 795183183e1d..000000000000 Binary files a/images/sortImportQuickFix.gif and /dev/null differ diff --git a/images/standardDebugging.gif b/images/standardDebugging.gif deleted file mode 100644 index 4053d6e811d4..000000000000 Binary files a/images/standardDebugging.gif and /dev/null differ diff --git a/images/tests/classCodeLens.png b/images/tests/classCodeLens.png deleted file mode 100644 index ab036b7f253e..000000000000 Binary files a/images/tests/classCodeLens.png and /dev/null differ diff --git a/images/tests/functionCodeLens.png b/images/tests/functionCodeLens.png deleted file mode 100644 index 986bec1c6b65..000000000000 Binary files a/images/tests/functionCodeLens.png and /dev/null differ diff --git a/images/tests/options.png b/images/tests/options.png deleted file mode 100644 index 29f526fa5135..000000000000 Binary files a/images/tests/options.png and /dev/null differ diff --git a/images/tests/results.png b/images/tests/results.png deleted file mode 100644 index 9da4bf487cbe..000000000000 Binary files a/images/tests/results.png and /dev/null differ diff --git a/images/tests/runAll.png b/images/tests/runAll.png deleted file mode 100644 index f7c9d1c29fb0..000000000000 Binary files a/images/tests/runAll.png and /dev/null differ diff --git a/images/tests/runFailed.png b/images/tests/runFailed.png deleted file mode 100644 index c75d43157d1f..000000000000 Binary files a/images/tests/runFailed.png and /dev/null differ diff --git a/images/tests/runTestsInFile.png b/images/tests/runTestsInFile.png deleted file mode 100644 index dc394c999e1c..000000000000 Binary files a/images/tests/runTestsInFile.png and /dev/null differ diff --git a/images/tests/searchFunction.png b/images/tests/searchFunction.png deleted file mode 100644 index 61914a569c39..000000000000 Binary files a/images/tests/searchFunction.png and /dev/null differ diff --git a/images/unittest.gif b/images/unittest.gif new file mode 100644 index 000000000000..1511bccb7392 Binary files /dev/null and b/images/unittest.gif differ diff --git a/languages/pip-requirements.json b/languages/pip-requirements.json new file mode 100644 index 000000000000..746aa3ac3e2e --- /dev/null +++ b/languages/pip-requirements.json @@ -0,0 +1,5 @@ +{ + "comments": { + "lineComment": "#" + } +} diff --git a/package.json b/package.json index 2853193b6203..20ea9ddd16cc 100644 --- a/package.json +++ b/package.json @@ -1,36 +1,37 @@ { "name": "python", "displayName": "Python", - "description": "Linting, Debugging (multi-threaded, remote), Intellisense, code formatting, refactoring, unit tests, snippets, Data Science (with Jupyter), PySpark and more.", - "version": "0.5.5", - "publisher": "donjayamanne", + "description": "Linting, Debugging (multi-threaded, remote), Intellisense, code formatting, refactoring, unit tests, snippets, and more.", + "version": "0.8.0", + "publisher": "ms-python", "author": { - "name": "Don Jayamanne", - "email": "don.jayamanne@yahoo.com" + "name": "Microsoft Corporation" }, "license": "MIT", - "homepage": "http://donjayamanne.github.io/pythonVSCode/", + "homepage": "https://github.com/Microsoft/vscode-python", "repository": { "type": "git", - "url": "https://github.com/DonJayamanne/pythonVSCode" + "url": "https://github.com/Microsoft/vscode-python" }, "bugs": { - "url": "https://github.com/DonJayamanne/pythonVSCode/issues" + "url": "https://github.com/Microsoft/vscode-python/issues" }, - "icon": "images/icon.svg", + "icon": "icon.png", "galleryBanner": { "color": "#1e415e", "theme": "dark" }, "engines": { - "vscode": "^1.6.0" + "vscode": "^1.17.0" }, + "recommendations": [ + "donjayamanne.jupyter" + ], "keywords": [ "python", - "jupyter", "django", - "debugger", - "unittest" + "unittest", + "multi-root ready" ], "categories": [ "Languages", @@ -41,50 +42,33 @@ "Other" ], "activationEvents": [ + "onDebug", "onLanguage:python", "onCommand:python.execInTerminal", "onCommand:python.sortImports", "onCommand:python.runtests", "onCommand:python.debugtests", "onCommand:python.setInterpreter", + "onCommand:python.setShebangInterpreter", "onCommand:python.viewTestUI", "onCommand:python.viewTestOutput", "onCommand:python.selectAndRunTestMethod", "onCommand:python.selectAndDebugTestMethod", + "onCommand:python.selectAndRunTestFile", + "onCommand:python.runCurrentTestFile", "onCommand:python.runFailedTests", "onCommand:python.execSelectionInTerminal", + "onCommand:python.execSelectionInDjangoShell", "onCommand:jupyter.runSelectionLine", "onCommand:jupyter.execCurrentCell", "onCommand:jupyter.execCurrentCellAndAdvance", - "onCommand:python.displayHelp", "onCommand:python.buildWorkspaceSymbols", - "onCommand:python.updateSparkLibrary" + "onCommand:python.updateSparkLibrary", + "onCommand:python.startREPL", + "onCommand:python.goToPythonObject" ], "main": "./out/client/extension", "contributes": { - "languages": [ - { - "id": "testOutput", - "aliases": [ - "vscode-output-colorizer", - "testOutput" - ], - "extensions": [ - ".log" - ], - "configuration": "./syntaxes/test.configuration.json", - "mimetypes": [ - "text/x-code-output" - ] - } - ], - "grammars": [ - { - "language": "testOutput", - "scopeName": "testOutput.log", - "path": "./syntaxes/test.tmLanguage" - } - ], "snippets": [ { "language": "python", @@ -131,7 +115,7 @@ }, { "command": "python.setInterpreter", - "title": "Select Workspace Interpreter", + "title": "Select Interpreter", "category": "Python" }, { @@ -164,6 +148,16 @@ "title": "Debug Unit Test Method ...", "category": "Python" }, + { + "command": "python.selectAndRunTestFile", + "title": "Run Unit Test File ...", + "category": "Python" + }, + { + "command": "python.runCurrentTestFile", + "title": "Run Current Unit Test File", + "category": "Python" + }, { "command": "python.runFailedTests", "title": "Run Failed Unit Tests", @@ -171,7 +165,12 @@ }, { "command": "python.execSelectionInTerminal", - "title": "Run Selection in Python Terminal", + "title": "Run Selection/Line in Python Terminal", + "category": "Python" + }, + { + "command": "python.execSelectionInDjangoShell", + "title": "Run Selection/Line in Django Shell", "category": "Python" }, { @@ -180,28 +179,28 @@ "category": "Jupyter" }, { - "command": "jupyter:execCurrentCell", + "command": "jupyter.execCurrentCell", "title": "Run Cell", "category": "Jupyter" }, { - "command": "jupyter:execCurrentCellAndAdvance", + "command": "jupyter.execCurrentCellAndAdvance", "title": "Run Cell and Advance", "category": "Jupyter" }, { - "command": "jupyter:gotToPreviousCell", + "command": "jupyter.gotToPreviousCell", "title": "Go to Previous Cell", "category": "Jupyter" }, { - "command": "jupyter:gotToNextCell", + "command": "jupyter.gotToNextCell", "title": "Go to Next Cell", "category": "Jupyter" }, { - "command": "python.displayHelp", - "title": "Help", + "command": "python.goToPythonObject", + "title": "Go to Python Object", "category": "Python" } ], @@ -230,10 +229,20 @@ "group": "Python", "when": "editorHasSelection && editorLangId == python" }, + { + "command": "python.execSelectionInDjangoShell", + "group": "Python", + "when": "editorHasSelection && editorLangId == python && python.isDjangoProject" + }, { "when": "resourceLangId == python", "command": "python.execInTerminal", "group": "Python" + }, + { + "when": "resourceLangId == python", + "command": "python.runCurrentTestFile", + "group": "Python" } ], "explorer/context": [ @@ -258,20 +267,292 @@ { "type": "python", "label": "Python", + "languages": [ + "python" + ], "enableBreakpointsFor": { "languageIds": [ "python", "html" ] }, - "aiKey": "fce7a3d5-4665-4404-b786-31a6306749a6", + "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", "program": "./out/client/debugger/Main.js", "runtime": "node", + "configurationSnippets": [ + { + "label": "%python.snippet.launch.standard.label%", + "description": "%python.snippet.launch.standard.description%", + "body": { + "name": "Python", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "pythonPath": "^\"\\${config:python.pythonPath}\"", + "program": "^\"\\${file}\"", + "cwd": "^\"\\${workspaceRoot}\"", + "env": {}, + "envFile": "^\"\\${workspaceRoot}/.env\"", + "debugOptions": [ + "WaitOnAbnormalExit", + "WaitOnNormalExit", + "RedirectOutput" + ] + } + }, + { + "label": "%python.snippet.launch.pyspark.label%", + "description": "%python.snippet.launch.pyspark.description%", + "body": { + "name": "PySpark", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "osx": { + "pythonPath": "^\"\\${env:SPARK_HOME}/bin/spark-submit\"" + }, + "windows": { + "pythonPath": "^\"\\${env:SPARK_HOME}/bin/spark-submit.cmd\"" + }, + "linux": { + "pythonPath": "^\"\\${env:SPARK_HOME}/bin/spark-submit\"" + }, + "program": "^\"\\${file}\"", + "cwd": "^\"\\${workspaceRoot}\"", + "env": {}, + "envFile": "^\"\\${workspaceRoot}/.env\"", + "debugOptions": [ + "WaitOnAbnormalExit", + "WaitOnNormalExit", + "RedirectOutput" + ] + } + }, + { + "label": "%python.snippet.launch.module.label%", + "description": "%python.snippet.launch.module.description%", + "body": { + "name": "Python Module", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "pythonPath": "^\"\\${config:python.pythonPath}\"", + "module": "module.name", + "cwd": "^\"\\${workspaceRoot}\"", + "env": {}, + "envFile": "^\"\\${workspaceRoot}/.env\"", + "debugOptions": [ + "WaitOnAbnormalExit", + "WaitOnNormalExit", + "RedirectOutput" + ] + } + }, + { + "label": "%python.snippet.launch.terminal.label%", + "description": "%python.snippet.launch.terminal.description%", + "body": { + "name": "Integrated Terminal/Console", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "pythonPath": "^\"\\${config:python.pythonPath}\"", + "program": "^\"\\${file}\"", + "cwd": "", + "console": "integratedTerminal", + "env": {}, + "envFile": "^\"\\${workspaceRoot}/.env\"", + "debugOptions": [ + "WaitOnAbnormalExit", + "WaitOnNormalExit" + ] + } + }, + { + "label": "%python.snippet.launch.externalTerminal.label%", + "description": "%python.snippet.launch.externalTerminal.description%", + "body": { + "name": "External Terminal/Console", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "pythonPath": "^\"\\${config:python.pythonPath}\"", + "program": "^\"\\${file}\"", + "cwd": "", + "console": "externalTerminal", + "env": {}, + "envFile": "^\"\\${workspaceRoot}/.env\"", + "debugOptions": [ + "WaitOnAbnormalExit", + "WaitOnNormalExit" + ] + } + }, + { + "label": "%python.snippet.launch.django.label%", + "description": "%python.snippet.launch.django.description%", + "body": { + "name": "Django", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "pythonPath": "^\"\\${config:python.pythonPath}\"", + "program": "^\"\\${workspaceRoot}/manage.py\"", + "cwd": "^\"\\${workspaceRoot}\"", + "args": [ + "runserver", + "--noreload", + "--nothreading" + ], + "env": {}, + "envFile": "^\"\\${workspaceRoot}/.env\"", + "debugOptions": [ + "WaitOnAbnormalExit", + "WaitOnNormalExit", + "RedirectOutput", + "DjangoDebugging" + ] + } + }, + { + "label": "%python.snippet.launch.flask.label%", + "description": "%python.snippet.launch.flask.description%", + "body": { + "name": "Flask", + "type": "python", + "request": "launch", + "stopOnEntry": false, + "pythonPath": "^\"\\${config:python.pythonPath}\"", + "program": "fully qualified path fo 'flask' executable. Generally located along with python interpreter", + "cwd": "^\"\\${workspaceRoot}\"", + "env": { + "FLASK_APP": "^\"\\${workspaceRoot}/quickstart/app.py\"" + }, + "args": [ + "run", + "--no-debugger", + "--no-reload" + ], + "envFile": "^\"\\${workspaceRoot}/.env\"", + "debugOptions": [ + "WaitOnAbnormalExit", + "WaitOnNormalExit", + "RedirectOutput" + ] + } + }, + { + "label": "%python.snippet.launch.flaskOld.label%", + "description": "%python.snippet.launch.flaskOld.description%", + "body": { + "name": "Flask (old)", + "type": "python", + "request": "launch", + "stopOnEntry": false, + "pythonPath": "^\"\\${config:python.pythonPath}\"", + "program": "^\"\\${workspaceRoot}/run.py\"", + "cwd": "^\"\\${workspaceRoot}\"", + "args": [], + "env": {}, + "envFile": "^\"\\${workspaceRoot}/.env\"", + "debugOptions": [ + "WaitOnAbnormalExit", + "WaitOnNormalExit", + "RedirectOutput" + ] + } + }, + { + "label": "%python.snippet.launch.pyramid.label%", + "description": "%python.snippet.launch.pyramid.description%", + "body": { + "name": "Pyramid", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "pythonPath": "^\"\\${config:python.pythonPath}\"", + "cwd": "^\"\\${workspaceRoot}\"", + "env": {}, + "envFile": "^\"\\${workspaceRoot}/.env\"", + "args": [ + "^\"\\${workspaceRoot}/development.ini\"" + ], + "debugOptions": [ + "WaitOnAbnormalExit", + "WaitOnNormalExit", + "RedirectOutput", + "Pyramid" + ] + } + }, + { + "label": "%python.snippet.launch.watson.label%", + "description": "%python.snippet.launch.watson.description%", + "body": { + "name": "Watson", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "pythonPath": "^\"\\${config:python.pythonPath}\"", + "program": "^\"\\${workspaceRoot}/console.py\"", + "cwd": "^\"\\${workspaceRoot}\"", + "args": [ + "dev", + "runserver", + "--noreload=True" + ], + "env": {}, + "envFile": "^\"\\${workspaceRoot}/.env\"", + "debugOptions": [ + "WaitOnAbnormalExit", + "WaitOnNormalExit", + "RedirectOutput" + ] + } + }, + { + "label": "%python.snippet.launch.scrapy.label%", + "description": "%python.snippet.launch.scrapy.description%", + "body": { + "name": "Scrapy", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "pythonPath": "^\"\\${config:python.pythonPath}\"", + "program": "~/.virtualenvs/scrapy/bin/scrapy", + "cwd": "^\"\\${workspaceRoot}\"", + "args": [ + "crawl", + "specs", + "-o", + "bikes.json" + ], + "console": "integratedTerminal", + "env": {}, + "envFile": "^\"\\${workspaceRoot}/.env\"", + "debugOptions": [ + "WaitOnAbnormalExit", + "WaitOnNormalExit" + ] + } + }, + { + "label": "%python.snippet.launch.attach.label%", + "description": "%python.snippet.launch.attach.description%", + "body": { + "name": "Attach (Remote Debug)", + "type": "python", + "request": "attach", + "localRoot": "^\"\\${workspaceRoot}\"", + "remoteRoot": "^\"\\${workspaceRoot}\"", + "port": 3000, + "secret": "my_secret", + "host": "localhost" + } + } + ], "configurationAttributes": { "launch": { - "required": [ - "program" - ], "properties": { "module": { "type": "string", @@ -286,7 +567,7 @@ "pythonPath": { "type": "string", "description": "Path (fully qualified) to python executable. Defaults to the value in settings.json", - "default": "${config.python.pythonPath}" + "default": "${config:python.pythonPath}" }, "args": { "type": "array", @@ -317,8 +598,8 @@ }, "cwd": { "type": "string", - "description": "Absolute path to the working directory of the program being debugged. Default is the root directory of the file (leave null).", - "default": null + "description": "Absolute path to the working directory of the program being debugged. Default is the root directory of the file (leave empty).", + "default": "" }, "debugOptions": { "type": "array", @@ -333,7 +614,8 @@ "BreakOnSystemExitZero", "DjangoDebugging", "Sudo", - "IgnoreDjangoTemplateWarnings" + "IgnoreDjangoTemplateWarnings", + "Pyramid" ] }, "default": [ @@ -374,7 +656,12 @@ "env": { "type": "object", "description": "Environment variables defined as a key value pair. Property ends up being the Environment Variable and the value of the property ends up being the value of the Env Variable.", - "default": null + "default": {} + }, + "envFile": { + "type": "string", + "description": "Absolute path to a file containing environment variable definitions.", + "default": "" } } }, @@ -392,12 +679,12 @@ "remoteRoot": { "type": "string", "description": "The source root of the remote host.", - "default": null + "default": "" }, "port": { "type": "number", "description": "Debug port to attach", - "default": null + "default": 0 }, "host": { "type": "string", @@ -418,9 +705,11 @@ "type": "python", "request": "launch", "stopOnEntry": true, - "pythonPath": "${config.python.pythonPath}", + "pythonPath": "${config:python.pythonPath}", "program": "${file}", "cwd": "${workspaceRoot}", + "env": {}, + "envFile": "${workspaceRoot}/.env", "debugOptions": [ "WaitOnAbnormalExit", "WaitOnNormalExit", @@ -428,81 +717,62 @@ ] }, { - "name": "PySpark", + "name": "Python: Attach", "type": "python", - "request": "launch", - "stopOnEntry": true, - "osx": { - "pythonPath": "${env.SPARK_HOME}/bin/spark-submit" - }, - "windows": { - "pythonPath": "${env.SPARK_HOME}/bin/spark-submit.cmd" - }, - "linux": { - "pythonPath": "${env.SPARK_HOME}/bin/spark-submit" - }, - "program": "${file}", - "cwd": "${workspaceRoot}", - "debugOptions": [ - "WaitOnAbnormalExit", - "WaitOnNormalExit", - "RedirectOutput" - ] - }, - { - "name": "Python Module", - "type": "python", - "request": "launch", - "stopOnEntry": true, - "pythonPath": "${config.python.pythonPath}", - "module": "module.name", - "cwd": "${workspaceRoot}", - "debugOptions": [ - "WaitOnAbnormalExit", - "WaitOnNormalExit", - "RedirectOutput" - ] + "request": "attach", + "localRoot": "${workspaceRoot}", + "remoteRoot": "${workspaceRoot}", + "port": 3000, + "secret": "my_secret", + "host": "localhost" }, { - "name": "Integrated Terminal/Console", + "name": "Python: Terminal (integrated)", "type": "python", "request": "launch", "stopOnEntry": true, - "pythonPath": "${config.python.pythonPath}", + "pythonPath": "${config:python.pythonPath}", "program": "${file}", - "cwd": "null", + "cwd": "", "console": "integratedTerminal", + "env": {}, + "envFile": "${workspaceRoot}/.env", "debugOptions": [ "WaitOnAbnormalExit", "WaitOnNormalExit" ] }, { - "name": "External Terminal/Console", + "name": "Python: Terminal (external)", "type": "python", "request": "launch", "stopOnEntry": true, - "pythonPath": "${config.python.pythonPath}", + "pythonPath": "${config:python.pythonPath}", "program": "${file}", - "cwd": "null", + "cwd": "", "console": "externalTerminal", + "env": {}, + "envFile": "${workspaceRoot}/.env", "debugOptions": [ "WaitOnAbnormalExit", "WaitOnNormalExit" ] }, { - "name": "Django", + "name": "Python: Django", "type": "python", "request": "launch", "stopOnEntry": true, - "pythonPath": "${config.python.pythonPath}", + "pythonPath": "${config:python.pythonPath}", "program": "${workspaceRoot}/manage.py", "cwd": "${workspaceRoot}", "args": [ "runserver", - "--noreload" + "--noreload", + "--nothreading" ], + "env": {}, + "envFile": "${workspaceRoot}/.env", "debugOptions": [ "WaitOnAbnormalExit", "WaitOnNormalExit", @@ -511,11 +781,11 @@ ] }, { - "name": "Flask", + "name": "Python: Flask (0.11.x or later)", "type": "python", "request": "launch", "stopOnEntry": false, - "pythonPath": "${config.python.pythonPath}", + "pythonPath": "${config:python.pythonPath}", "program": "fully qualified path fo 'flask' executable. Generally located along with python interpreter", "cwd": "${workspaceRoot}", "env": { @@ -526,6 +796,7 @@ "--no-debugger", "--no-reload" ], + "envFile": "${workspaceRoot}/.env", "debugOptions": [ "WaitOnAbnormalExit", "WaitOnNormalExit", @@ -533,14 +804,16 @@ ] }, { - "name": "Flask (old)", + "name": "Python: Flask (0.10.x or earlier)", "type": "python", "request": "launch", "stopOnEntry": false, - "pythonPath": "${config.python.pythonPath}", + "pythonPath": "${config:python.pythonPath}", "program": "${workspaceRoot}/run.py", "cwd": "${workspaceRoot}", "args": [], + "env": {}, + "envFile": "${workspaceRoot}/.env", "debugOptions": [ "WaitOnAbnormalExit", "WaitOnNormalExit", @@ -548,11 +821,70 @@ ] }, { - "name": "Watson", + "name": "Python: PySpark", "type": "python", "request": "launch", "stopOnEntry": true, - "pythonPath": "${config.python.pythonPath}", + "osx": { + "pythonPath": "${env:SPARK_HOME}/bin/spark-submit" + }, + "windows": { + "pythonPath": "${env:SPARK_HOME}/bin/spark-submit.cmd" + }, + "linux": { + "pythonPath": "${env:SPARK_HOME}/bin/spark-submit" + }, + "program": "${file}", + "cwd": "${workspaceRoot}", + "env": {}, + "envFile": "${workspaceRoot}/.env", + "debugOptions": [ + "WaitOnAbnormalExit", + "WaitOnNormalExit", + "RedirectOutput" + ] + }, + { + "name": "Python: Module", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "pythonPath": "${config:python.pythonPath}", + "module": "module.name", + "cwd": "${workspaceRoot}", + "env": {}, + "envFile": "${workspaceRoot}/.env", + "debugOptions": [ + "WaitOnAbnormalExit", + "WaitOnNormalExit", + "RedirectOutput" + ] + }, + { + "name": "Python: Pyramid", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "pythonPath": "${config:python.pythonPath}", + "cwd": "${workspaceRoot}", + "env": {}, + "envFile": "${workspaceRoot}/.env", + "args": [ + "${workspaceRoot}/development.ini" + ], + "debugOptions": [ + "WaitOnAbnormalExit", + "WaitOnNormalExit", + "RedirectOutput", + "Pyramid" + ] + }, + { + "name": "Python: Watson", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "pythonPath": "${config:python.pythonPath}", "program": "${workspaceRoot}/console.py", "cwd": "${workspaceRoot}", "args": [ @@ -560,21 +892,13 @@ "runserver", "--noreload=True" ], + "env": {}, + "envFile": "${workspaceRoot}/.env", "debugOptions": [ "WaitOnAbnormalExit", "WaitOnNormalExit", "RedirectOutput" ] - }, - { - "name": "Attach (Remote Debug)", - "type": "python", - "request": "attach", - "localRoot": "${workspaceRoot}", - "remoteRoot": "${workspaceRoot}", - "port": 3000, - "secret": "my_secret", - "host": "localhost" } ] } @@ -583,15 +907,41 @@ "type": "object", "title": "Python Configuration", "properties": { + "python.promptToInstallJupyter": { + "type": "boolean", + "default": true, + "description": "Display prompt to install Jupyter Extension.", + "scope": "resource" + }, "python.pythonPath": { "type": "string", "default": "python", - "description": "Path to Python, you can use a custom version of Python by modifying this setting to include the full path." + "description": "Path to Python, you can use a custom version of Python by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.venvPath": { + "type": "string", + "default": "", + "description": "Path to folder with a list of Virtual Environments (e.g. ~/.pyenv, ~/Envs, ~/.virtualenvs).", + "scope": "resource" + }, + "python.envFile": { + "type": "string", + "description": "Absolute path to a file containing environment variable definitions.", + "default": "${workspaceRoot}/.env", + "scope": "resource" }, "python.jediPath": { "type": "string", "default": "", - "description": "Path to directory containing the Jedi library (this path will contain the 'Jedi' sub directory)." + "description": "Path to directory containing the Jedi library (this path will contain the 'Jedi' sub directory).", + "scope": "resource" + }, + "python.sortImports.path": { + "type": "string", + "description": "Path to isort script, default using inner version", + "default": "", + "scope": "resource" }, "python.sortImports.args": { "type": "array", @@ -599,57 +949,94 @@ "default": [], "items": { "type": "string" - } + }, + "scope": "resource" + }, + "python.disablePromptForFeatures": { + "type": "array", + "default": [], + "description": "Do not display a prompt to install these features", + "items": { + "type": "string", + "default": "pylint", + "description": "Feature", + "enum": [ + "flake8", + "mypy", + "pep8", + "pylama", + "prospector", + "pydocstyle", + "pylint" + ] + }, + "scope": "resource" }, "python.linting.enabled": { "type": "boolean", "default": true, - "description": "Whether to lint Python files." + "description": "Whether to lint Python files.", + "scope": "resource" + }, + "python.linting.enabledWithoutWorkspace": { + "type": "boolean", + "default": true, + "description": "Whether to lint Python files when no workspace is opened.", + "scope": "resource" }, "python.linting.prospectorEnabled": { "type": "boolean", "default": false, - "description": "Whether to lint Python files using prospector." + "description": "Whether to lint Python files using prospector.", + "scope": "resource" }, "python.linting.pylintEnabled": { "type": "boolean", "default": true, - "description": "Whether to lint Python files using pylint." + "description": "Whether to lint Python files using pylint.", + "scope": "resource" }, "python.linting.pep8Enabled": { "type": "boolean", "default": false, - "description": "Whether to lint Python files using pep8" + "description": "Whether to lint Python files using pep8", + "scope": "resource" }, "python.linting.flake8Enabled": { "type": "boolean", "default": false, - "description": "Whether to lint Python files using flake8" + "description": "Whether to lint Python files using flake8", + "scope": "resource" }, "python.linting.pydocstyleEnabled": { "type": "boolean", "default": false, - "description": "Whether to lint Python files using pydocstyle" + "description": "Whether to lint Python files using pydocstyle", + "scope": "resource" }, "python.linting.mypyEnabled": { "type": "boolean", "default": false, - "description": "Whether to lint Python files using mypy." + "description": "Whether to lint Python files using mypy.", + "scope": "resource" }, "python.linting.lintOnTextChange": { "type": "boolean", "default": true, - "description": "Whether to lint Python files when modified." + "description": "Whether to lint Python files when modified.", + "scope": "resource" }, "python.linting.lintOnSave": { "type": "boolean", "default": true, - "description": "Whether to lint Python files when saved." + "description": "Whether to lint Python files when saved.", + "scope": "resource" }, "python.linting.maxNumberOfProblems": { "type": "number", "default": 100, - "description": "Controls the maximum number of problems produced by the server." + "description": "Controls the maximum number of problems produced by the server.", + "scope": "resource" }, "python.linting.pylintCategorySeverity.convention": { "type": "string", @@ -660,7 +1047,8 @@ "Error", "Information", "Warning" - ] + ], + "scope": "resource" }, "python.linting.pylintCategorySeverity.refactor": { "type": "string", @@ -671,7 +1059,8 @@ "Error", "Information", "Warning" - ] + ], + "scope": "resource" }, "python.linting.pylintCategorySeverity.warning": { "type": "string", @@ -682,7 +1071,8 @@ "Error", "Information", "Warning" - ] + ], + "scope": "resource" }, "python.linting.pylintCategorySeverity.error": { "type": "string", @@ -693,7 +1083,8 @@ "Error", "Information", "Warning" - ] + ], + "scope": "resource" }, "python.linting.pylintCategorySeverity.fatal": { "type": "string", @@ -704,37 +1095,128 @@ "Error", "Information", "Warning" - ] + ], + "scope": "resource" + }, + "python.linting.pep8CategorySeverity.W": { + "type": "string", + "default": "Warning", + "description": "Severity of Pep8 message type 'W'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], + "scope": "resource" + }, + "python.linting.pep8CategorySeverity.E": { + "type": "string", + "default": "Error", + "description": "Severity of Pep8 message type 'E'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], + "scope": "resource" + }, + "python.linting.flake8CategorySeverity.F": { + "type": "string", + "default": "Error", + "description": "Severity of Flake8 message type 'F'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], + "scope": "resource" + }, + "python.linting.flake8CategorySeverity.E": { + "type": "string", + "default": "Error", + "description": "Severity of Flake8 message type 'E'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], + "scope": "resource" + }, + "python.linting.flake8CategorySeverity.W": { + "type": "string", + "default": "Warning", + "description": "Severity of Flake8 message type 'W'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], + "scope": "resource" + }, + "python.linting.mypyCategorySeverity.error": { + "type": "string", + "default": "Error", + "description": "Severity of Mypy message type 'Error'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], + "scope": "resource" + }, + "python.linting.mypyCategorySeverity.note": { + "type": "string", + "default": "Information", + "description": "Severity of Mypy message type 'Note'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], + "scope": "resource" }, "python.linting.prospectorPath": { "type": "string", "default": "prospector", - "description": "Path to Prospector, you can use a custom version of prospector by modifying this setting to include the full path." + "description": "Path to Prospector, you can use a custom version of prospector by modifying this setting to include the full path.", + "scope": "resource" }, "python.linting.pylintPath": { "type": "string", "default": "pylint", - "description": "Path to Pylint, you can use a custom version of pylint by modifying this setting to include the full path." + "description": "Path to Pylint, you can use a custom version of pylint by modifying this setting to include the full path.", + "scope": "resource" }, "python.linting.pep8Path": { "type": "string", "default": "pep8", - "description": "Path to pep8, you can use a custom version of pep8 by modifying this setting to include the full path." + "description": "Path to pep8, you can use a custom version of pep8 by modifying this setting to include the full path.", + "scope": "resource" }, "python.linting.flake8Path": { "type": "string", "default": "flake8", - "description": "Path to flake8, you can use a custom version of flake8 by modifying this setting to include the full path." + "description": "Path to flake8, you can use a custom version of flake8 by modifying this setting to include the full path.", + "scope": "resource" }, "python.linting.pydocstylePath": { "type": "string", "default": "pydocstyle", - "description": "Path to pydocstyle, you can use a custom version of pydocstyle by modifying this setting to include the full path." + "description": "Path to pydocstyle, you can use a custom version of pydocstyle by modifying this setting to include the full path.", + "scope": "resource" }, "python.linting.mypyPath": { "type": "string", "default": "mypy", - "description": "Path to mypy, you can use a custom version of mypy by modifying this setting to include the full path." + "description": "Path to mypy, you can use a custom version of mypy by modifying this setting to include the full path.", + "scope": "resource" }, "python.linting.prospectorArgs": { "type": "array", @@ -742,7 +1224,8 @@ "default": [], "items": { "type": "string" - } + }, + "scope": "resource" }, "python.linting.pylintArgs": { "type": "array", @@ -750,7 +1233,8 @@ "default": [], "items": { "type": "string" - } + }, + "scope": "resource" }, "python.linting.pep8Args": { "type": "array", @@ -758,7 +1242,8 @@ "default": [], "items": { "type": "string" - } + }, + "scope": "resource" }, "python.linting.flake8Args": { "type": "array", @@ -766,7 +1251,8 @@ "default": [], "items": { "type": "string" - } + }, + "scope": "resource" }, "python.linting.pydocstyleArgs": { "type": "array", @@ -774,20 +1260,26 @@ "default": [], "items": { "type": "string" - } + }, + "scope": "resource" }, "python.linting.mypyArgs": { "type": "array", "description": "Arguments passed in. Each argument is a separate item in the array.", - "default": [], + "default": [ + "--ignore-missing-imports", + "--follow-imports=silent" + ], "items": { "type": "string" - } + }, + "scope": "resource" }, "python.linting.outputWindow": { "type": "string", "default": "Python", - "description": "The output window name for the linting messages, defaults to Python output window." + "description": "The output window name for the linting messages, defaults to Python output window.", + "scope": "resource" }, "python.formatting.provider": { "type": "string", @@ -795,18 +1287,22 @@ "description": "Provider for formatting. Possible options include 'autopep8' and 'yapf'.", "enum": [ "autopep8", - "yapf" - ] + "yapf", + "none" + ], + "scope": "resource" }, "python.formatting.autopep8Path": { "type": "string", "default": "autopep8", - "description": "Path to autopep8, you can use a custom version of autopep8 by modifying this setting to include the full path." + "description": "Path to autopep8, you can use a custom version of autopep8 by modifying this setting to include the full path.", + "scope": "resource" }, "python.formatting.yapfPath": { "type": "string", "default": "yapf", - "description": "Path to yapf, you can use a custom version of yapf by modifying this setting to include the full path." + "description": "Path to yapf, you can use a custom version of yapf by modifying this setting to include the full path.", + "scope": "resource" }, "python.formatting.autopep8Args": { "type": "array", @@ -814,7 +1310,8 @@ "default": [], "items": { "type": "string" - } + }, + "scope": "resource" }, "python.formatting.yapfArgs": { "type": "array", @@ -822,90 +1319,124 @@ "default": [], "items": { "type": "string" - } + }, + "scope": "resource" }, "python.formatting.formatOnSave": { "type": "boolean", "default": false, - "description": "Format the document upon saving." + "description": "Format the document upon saving.", + "scope": "resource" }, "python.formatting.outputWindow": { "type": "string", "default": "Python", - "description": "The output window name for the formatting messages, defaults to Python output window." + "description": "The output window name for the formatting messages, defaults to Python output window.", + "scope": "resource" + }, + "python.autoComplete.preloadModules": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "description": "Comma delimited list of modules preloaded to speed up Auto Complete (e.g. add Numpy, Pandas, etc, items slow to load when autocompleting).", + "scope": "resource" }, "python.autoComplete.extraPaths": { "type": "array", "default": [], - "description": "List of paths to libraries and the like that need to be imported by auto complete engine. E.g. when using Google App SDK, the paths are not in system path, hence need to be added into this list." + "description": "List of paths to libraries and the like that need to be imported by auto complete engine. E.g. when using Google App SDK, the paths are not in system path, hence need to be added into this list.", + "scope": "resource" }, "python.autoComplete.addBrackets": { "type": "boolean", "default": false, - "description": "Automatically add brackets for functions." + "description": "Automatically add brackets for functions.", + "scope": "resource" }, "python.workspaceSymbols.tagFilePath": { "type": "string", - "default": "${workspaceRoot}/tags", - "description": "Fully qualified path to tag file (exuberant ctag file), used to provide workspace symbols." + "default": "${workspaceRoot}/.vscode/tags", + "description": "Fully qualified path to tag file (exuberant ctag file), used to provide workspace symbols.", + "scope": "resource" }, "python.workspaceSymbols.enabled": { "type": "boolean", "default": true, - "description": "Set to 'false' to disable Workspace Symbol provider using ctags." + "description": "Set to 'false' to disable Workspace Symbol provider using ctags.", + "scope": "resource" }, "python.workspaceSymbols.rebuildOnStart": { "type": "boolean", "default": true, - "description": "Whether to re-build the tags file on start (deaults to true)." + "description": "Whether to re-build the tags file on start (defaults to true).", + "scope": "resource" }, "python.workspaceSymbols.rebuildOnFileSave": { "type": "boolean", "default": true, - "description": "Whether to re-build the tags file on when changes made to python files are saved." + "description": "Whether to re-build the tags file on when changes made to python files are saved.", + "scope": "resource" }, "python.workspaceSymbols.ctagsPath": { "type": "string", "default": "ctags", - "description": "Fully qualilified path to the ctags executable (else leave as ctags, assuming it is in current path)." + "description": "Fully qualilified path to the ctags executable (else leave as ctags, assuming it is in current path).", + "scope": "resource" }, "python.workspaceSymbols.exclusionPatterns": { "type": "array", - "default": [], + "default": [ + "**/site-packages/**" + ], "items": { "type": "string" }, - "description": "Pattern used to exclude files and folders from ctags (see http://ctags.sourceforge.net/ctags.html)." + "description": "Pattern used to exclude files and folders from ctags See http://ctags.sourceforge.net/ctags.html.", + "scope": "resource" }, "python.unitTest.promptToConfigure": { "type": "boolean", "default": true, - "description": "Where to prompt to configure a test framework if potential tests directories are discovered." + "description": "Where to prompt to configure a test framework if potential tests directories are discovered.", + "scope": "resource" }, "python.unitTest.debugPort": { "type": "number", "default": 3000, - "description": "Port number used for debugging of unittests." + "description": "Port number used for debugging of unittests.", + "scope": "resource" + }, + "python.unitTest.cwd": { + "type": "string", + "default": null, + "description": "Optional working directory for unit tests.", + "scope": "resource" }, "python.unitTest.nosetestsEnabled": { "type": "boolean", "default": false, - "description": "Whether to enable or disable unit testing using nosetests." + "description": "Whether to enable or disable unit testing using nosetests.", + "scope": "resource" }, "python.unitTest.nosetestPath": { "type": "string", "default": "nosetests", - "description": "Path to nosetests, you can use a custom version of nosetests by modifying this setting to include the full path." + "description": "Path to nosetests, you can use a custom version of nosetests by modifying this setting to include the full path.", + "scope": "resource" }, "python.unitTest.pyTestEnabled": { "type": "boolean", "default": false, - "description": "Whether to enable or disable unit testing using pytest." + "description": "Whether to enable or disable unit testing using pytest.", + "scope": "resource" }, "python.unitTest.pyTestPath": { "type": "string", "default": "py.test", - "description": "Path to pytest (py.test), you can use a custom version of pytest by modifying this setting to include the full path." + "description": "Path to pytest (py.test), you can use a custom version of pytest by modifying this setting to include the full path.", + "scope": "resource" }, "python.unitTest.nosetestArgs": { "type": "array", @@ -913,7 +1444,8 @@ "default": [], "items": { "type": "string" - } + }, + "scope": "resource" }, "python.unitTest.pyTestArgs": { "type": "array", @@ -921,12 +1453,14 @@ "default": [], "items": { "type": "string" - } + }, + "scope": "resource" }, "python.unitTest.unittestEnabled": { "type": "boolean", "default": false, - "description": "Whether to enable or disable unit testing using unittest." + "description": "Whether to enable or disable unit testing using unittest.", + "scope": "resource" }, "python.unitTest.unittestArgs": { "type": "array", @@ -940,7 +1474,8 @@ ], "items": { "type": "string" - } + }, + "scope": "resource" }, "python.linting.ignorePatterns": { "type": "array", @@ -951,17 +1486,20 @@ ], "items": { "type": "string" - } + }, + "scope": "resource" }, "python.linting.pylamaEnabled": { "type": "boolean", "default": false, - "description": "Whether to lint Python files using pylama." + "description": "Whether to lint Python files using pylama.", + "scope": "resource" }, "python.linting.pylamaPath": { "type": "string", "default": "pylama", - "description": "Path to pylama, you can use a custom version of pylama by modifying this setting to include the full path." + "description": "Path to pylama, you can use a custom version of pylama by modifying this setting to include the full path.", + "scope": "resource" }, "python.linting.pylamaArgs": { "type": "array", @@ -969,32 +1507,38 @@ "default": [], "items": { "type": "string" - } + }, + "scope": "resource" }, "python.unitTest.outputWindow": { "type": "string", "default": "Python Test Log", - "description": "The output window name for the unit test messages, defaults to Python output window." + "description": "The output window name for the unit test messages, defaults to Python output window.", + "scope": "resource" }, "python.terminal.executeInFileDir": { "type": "boolean", "default": false, - "description": "When executing a file in the terminal, whether to use execute in the file's directory, instead of the current open folder." + "description": "When executing a file in the terminal, whether to use execute in the file's directory, instead of the current open folder.", + "scope": "resource" }, "python.terminal.launchArgs": { "type": "array", "default": [], - "description": "Python launch arguments to use when executing a file in the terminal." + "description": "Python launch arguments to use when executing a file in the terminal.", + "scope": "resource" }, "python.jupyter.appendResults": { "type": "boolean", "default": true, - "description": "Whether to appen the results to results window, else clear and display." + "description": "Whether to appen the results to results window, else clear and display.", + "scope": "resource" }, "python.jupyter.defaultKernel": { "type": "string", "default": "", - "description": "Default kernel to be used. By default the first available kernel is used." + "description": "Default kernel to be used. By default the first available kernel is used.", + "scope": "resource" }, "python.jupyter.startupCode": { "type": "array", @@ -1004,60 +1548,108 @@ "default": [ "%matplotlib inline" ], - "description": "Code executed when the kernel starts. Such as the default of '%matplotlib inline'. Individual lines can be placed in separate items of the array." + "description": "Code executed when the kernel starts. Such as the default of '%matplotlib inline'. Individual lines can be placed in separate items of the array.", + "scope": "resource" } } - } + }, + "languages": [ + { + "id": "pip-requirements", + "aliases": [ + "pip requirements", + "requirements.txt" + ], + "filenames": [ + "requirements.txt" + ], + "filenamePatterns": [ + "*-requirements.txt", + "requirements-*.txt" + ], + "configuration": "./languages/pip-requirements.json" + } + ], + "grammars": [ + { + "language": "pip-requirements", + "scopeName": "source.pip-requirements", + "path": "./syntaxes/pip-requirements.tmLanguage.json" + } + ] }, "scripts": { "vscode:prepublish": "tsc -p ./ && webpack", "compile": "webpack && tsc -watch -p ./", "postinstall": "node ./node_modules/vscode/bin/install", - "test": "node ./node_modules/vscode/bin/test" + "test": "node ./out/test/standardTest.js && node ./out/test/multiRootTest.js", + "precommit": "node gulpfile.js", + "lint-staged": "node gulpfile.js", + "lint": "tslint src/**/*.ts -t verbose" }, "dependencies": { - "anser": "^1.1.0", - "copy-paste": "^1.3.0", "diff-match-patch": "^1.0.0", - "fs-extra": "^0.30.0", + "fs-extra": "^4.0.2", "fuzzy": "^0.1.3", "line-by-line": "^0.1.5", + "lodash": "^4.17.4", "minimatch": "^3.0.3", "named-js-regexp": "^1.3.1", - "node-static": "^0.7.9", - "prepend-file": "^1.3.0", "rx": "^4.1.0", + "semver": "^5.4.1", "socket.io": "^1.4.8", "tmp": "0.0.29", - "transformime": "^3.1.2", - "transformime-marked": "0.0.1", "tree-kill": "^1.1.0", "uint64be": "^1.0.1", + "untildify": "^3.0.2", "vscode-debugadapter": "^1.0.1", "vscode-debugprotocol": "^1.0.1", - "vscode-extension-telemetry": "0.0.5", - "vscode-languageclient": "^1.1.0", - "vscode-languageserver": "^1.1.0", + "vscode-extension-telemetry": "^0.0.5", + "vscode-languageclient": "^3.1.0", + "vscode-languageserver": "^3.1.0", + "winreg": "^1.2.4", "xml2js": "^0.4.17" }, "devDependencies": { + "@types/chai": "^4.0.4", + "@types/chai-as-promised": "^7.1.0", + "@types/fs-extra": "^4.0.2", "@types/jquery": "^1.10.31", - "@types/mocha": "^2.2.32", + "@types/lodash": "^4.14.74", + "@types/mocha": "^2.2.43", "@types/node": "^6.0.40", "@types/rx": "^2.5.33", + "@types/semver": "^5.4.0", + "@types/sinon": "^2.3.2", "@types/socket.io": "^1.4.27", "@types/socket.io-client": "^1.4.27", "@types/uuid": "^3.3.27", + "@types/winreg": "^1.2.30", + "@types/xml2js": "^0.4.0", "babel-core": "^6.14.0", "babel-loader": "^6.2.5", "babel-preset-es2015": "^6.14.0", + "chai": "^4.1.2", + "chai-as-promised": "^7.1.1", + "event-stream": "^3.3.4", + "gulp": "^3.9.1", + "gulp-filter": "^5.0.1", + "gulp-typescript": "^3.2.2", + "husky": "^0.14.3", "ignore-loader": "^0.1.1", "mocha": "^2.3.3", + "relative": "^3.0.2", "retyped-diff-match-patch-tsd-ambient": "^1.0.0-0", - "ts-loader": "^0.8.2", - "tslint": "^3.15.1", - "typescript": "^2.0.3", - "vscode": "^1.0.0", - "webpack": "^1.13.2" + "sinon": "^2.3.6", + "transformime": "^3.1.2", + "transformime-marked": "0.0.1", + "ts-loader": "^2.3.4", + "tslint": "^5.7.0", + "tslint-eslint-rules": "^4.1.1", + "tslint-microsoft-contrib": "^5.0.1", + "typescript": "^2.5.2", + "typescript-formatter": "^6.0.0", + "webpack": "^1.13.2", + "vscode": "^1.1.5" } -} \ No newline at end of file +} diff --git a/package.nls.json b/package.nls.json new file mode 100644 index 000000000000..cf092729a945 --- /dev/null +++ b/package.nls.json @@ -0,0 +1,26 @@ +{ + "python.snippet.launch.standard.label": "Python", + "python.snippet.launch.standard.description": "Debug a Python program with standard output", + "python.snippet.launch.pyspark.label": "Python: PySpark", + "python.snippet.launch.pyspark.description": "Debug PySpark", + "python.snippet.launch.module.label": "Python: Module", + "python.snippet.launch.module.description": "Debug a Python Module", + "python.snippet.launch.terminal.label": "Python: Terminal (integrated)", + "python.snippet.launch.terminal.description": "Debug a Python program with Integrated Terminal/Console", + "python.snippet.launch.externalTerminal.label": "Python: Terminal (external)", + "python.snippet.launch.externalTerminal.description": "Debug a Python program with External Terminal/Console", + "python.snippet.launch.django.label": "Python: Django", + "python.snippet.launch.django.description": "Debug a Django Application", + "python.snippet.launch.flask.label": "Python: Flask (0.11.x or later)", + "python.snippet.launch.flask.description": "Debug a Flask Application", + "python.snippet.launch.flaskOld.label": "Python: Flask (0.10.x or earlier)", + "python.snippet.launch.flaskOld.description": "Debug an older styled Flask Application", + "python.snippet.launch.pyramid.label": "Python: Pyramid Application", + "python.snippet.launch.pyramid.description": "Debug a Pyramid Application", + "python.snippet.launch.watson.label": "Python: Watson Application", + "python.snippet.launch.watson.description": "Debug a Watson Application", + "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" +} diff --git a/pythonFiles/PythonTools/ipythonServer.py b/pythonFiles/PythonTools/ipythonServer.py index 99ff5dc7ac6c..d909eb49a688 100644 --- a/pythonFiles/PythonTools/ipythonServer.py +++ b/pythonFiles/PythonTools/ipythonServer.py @@ -54,8 +54,8 @@ except ImportError: from queue import Empty, Queue # Python 3 -DEBUG = os.environ.get('DEBUG_DJAYAMANNE_IPYTHON', '0') == '1' -TEST = os.environ.get('PYTHON_DONJAYAMANNE_TEST', '0') == '1' +DEBUG = os.environ.get('DEBUG_EXTENSION_IPYTHON', '0') == '1' +TEST = os.environ.get('VSC_PYTHON_CI_TEST', '0') == '1' # The great "support IPython 2, 3, 4" strat begins if not TEST: diff --git a/pythonFiles/PythonTools/ptvsd/__init__.py b/pythonFiles/PythonTools/ptvsd/__init__.py index 98acce6c9de1..25765e165f14 100644 --- a/pythonFiles/PythonTools/ptvsd/__init__.py +++ b/pythonFiles/PythonTools/ptvsd/__init__.py @@ -19,4 +19,4 @@ __all__ = ['enable_attach', 'wait_for_attach', 'break_into_debugger', 'settrace', 'is_attached', 'AttachAlreadyEnabledError'] -from ptvsd.attach_server import enable_attach, wait_for_attach, break_into_debugger, settrace, is_attached, AttachAlreadyEnabledError +from ptvsd.attach_server import enable_attach, wait_for_attach, break_into_debugger, settrace, is_attached, AttachAlreadyEnabledError \ No newline at end of file diff --git a/pythonFiles/PythonTools/ptvsd/__main__.py b/pythonFiles/PythonTools/ptvsd/__main__.py index 2837de743528..8fc5004aa43a 100644 --- a/pythonFiles/PythonTools/ptvsd/__main__.py +++ b/pythonFiles/PythonTools/ptvsd/__main__.py @@ -54,4 +54,4 @@ DONT_DEBUG.append(os.path.normcase(__file__)) sys.argv = script_argv -exec_file(script_argv[0], {'__name__': '__main__'}) +exec_file(script_argv[0], {'__name__': '__main__'}) \ No newline at end of file diff --git a/pythonFiles/PythonTools/ptvsd/setup.py b/pythonFiles/PythonTools/ptvsd/setup.py index 1c5aa2b96686..39f1b7b5d3be 100644 --- a/pythonFiles/PythonTools/ptvsd/setup.py +++ b/pythonFiles/PythonTools/ptvsd/setup.py @@ -18,17 +18,17 @@ from distutils.core import setup setup(name='ptvsd', - version='3.0.0rc1', - description='Python Tools for Visual Studio remote debugging server', + version='3.0.0', + description='Visual Studio remote debugging server for Python', license='Apache License 2.0', author='Microsoft Corporation', author_email='ptvshelp@microsoft.com', url='https://aka.ms/ptvs', classifiers=[ - 'Development Status :: 4 - Beta', + 'Development Status :: 5 - Production/Stable', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3', 'License :: OSI Approved :: Apache Software License'], packages=['ptvsd'] - ) + ) \ No newline at end of file diff --git a/pythonFiles/PythonTools/ptvsd/visualstudio_py_debugger.py b/pythonFiles/PythonTools/ptvsd/visualstudio_py_debugger.py index 97f7b387e1f5..b589a8208baa 100644 --- a/pythonFiles/PythonTools/ptvsd/visualstudio_py_debugger.py +++ b/pythonFiles/PythonTools/ptvsd/visualstudio_py_debugger.py @@ -1,19 +1,25 @@ - # ############################################################################ - # - # Copyright (c) Microsoft Corporation. - # - # This source code is subject to terms and conditions of the Apache License, Version 2.0. A - # copy of the license can be found in the License.html file at the root of this distribution. If - # you cannot locate the Apache License, Version 2.0, please send an email to - # vspython@microsoft.com. By using this source code in any fashion, you are agreeing to be bound - # by the terms of the Apache License, Version 2.0. - # - # You must not remove this notice, or any other, from this software. - # - # ########################################################################### +# Python Tools for Visual Studio +# Copyright(c) Microsoft Corporation +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may not use +# this file except in compliance with the License. You may obtain a copy of the +# License at http://www.apache.org/licenses/LICENSE-2.0 +# +# THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS +# OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY +# IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +# MERCHANTABLITY OR NON-INFRINGEMENT. +# +# See the Apache Version 2.0 License for specific language governing +# permissions and limitations under the License. +# With number of modifications by Don Jayamanne from __future__ import with_statement +__author__ = "Microsoft Corporation " +__version__ = "3.0.0.0" + # This module MUST NOT import threading in global scope. This is because in a direct (non-ptvsd) # attach scenario, it is loaded on the injected debugger attach thread, and if threading module # hasn't been loaded already, it will assume that the thread on which it is being loaded is the @@ -114,6 +120,9 @@ def thread_creator(func, args, kwargs = {}, *extra_args): DEBUG_STDLIB = False DJANGO_DEBUG = False +RICH_EXCEPTIONS = False +IGNORE_DJANGO_TEMPLATE_WARNINGS = False + # Py3k compat - alias unicode to str try: unicode @@ -169,6 +178,26 @@ def __len__(self): BREAKPOINT_PASS_COUNT_WHEN_EQUAL = 2 BREAKPOINT_PASS_COUNT_WHEN_EQUAL_OR_GREATER = 3 +## Begin modification by Don Jayamanne +DJANGO_VERSIONS_IDENTIFIED = False +IS_DJANGO18 = False +IS_DJANGO19 = False +IS_DJANGO19_OR_HIGHER = False + +try: + dict_contains = dict.has_key +except: + try: + #Py3k does not have has_key anymore, and older versions don't have __contains__ + dict_contains = dict.__contains__ + except: + try: + dict_contains = dict.has_key + except NameError: + def dict_contains(d, key): + return d.has_key(key) +## End modification by Don Jayamanne + class BreakpointInfo(object): __slots__ = [ 'breakpoint_id', 'filename', 'lineno', 'condition_kind', 'condition', @@ -330,6 +359,7 @@ class StackOverflowException(Exception): pass EXTT = to_bytes('EXTT') EXIT = to_bytes('EXIT') EXCP = to_bytes('EXCP') +EXC2 = to_bytes('EXC2') MODL = to_bytes('MODL') STPD = to_bytes('STPD') BRKS = to_bytes('BRKS') @@ -455,8 +485,7 @@ def should_break(self, thread, ex_type, ex_value, trace): if break_type: if issubclass(ex_type, SystemExit): if not BREAK_ON_SYSTEMEXIT_ZERO: - if ((isinstance(ex_value, int) and not ex_value) or - (isinstance(ex_value, SystemExit) and not ex_value.code)): + if not ex_value or (isinstance(ex_value, SystemExit) and not ex_value.code): break_type = BREAK_TYPE_NONE return break_type @@ -467,10 +496,10 @@ def is_handled(self, thread, ex_type, ex_value, trace): return False if trace.tb_next is not None: - if should_send_frame(trace.tb_next.tb_frame) and should_debug_code(trace.tb_next.tb_frame.f_code): - # don't break if this is not the top of the traceback, - # unless the previous frame was not debuggable - return True + if should_send_frame(trace.tb_next.tb_frame) and should_debug_code(trace.tb_next.tb_frame.f_code): + # don't break if this is not the top of the traceback, + # unless the previous frame was not debuggable + return True cur_frame = trace.tb_frame @@ -615,19 +644,57 @@ def update_all_thread_stacks(blocking_thread = None, check_is_blocked = True): cur_thread._block_starting_lock.release() DJANGO_BREAKPOINTS = {} +DJANGO_TEMPLATES = {} class DjangoBreakpointInfo(object): def __init__(self, filename): self._line_locations = None self.filename = filename self.breakpoints = {} + self.rangeIsPlainText = {} def add_breakpoint(self, lineno, brkpt_id): self.breakpoints[lineno] = brkpt_id def remove_breakpoint(self, lineno): - del self.breakpoints[lineno] - + try: + del self.breakpoints[lineno] + except: + pass + + def has_breakpoint_for_line(self, lineNumber): + return self.breakpoints.get(lineNumber) is not None + + def is_range_plain_text(self, start, end): + key = str(start) + '-' + str(end) + if self.rangeIsPlainText.get(key) is None: + # we need to calculate our line number offset information + try: + with open(self.filename, 'rb') as contents: + contents.seek(start, 0) # 0 = start of file, optional in this case + data = contents.read(end - start) + isPlainText = True + if data.startswith('{{') and data.endswith('}}'): + isPlainText = False + if data.startswith('{%') and data.endswith('%}'): + isPlainText = False + self.rangeIsPlainText[key] = isPlainText + return isPlainText + except: + return False + else: + return self.rangeIsPlainText.get(key) + + def line_number_to_offset(self, lineNumber): + line_locs = self.line_locations + if line_locs is not None: + low_line = line_locs[lineNumber - 1] + hi_line = line_locs[lineNumber] + + return low_line, hi_line + + return (None, None) + @property def line_locations(self): if self._line_locations is None: @@ -642,9 +709,12 @@ def line_locations(self): line_info = [] file_len = 0 for line in contents: + line_len = len(line) if not line_info and line.startswith(BOM_UTF8): - line = line[3:] # Strip the BOM, Django seems to ignore this... - file_len += len(line) + line_len -= len(BOM_UTF8) # Strip the BOM, Django seems to ignore this... + if line.endswith(to_bytes('\r\n')): + line_len -= 1 # Django normalizes newlines to \n + file_len += line_len line_info.append(file_len) contents.close() self._line_locations = line_info @@ -673,12 +743,43 @@ def should_break(self, start, end): return False, 0 def get_django_frame_source(frame): + global DJANGO_VERSIONS_IDENTIFIED + global IS_DJANGO18 + global IS_DJANGO19 + global IS_DJANGO19_OR_HIGHER + if DJANGO_VERSIONS_IDENTIFIED == False: + DJANGO_VERSIONS_IDENTIFIED = True + try: + import django + version = django.VERSION + IS_DJANGO18 = version[0] == 1 and version[1] == 8 + IS_DJANGO19 = version[0] == 1 and version[1] == 9 + IS_DJANGO19_OR_HIGHER = ((version[0] == 1 and version[1] >= 9) or version[0] > 1) + except: + pass if frame.f_code.co_name == 'render': self_obj = frame.f_locals.get('self', None) - if self_obj is not None and type(self_obj).__name__ != 'TextNode': - source_obj = getattr(self_obj, 'source', None) - if source_obj is not None: - return source_obj + if self_obj is None: + return None + + origin = _get_template_file_name(frame) + line = _get_template_line(frame) + position = None + + if self_obj is not None and hasattr(self_obj, 'token') and hasattr(self_obj.token, 'position'): + position = self_obj.token.position + + if origin is not None and position is None: + active_bps = DJANGO_BREAKPOINTS.get(origin.lower()) + if active_bps is None: + active_bps = DJANGO_TEMPLATES.get(origin.lower()) + if active_bps is None: + DJANGO_BREAKPOINTS[origin.lower()] = active_bps = DjangoBreakpointInfo(origin.lower()) + if active_bps is not None: + if line is not None: + position = active_bps.line_number_to_offset(line) + if origin and position: + return str(origin), position, line return None @@ -878,12 +979,15 @@ def handle_call(self, frame, arg): if DJANGO_BREAKPOINTS: source_obj = get_django_frame_source(frame) if source_obj is not None: - origin, (start, end) = source_obj - - active_bps = DJANGO_BREAKPOINTS.get(origin.name.lower()) + origin, (start, end), lineNumber = source_obj + + active_bps = DJANGO_BREAKPOINTS.get(origin.lower()) should_break = False - if active_bps is not None: + if active_bps is not None and origin != '': should_break, bkpt_id = active_bps.should_break(start, end) + isPlainText = active_bps.is_range_plain_text(start, end) + if isPlainText: + should_break = False if should_break: probe_stack() update_all_thread_stacks(self) @@ -1441,7 +1545,7 @@ def get_frame_list(self): frame_info = None if source_obj is not None: - origin, (start, end) = source_obj + origin, (start, end), lineNumber = source_obj filename = str(origin) bp_info = DJANGO_BREAKPOINTS.get(filename.lower()) @@ -1582,10 +1686,11 @@ class DebuggerLoop(object): instance = None - def __init__(self, conn): + def __init__(self, conn, rich_exceptions=False): DebuggerLoop.instance = self self.conn = conn self.repl_backend = None + self.rich_exceptions = rich_exceptions self.command_table = { to_bytes('stpi') : self.command_step_into, to_bytes('stpo') : self.command_step_out, @@ -1599,6 +1704,7 @@ def __init__(self, conn): to_bytes('brka') : self.command_break_all, to_bytes('resa') : self.command_resume_all, to_bytes('rest') : self.command_resume_thread, + to_bytes('thrf') : self.command_get_thread_frames, to_bytes('ares') : self.command_auto_resume, to_bytes('exec') : self.command_execute_code, to_bytes('chld') : self.command_enum_children, @@ -1782,6 +1888,13 @@ def command_break_all(self): SEND_BREAK_COMPLETE = True mark_all_threads_for_break() + def command_get_thread_frames(self): + tid = read_int(self.conn) + THREADS_LOCK.acquire() + thread = THREADS[tid] + THREADS_LOCK.release() + thread.enum_thread_frames_locally() + def command_resume_all(self): # resume all THREADS_LOCK.acquire() @@ -1992,14 +2105,45 @@ def report_exception(frame, exc_info, tid, break_type): # so we can get the correct msg. exc_value = exc_type(*exc_value) - excp_text = str(exc_value) + data = { + 'typename': get_exception_name(exc_type), + 'message': str(exc_value), + } + if break_type == 1: + data['breaktype'] = 'unhandled' + if tb_value: + try: + data['trace'] = '\n'.join(','.join(repr(v) for v in line) for line in traceback.extract_tb(tb_value)) + except: + pass + if not DJANGO_DEBUG or get_django_frame_source(frame) is None: + data['excvalue'] = '__exception_info' + i = 0 + while data['excvalue'] in frame.f_locals: + i += 1 + data['excvalue'] = '__exception_info_%d' % i + frame.f_locals[data['excvalue']] = { + 'exception': exc_value, + 'exception_type': exc_type, + 'message': data['message'], + } with _SendLockCtx: - write_bytes(conn, EXCP) - write_string(conn, exc_name) - write_int(conn, tid) - write_int(conn, break_type) - write_string(conn, excp_text) + if RICH_EXCEPTIONS: + write_bytes(conn, EXC2) + write_int(conn, tid) + write_int(conn, len(data)) + for key, value in data.items(): + write_string(conn, key) + write_string(conn, str(value)) + else: + # Old message is fixed format. If RichExceptions is not passed in + # debug options, we'll send this format. + write_bytes(conn, EXCP) + write_string(conn, str(data['typename'])) + write_int(conn, tid) + write_int(conn, 1 if 'breaktype' in data else 0) + write_string(conn, str(data['message'])) def new_module(frame): mod = Module(get_code_filename(frame.f_code)) @@ -2154,7 +2298,9 @@ def intercept_threads(for_attach = False): global _INTERCEPTING_FOR_ATTACH _INTERCEPTING_FOR_ATTACH = for_attach -def attach_process(port_num, debug_id, debug_options, report = False, block = False): +## Modified parameters by Don Jayamanne +# Accept current Process id to pass back to debugger +def attach_process(port_num, debug_id, debug_options, currentPid, report = False, block = False): global conn for i in xrange(50): try: @@ -2162,6 +2308,10 @@ def attach_process(port_num, debug_id, debug_options, report = False, block = Fa conn.connect(('127.0.0.1', port_num)) write_string(conn, debug_id) write_int(conn, 0) # success + ## Begin modification by Don Jayamanne + # Pass current Process id to pass back to debugger + write_int(conn, currentPid) # success + ## End Modification by Don Jayamanne break except: import time @@ -2172,9 +2322,11 @@ def attach_process(port_num, debug_id, debug_options, report = False, block = Fa def attach_process_from_socket(sock, debug_options, report = False, block = False): global conn, attach_sent_break, DETACHED, DEBUG_STDLIB, BREAK_ON_SYSTEMEXIT_ZERO, DJANGO_DEBUG + global RICH_EXCEPTIONS BREAK_ON_SYSTEMEXIT_ZERO = 'BreakOnSystemExitZero' in debug_options DJANGO_DEBUG = 'DjangoDebugging' in debug_options + IGNORE_DJANGO_TEMPLATE_WARNINGS = 'IgnoreDjangoTemplateWarnings' in debug_options if '' in PREFIXES: # If one or more of the prefixes are empty, we can't reliably distinguish stdlib @@ -2186,10 +2338,12 @@ def attach_process_from_socket(sock, debug_options, report = False, block = Fals wait_on_normal_exit = 'WaitOnNormalExit' in debug_options wait_on_abnormal_exit = 'WaitOnAbnormalExit' in debug_options + RICH_EXCEPTIONS = 'RichExceptions' in debug_options + def _excepthook(exc_type, exc_value, exc_tb): # Display the exception and wait on exit if exc_type is SystemExit: - if (wait_on_abnormal_exit and exc_value.code != 0) or (wait_on_normal_exit and exc_value.code == 0): + if (wait_on_abnormal_exit and exc_value.code) or (wait_on_normal_exit and not exc_value.code): print_exception(exc_type, exc_value, exc_tb) do_wait() else: @@ -2437,7 +2591,9 @@ def print_exception(exc_type, exc_value, exc_tb): def parse_debug_options(s): return set([opt.strip() for opt in s.split(',')]) -def debug(file, port_num, debug_id, debug_options, run_as = 'script'): +## Modified parameters by Don Jayamanne +# Accept current Process id to pass back to debugger +def debug(file, port_num, debug_id, debug_options, currentPid, run_as = 'script'): # remove us from modules so there's no trace of us sys.modules['$visualstudio_py_debugger'] = sys.modules['visualstudio_py_debugger'] __name__ = '$visualstudio_py_debugger' @@ -2445,7 +2601,10 @@ def debug(file, port_num, debug_id, debug_options, run_as = 'script'): wait_on_normal_exit = 'WaitOnNormalExit' in debug_options - attach_process(port_num, debug_id, debug_options, report = True) + ## Begin modification by Don Jayamanne + # Pass current Process id to pass back to debugger + attach_process(port_num, debug_id, debug_options, currentPid, report = True) + ## End Modification by Don Jayamanne # setup the current thread cur_thread = new_thread() @@ -2495,3 +2654,99 @@ def debug(file, port_num, debug_id, debug_options, run_as = 'script'): get_code(exec_code), get_code(new_thread_wrapper) )) + +## Begin modification by Don Jayamanne +def _read_file(filename): + f = open(filename, "r") + s = f.read() + f.close() + return s + + +def _offset_to_line_number(text, offset): + curLine = 1 + curOffset = 0 + while curOffset < offset: + if curOffset == len(text): + return -1 + c = text[curOffset] + if c == '\n': + curLine += 1 + elif c == '\r': + curLine += 1 + if curOffset < len(text) and text[curOffset + 1] == '\n': + curOffset += 1 + + curOffset += 1 + + return curLine + + +def _get_source_django_18_or_lower(frame): + # This method is usable only for the Django <= 1.8 + try: + node = frame.f_locals['self'] + if hasattr(node, 'source'): + return node.source + else: + if IGNORE_DJANGO_TEMPLATE_WARNINGS: + return None + + if IS_DJANGO18: + # The debug setting was changed since Django 1.8 + print("WARNING: Template path is not available. Set the 'debug' option in the OPTIONS of a DjangoTemplates " + "backend.") + else: + # The debug setting for Django < 1.8 + print("WARNING: Template path is not available. Please set TEMPLATE_DEBUG=True in your settings.py to make " + "django template breakpoints working") + return None + + except: + print(traceback.format_exc()) + return None + +def _get_template_file_name(frame): + try: + if IS_DJANGO19_OR_HIGHER: + # The Node source was removed since Django 1.9 + if dict_contains(frame.f_locals, 'context'): + context = frame.f_locals['context'] + if hasattr(context, 'template') and hasattr(context.template, 'origin') and \ + hasattr(context.template.origin, 'name'): + return context.template.origin.name + return None + source = _get_source_django_18_or_lower(frame) + if source is None: + if not IGNORE_DJANGO_TEMPLATE_WARNINGS: + print("Source is None\n") + return None + fname = source[0].name + + if fname == '': + if not IGNORE_DJANGO_TEMPLATE_WARNINGS: + print("Source name is %s\n" + fname) + return None + else: + abs_path_real_path_and_base = get_abs_path_real_path_and_base_from_file(fname) + return abs_path_real_path_and_base[1] + except: + print(traceback.format_exc()) + return None + + +def _get_template_line(frame): + if IS_DJANGO19_OR_HIGHER: + # The Node source was removed since Django 1.9 + self = frame.f_locals['self'] + if hasattr(self, 'token') and hasattr(self.token, 'lineno'): + return self.token.lineno + else: + return None + source = _get_source_django_18_or_lower(frame) + file_name = _get_template_file_name(frame) + try: + return _offset_to_line_number(_read_file(file_name), source[1][0]) + except: + return None +## End modification by Don Jayamanne \ No newline at end of file diff --git a/pythonFiles/PythonTools/ptvsd/visualstudio_py_repl.py b/pythonFiles/PythonTools/ptvsd/visualstudio_py_repl.py index 373dd95860c5..3618533910e1 100644 --- a/pythonFiles/PythonTools/ptvsd/visualstudio_py_repl.py +++ b/pythonFiles/PythonTools/ptvsd/visualstudio_py_repl.py @@ -1,19 +1,24 @@ -๏ปฟ # ############################################################################ - # - # Copyright (c) Microsoft Corporation. - # - # This source code is subject to terms and conditions of the Apache License, Version 2.0. A - # copy of the license can be found in the License.html file at the root of this distribution. If - # you cannot locate the Apache License, Version 2.0, please send an email to - # vspython@microsoft.com. By using this source code in any fashion, you are agreeing to be bound - # by the terms of the Apache License, Version 2.0. - # - # You must not remove this notice, or any other, from this software. - # - # ########################################################################### +๏ปฟ# Python Tools for Visual Studio +# Copyright(c) Microsoft Corporation +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may not use +# this file except in compliance with the License. You may obtain a copy of the +# License at http://www.apache.org/licenses/LICENSE-2.0 +# +# THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS +# OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY +# IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +# MERCHANTABLITY OR NON-INFRINGEMENT. +# +# See the Apache Version 2.0 License for specific language governing +# permissions and limitations under the License. from __future__ import with_statement +__author__ = "Microsoft Corporation " +__version__ = "3.0.0.0" + # This module MUST NOT import threading in global scope. This is because in a direct (non-ptvsd) # attach scenario, it is loaded on the injected debugger attach thread, and if threading module # hasn't been loaded already, it will assume that the thread on which it is being loaded is the @@ -158,6 +163,7 @@ class ReplBackend(object): _DETC = to_bytes('DETC') _DPNG = to_bytes('DPNG') _DXAM = to_bytes('DXAM') + _CHWD = to_bytes('CHWD') _MERR = to_bytes('MERR') _SERR = to_bytes('SERR') @@ -165,16 +171,16 @@ class ReplBackend(object): _EXIT = to_bytes('EXIT') _DONE = to_bytes('DONE') _MODC = to_bytes('MODC') - - def __init__(self): + + def __init__(self, *args, **kwargs): import threading self.conn = None self.send_lock = SafeSendLock() self.input_event = threading.Lock() - self.input_event.acquire() # lock starts acquired (we use it like a manual reset event) + self.input_event.acquire() # lock starts acquired (we use it like a manual reset event) self.input_string = None self.exit_requested = False - + def connect(self, port): self.conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.conn.connect(('127.0.0.1', port)) @@ -198,7 +204,7 @@ def _repl_loop(self): # the next command. self.flush() self.conn.settimeout(10) - + # 2.x raises SSLError in case of timeout (http://bugs.python.org/issue10272) if SSLError: timeout_exc_types = (socket.timeout, SSLError) @@ -217,7 +223,7 @@ def _repl_loop(self): if inp == '': break self.flush() - + cmd = ReplBackend._COMMANDS.get(inp) if cmd is not None: cmd(self) @@ -225,9 +231,9 @@ def _repl_loop(self): _debug_write('error in repl loop') _debug_write(traceback.format_exc()) self.exit_process() - + time.sleep(2) # try and exit gracefully, then interrupt main if necessary - + if sys.platform == 'cli': # just kill us as fast as possible import System @@ -289,7 +295,7 @@ def _cmd_sigs(self): write_string(self.conn, (doc or '')[:4096]) arg_count = len(args) + (vargs is not None) + (varkw is not None) write_int(self.conn, arg_count) - + def_values = [''] * (len(args) - len(defaults)) + ['=' + d for d in defaults] for arg, def_value in zip(args, def_values): write_string(self.conn, (arg or '') + def_value) @@ -297,7 +303,7 @@ def _cmd_sigs(self): write_string(self.conn, '*' + vargs) if varkw is not None: write_string(self.conn, '**' + varkw) - + def _cmd_setm(self): global exec_mod """sets the current module which code will execute against""" @@ -318,7 +324,7 @@ def _cmd_mods(self): res.sort() except: res = [] - + with self.send_lock: write_bytes(self.conn, ReplBackend._MODS) write_int(self.conn, len(res)) @@ -330,7 +336,7 @@ def _cmd_inpl(self): """handles the input command which returns a string of input""" self.input_string = read_string(self.conn) self.input_event.release() - + def _cmd_excf(self): """handles executing a single file""" filename = read_string(self.conn) @@ -402,19 +408,25 @@ def write_xaml(self, xaml_bytes): write_int(self.conn, len(xaml_bytes)) write_bytes(self.conn, xaml_bytes) - def send_prompt(self, ps1, ps2, update_all = True): + def send_prompt(self, ps1, ps2, allow_multiple_statements): """sends the current prompt to the interactive window""" with self.send_lock: write_bytes(self.conn, ReplBackend._PRPC) write_string(self.conn, ps1) write_string(self.conn, ps2) - write_int(self.conn, update_all) - + write_int(self.conn, 1 if allow_multiple_statements else 0) + + def send_cwd(self): + """sends the current working directory""" + with self.send_lock: + write_bytes(self.conn, ReplBackend._CHWD) + write_string(self.conn, os.getcwd()) + def send_error(self): """reports that an error occured to the interactive window""" with self.send_lock: write_bytes(self.conn, ReplBackend._ERRE) - + def send_exit(self): """reports the that the REPL process has exited to the interactive window""" with self.send_lock: @@ -423,7 +435,7 @@ def send_exit(self): def send_command_executed(self): with self.send_lock: write_bytes(self.conn, ReplBackend._DONE) - + def send_modules_changed(self): with self.send_lock: write_bytes(self.conn, ReplBackend._MODC) @@ -440,7 +452,7 @@ def write_stdout(self, value): with self.send_lock: write_bytes(self.conn, ReplBackend._STDO) write_string(self.conn, value) - + def write_stderr(self, value): """writes a string to standard input in the remote console""" with self.send_lock: @@ -449,15 +461,15 @@ def write_stderr(self, value): ################################################################ # Implementation of execution, etc... - + def execution_loop(self): """starts processing execution requests""" raise NotImplementedError - + def run_command(self, command): """runs the specified command which is a string containing code""" raise NotImplementedError - + def execute_file(self, filename, args): """executes the given filename as the main module""" return self.execute_file_ex('script', filename, args) @@ -469,7 +481,7 @@ def execute_file_ex(self, filetype, filename, args): def interrupt_main(self): """aborts the current running command""" raise NotImplementedError - + def exit_process(self): """exits the REPL process""" raise NotImplementedError @@ -477,7 +489,7 @@ def exit_process(self): def get_members(self, expression): """returns a tuple of the type name, instance members, and type members""" raise NotImplementedError - + def get_signatures(self, expression): """returns doc, args, vargs, varkw, defaults.""" raise NotImplementedError @@ -485,7 +497,7 @@ def get_signatures(self, expression): def set_current_module(self, module): """sets the module which code executes against""" raise NotImplementedError - + def set_current_thread_and_frame(self, thread_id, frame_id, frame_kind): """sets the current thread and frame which code will execute against""" raise NotImplementedError @@ -501,7 +513,7 @@ def flush(self): def attach_process(self, port, debugger_id, debug_options): """starts processing execution requests""" raise NotImplementedError - + def exit_work_item(): sys.exit(0) @@ -525,7 +537,7 @@ class ReplAbortException(Exception): pass from System import DBNull, ParamArrayAttribute builtin_method_descriptor_type = type(list.append) - + import System NamespaceType = type(System) @@ -539,7 +551,7 @@ class BasicReplBackend(ReplBackend): future_bits = 0x3e010 # code flags used to mark future bits """Basic back end which executes all Python code in-proc""" - def __init__(self, mod_name = '__main__', launch_file = None): + def __init__(self, mod_name='__main__'): import threading ReplBackend.__init__(self) if mod_name is not None: @@ -551,7 +563,6 @@ def __init__(self, mod_name = '__main__', launch_file = None): else: self.exec_mod = sys.modules['__main__'] - self.launch_file = launch_file self.code_flags = 0 self.execute_item = None self.execute_item_lock = threading.Lock() @@ -574,7 +585,6 @@ def connect_using_socket(self, socket): ReplBackend.connect_using_socket(self, socket) self.init_connection() - def run_file_as_main(self, filename, args): f = open(filename, 'rb') try: @@ -605,29 +615,29 @@ def python_executor(self, code): def func(): code.Execute(self.exec_mod) return func - + def execute_code_work_item(self): _debug_write('Executing: ' + repr(self.current_code)) stripped_code = self.current_code.strip() - - if sys.platform == 'cli': - code_to_send = '' - for line in stripped_code.split('\n'): - stripped = line.strip() - if (stripped.startswith('#') or not stripped) and not code_to_send: - continue - code_to_send += line + '\n' - - code = python_context.CreateSnippet(code_to_send, None, SourceCodeKind.InteractiveCode) - dispatcher = clr.GetCurrentRuntime().GetLanguage(PythonContext).GetCommandDispatcher() - if dispatcher is not None: - dispatcher(self.python_executor(code)) + if stripped_code: + if sys.platform == 'cli': + code_to_send = '' + for line in stripped_code.split('\n'): + stripped = line.strip() + if (stripped.startswith('#') or not stripped) and not code_to_send: + continue + code_to_send += line + '\n' + + code = python_context.CreateSnippet(code_to_send, None, SourceCodeKind.InteractiveCode) + dispatcher = clr.GetCurrentRuntime().GetLanguage(PythonContext).GetCommandDispatcher() + if dispatcher is not None: + dispatcher(self.python_executor(code)) + else: + code.Execute(self.exec_mod) else: - code.Execute(self.exec_mod) - else: - code = compile(self.current_code, '', 'single', self.code_flags) - self.code_flags |= (code.co_flags & BasicReplBackend.future_bits) - exec(code, self.exec_mod.__dict__, self.exec_mod.__dict__) + code = compile(self.current_code, '', 'single', self.code_flags) + self.code_flags |= (code.co_flags & BasicReplBackend.future_bits) + exec(code, self.exec_mod.__dict__, self.exec_mod.__dict__) self.current_code = None def run_one_command(self, cur_modules, cur_ps1, cur_ps2): @@ -643,33 +653,39 @@ def run_one_command(self, cur_modules, cur_ps1, cur_ps2): except: pass cur_modules = new_modules - + self.execute_item_lock.acquire() + cur_cwd = os.getcwd() if self.check_for_exit_execution_loop(): return True, None, None, None - + if self.execute_item is not None: try: self.execute_item() finally: self.execute_item = None - + try: self.send_command_executed() except SocketError: return True, None, None, None - + try: if cur_ps1 != sys.ps1 or cur_ps2 != sys.ps2: new_ps1 = str(sys.ps1) new_ps2 = str(sys.ps2) - - self.send_prompt(new_ps1, new_ps2) - + + self.send_prompt(new_ps1, new_ps2, allow_multiple_statements=False) + cur_ps1 = new_ps1 cur_ps2 = new_ps2 - except: + except Exception: + pass + try: + if cur_cwd != os.getcwd(): + self.send_cwd() + except Exception: pass except SystemExit: self.send_error() @@ -702,7 +718,7 @@ def run_one_command(self, cur_modules, cur_ps1, cur_ps2): except SocketError: _debug_write('err sending DONE') return True, None, None, None - + return False, cur_modules, cur_ps1, cur_ps2 def skip_internal_frames(self, tb): @@ -720,13 +736,13 @@ def is_internal_frame(self, tb): def execution_loop(self): """loop on the main thread which is responsible for executing code""" - + if sys.platform == 'cli' and sys.version_info[:3] < (2, 7, 1): # IronPython doesn't support thread.interrupt_main until 2.7.1 import System self.main_thread = System.Threading.Thread.CurrentThread - # save our selves so global lookups continue to work (required pre-2.6)... + # save ourselves so global lookups continue to work (required pre-2.6)... cur_modules = set() try: cur_ps1 = sys.ps1 @@ -736,15 +752,7 @@ def execution_loop(self): sys.ps1 = cur_ps1 = '>>> ' sys.ps2 = cur_ps2 = '... ' - self.send_prompt(cur_ps1, cur_ps2) - - # launch the startup script if one has been specified - if self.launch_file: - try: - self.run_file_as_main(self.launch_file, '') - except: - print('error in launching startup script:') - traceback.print_exc() + self.send_prompt(cur_ps1, cur_ps2, allow_multiple_statements=False) while True: exit, cur_modules, cur_ps1, cur_ps2 = self.run_one_command(cur_modules, cur_ps1, cur_ps2) @@ -859,7 +867,7 @@ def get_members(self, expression): else: val = eval(expression, self.exec_mod.__dict__, self.exec_mod.__dict__) members = dir(val) - + return self.collect_members(val, members, getattr_func) def collect_members(self, val, members, getattr_func): @@ -877,7 +885,7 @@ def collect_members(self, val, members, getattr_func): pass # collect the type members - + type_members = {} for mem_name in members: if mem_name not in inst_members: @@ -885,7 +893,7 @@ def collect_members(self, val, members, getattr_func): if mem_t is not None: type_members[mem_name] = mem_t - + return t.__module__ + '.' + t.__name__, inst_members, type_members def get_ipy_sig(self, obj, ctor): @@ -905,7 +913,7 @@ def get_ipy_sig(self, obj, ctor): defaults.append(repr(param.DefaultValue)) return obj.__doc__, args, vargs, varkw, tuple(defaults) - + def get_signatures(self, expression): if sys.platform == 'cli': code = python_context.CreateSnippet(expression, None, SourceCodeKind.AutoDetect) @@ -945,7 +953,7 @@ def collect_signatures(self, val): if remove_self: # remove self for instance methods and types args = args[1:] - + if defaults is not None: defaults = [repr(default) for default in defaults] else: @@ -960,7 +968,7 @@ def set_current_module(self, module): self.exec_mod = clr.GetClrType(type(sys)).GetProperty('Scope', System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(sys, ()) else: self.exec_mod = mod - else: + elif module: _debug_write('Unknown module ' + module) def get_module_names(self): @@ -971,13 +979,16 @@ def get_module_names(self): if sys.platform == 'cli' and type(module) is NamespaceType: self.get_namespaces(name, module, res) else: - filename = getattr(module, '__file__', '') or '' + try: + filename = os.path.abspath(module.__file__) + except Exception: + filename = None res.append((name, filename)) except: pass return res - + def get_namespaces(self, basename, namespace, names): names.append((basename, '')) try: @@ -989,7 +1000,7 @@ def get_namespaces(self, basename, namespace, names): self.get_namespaces(new_name, new_namespace, names) except: pass - + def flush(self): sys.stdout.flush() @@ -1003,10 +1014,10 @@ def execute_attach_process_work_item(): import visualstudio_py_debugger visualstudio_py_debugger.DETACH_CALLBACKS.append(self.do_detach) visualstudio_py_debugger.attach_process(port, debugger_id, debug_options, report=True, block=True) - + self.execute_item = execute_attach_process_work_item self.execute_item_lock.release() - + @staticmethod def get_type_name(val): try: @@ -1037,7 +1048,7 @@ def _get_member_type(inst, name, from_dict, getattr_func = None): class DebugReplBackend(BasicReplBackend): def __init__(self, debugger): - BasicReplBackend.__init__(self, None, None) + BasicReplBackend.__init__(self) self.debugger = debugger self.thread_id = None self.frame_id = None @@ -1087,7 +1098,8 @@ def execute_code_work_item(self): BasicReplBackend.execute_code_work_item(self) else: try: - self.debugger.execute_code_no_report(self.current_code, self.thread_id, self.frame_id, self.frame_kind) + if self.current_code and not self.current_code.isspace(): + self.debugger.execute_code_no_report(self.current_code, self.thread_id, self.frame_id, self.frame_kind) finally: self.current_code = None @@ -1169,6 +1181,8 @@ def check_for_exit_execution_loop(self): class _ReplOutput(object): """file like object which redirects output to the repl window.""" errors = None + closed = False + encoding = 'utf8' def __init__(self, backend, is_stdout, old_out = None): self.name = "" if is_stdout else "" @@ -1180,7 +1194,7 @@ def __init__(self, backend, is_stdout, old_out = None): def flush(self): if self.old_out: self.old_out.flush() - + def fileno(self): if self.pipe is None: self.pipe = os.pipe() @@ -1200,15 +1214,11 @@ def pipe_thread(self): else: self.write(data) - @property - def encoding(self): - return 'utf8' - def writelines(self, lines): for line in lines: self.write(line) self.write('\n') - + def write(self, value): _debug_write('printing ' + repr(value) + '\n') if self.is_stdout: @@ -1217,7 +1227,7 @@ def write(self, value): self.backend.write_stderr(value) if self.old_out: self.old_out.write(value) - + def isatty(self): return True @@ -1229,10 +1239,10 @@ class _ReplInput(object): """file like object which redirects input from the repl window""" def __init__(self, backend): self.backend = backend - + def readline(self): return self.backend.read_line() - + def readlines(self, size = None): res = [] while True: @@ -1241,12 +1251,12 @@ def readlines(self, size = None): res.append(line) else: break - + return res def xreadlines(self): return self - + def write(self, *args): raise IOError("File not open for writing") @@ -1267,7 +1277,7 @@ def next(self): class DotNetOutput(System.IO.TextWriter): def __new__(cls, backend, is_stdout, old_out=None): return System.IO.TextWriter.__new__(cls) - + def __init__(self, backend, is_stdout, old_out=None): self.backend = backend self.is_stdout = is_stdout @@ -1306,7 +1316,7 @@ def WriteLine(self, value, *args): @property def Encoding(self): return System.Text.Encoding.UTF8 - + BACKEND = None @@ -1315,17 +1325,15 @@ def _run_repl(): parser = OptionParser(prog='repl', description='Process REPL options') parser.add_option('--port', dest='port', - help='the port to connect back to') - parser.add_option('--launch_file', dest='launch_file', - help='the script file to run on startup') - parser.add_option('--execution_mode', dest='backend', - help='the backend to use') + help='the port to connect back to') + parser.add_option('--execution-mode', dest='backend', + help='the backend to use') parser.add_option('--enable-attach', dest='enable_attach', - action="store_true", default=False, - help='enable attaching the debugger via $attach') + action="store_true", default=False, + help='enable attaching the debugger via $attach') (options, args) = parser.parse_args() - + # kick off repl # make us available under our "normal" name, not just __main__ which we'll likely replace. sys.modules['visualstudio_py_repl'] = sys.modules['__main__'] @@ -1351,7 +1359,14 @@ def _run_repl(): sys.argv = args or [''] global BACKEND - BACKEND = backend_type(launch_file=options.launch_file) + try: + BACKEND = backend_type() + except UnsupportedReplException: + backend_error = sys.exc_info()[1].reason + BACKEND = BasicReplBackend() + except Exception: + backend_error = traceback.format_exc() + BACKEND = BasicReplBackend() BACKEND.connect(int(options.port)) if options.enable_attach: @@ -1373,4 +1388,4 @@ def _run_repl(): _debug_write(traceback.format_exc()) _debug_write('exiting') input() - raise + raise \ No newline at end of file diff --git a/pythonFiles/PythonTools/ptvsd/visualstudio_py_util.py b/pythonFiles/PythonTools/ptvsd/visualstudio_py_util.py index 536598606529..c00519eebb09 100644 --- a/pythonFiles/PythonTools/ptvsd/visualstudio_py_util.py +++ b/pythonFiles/PythonTools/ptvsd/visualstudio_py_util.py @@ -1,16 +1,21 @@ - # ############################################################################ - # - # Copyright (c) Microsoft Corporation. - # - # This source code is subject to terms and conditions of the Apache License, Version 2.0. A - # copy of the license can be found in the License.html file at the root of this distribution. If - # you cannot locate the Apache License, Version 2.0, please send an email to - # vspython@microsoft.com. By using this source code in any fashion, you are agreeing to be bound - # by the terms of the Apache License, Version 2.0. - # - # You must not remove this notice, or any other, from this software. - # - # ########################################################################### +# Python Tools for Visual Studio +# Copyright(c) Microsoft Corporation +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may not use +# this file except in compliance with the License. You may obtain a copy of the +# License at http://www.apache.org/licenses/LICENSE-2.0 +# +# THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS +# OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY +# IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +# MERCHANTABLITY OR NON-INFRINGEMENT. +# +# See the Apache Version 2.0 License for specific language governing +# permissions and limitations under the License. + +__author__ = "Microsoft Corporation " +__version__ = "3.0.0.0" # This module MUST NOT import threading in global scope. This is because in a direct (non-ptvsd) # attach scenario, it is loaded on the injected debugger attach thread, and if threading module @@ -82,7 +87,10 @@ def exec_code(code, file, global_variables): except AttributeError: pass - sys.path[0] = os.path.split(file)[0] + if os.path.isdir(sys.path[0]): + sys.path.insert(0, os.path.split(file)[0]) + else: + sys.path[0] = os.path.split(file)[0] code_obj = compile(code, file, 'exec') exec(code_obj, global_variables) @@ -126,7 +134,10 @@ def exec_module(module, global_variables): def read_bytes(conn, count): b = to_bytes('') while len(b) < count: - b += conn.recv(count - len(b)) + received_data = conn.recv(count - len(b)) + if received_data is None: + break + b += received_data return b @@ -617,4 +628,4 @@ def __repr__(self): return 'MyRepr' if __name__ == '__main__': print('Running tests...') - SafeRepr()._selftest() + SafeRepr()._selftest() \ No newline at end of file diff --git a/pythonFiles/PythonTools/visualstudio_ipython_repl.py b/pythonFiles/PythonTools/visualstudio_ipython_repl.py index d09e3988d9db..14f42362a8f8 100644 --- a/pythonFiles/PythonTools/visualstudio_ipython_repl.py +++ b/pythonFiles/PythonTools/visualstudio_ipython_repl.py @@ -172,7 +172,11 @@ def handle_execute_output(self, content): output = content['data'] execution_count = content['execution_count'] self._vs_backend.execution_count = execution_count + 1 - self._vs_backend.send_prompt('\r\nIn [%d]: ' % (execution_count + 1), ' ' + ('.' * (len(str(execution_count + 1)) + 2)) + ': ', False) + self._vs_backend.send_prompt( + '\r\nIn [%d]: ' % (execution_count + 1), + ' ' + ('.' * (len(str(execution_count + 1)) + 2)) + ': ', + allow_multiple_statements=True + ) self.write_data(output, execution_count) def write_data(self, data, execution_count = None): @@ -220,7 +224,11 @@ def handle_error(self, content): def handle_execute_input(self, content): # just a rebroadcast of the command to be executed, can be ignored self._vs_backend.execution_count += 1 - self._vs_backend.send_prompt('\r\nIn [%d]: ' % (self._vs_backend.execution_count), ' ' + ('.' * (len(str(self._vs_backend.execution_count)) + 2)) + ': ', False) + self._vs_backend.send_prompt( + '\r\nIn [%d]: ' % (self._vs_backend.execution_count), + ' ' + ('.' * (len(str(self._vs_backend.execution_count)) + 2)) + ': ', + allow_multiple_statements=True + ) pass def handle_status(self, content): @@ -309,13 +317,9 @@ def execute_file_as_main(self, filename, arg_string): self.run_command(code, True) def execution_loop(self): - # launch the startup script if one has been specified - if self.launch_file: - self.execute_file_as_main(self.launch_file, None) - # we've got a bunch of threads setup for communication, we just block # here until we're requested to exit. - self.send_prompt('\r\nIn [1]: ', ' ...: ', False) + self.send_prompt('\r\nIn [1]: ', ' ...: ', allow_multiple_statements=True) self.exit_lock.acquire() def run_command(self, command, silent = False): @@ -324,14 +328,17 @@ def run_command(self, command, silent = False): else: self.km.shell_channel.execute(command, silent) - def execute_file(self, filename, args): - self.execute_file_as_main(filename, args) + def execute_file_ex(self, filetype, filename, args): + if filetype == 'script': + self.execute_file_as_main(filename, args) + else: + raise NotImplementedError("Cannot execute %s file" % filetype) def exit_process(self): self.exit_lock.release() def get_members(self, expression): - """returns a tuple of the type name, instance members, and type members""" + """returns a tuple of the type name, instance members, and type members""" text = expression + '.' if is_ipython_versionorgreater(3, 0): self.km.complete(text) @@ -420,4 +427,4 @@ def do_detach(): class IPythonBackendWithoutPyLab(IPythonBackend): def get_extra_arguments(self): - return [] + return [] \ No newline at end of file diff --git a/pythonFiles/PythonTools/visualstudio_py_debugger.py b/pythonFiles/PythonTools/visualstudio_py_debugger.py index d7058da4dbc8..dc3f5643ce69 100644 --- a/pythonFiles/PythonTools/visualstudio_py_debugger.py +++ b/pythonFiles/PythonTools/visualstudio_py_debugger.py @@ -119,6 +119,8 @@ def thread_creator(func, args, kwargs = {}, *extra_args): BREAK_ON_SYSTEMEXIT_ZERO = False DEBUG_STDLIB = False DJANGO_DEBUG = False + +RICH_EXCEPTIONS = False IGNORE_DJANGO_TEMPLATE_WARNINGS = False # Py3k compat - alias unicode to str @@ -357,6 +359,7 @@ class StackOverflowException(Exception): pass EXTT = to_bytes('EXTT') EXIT = to_bytes('EXIT') EXCP = to_bytes('EXCP') +EXC2 = to_bytes('EXC2') MODL = to_bytes('MODL') STPD = to_bytes('STPD') BRKS = to_bytes('BRKS') @@ -493,10 +496,10 @@ def is_handled(self, thread, ex_type, ex_value, trace): return False if trace.tb_next is not None: - if should_send_frame(trace.tb_next.tb_frame) and should_debug_code(trace.tb_next.tb_frame.f_code): - # don't break if this is not the top of the traceback, - # unless the previous frame was not debuggable - return True + if should_send_frame(trace.tb_next.tb_frame) and should_debug_code(trace.tb_next.tb_frame.f_code): + # don't break if this is not the top of the traceback, + # unless the previous frame was not debuggable + return True cur_frame = trace.tb_frame @@ -1683,10 +1686,11 @@ class DebuggerLoop(object): instance = None - def __init__(self, conn): + def __init__(self, conn, rich_exceptions=False): DebuggerLoop.instance = self self.conn = conn self.repl_backend = None + self.rich_exceptions = rich_exceptions self.command_table = { to_bytes('stpi') : self.command_step_into, to_bytes('stpo') : self.command_step_out, @@ -2101,14 +2105,45 @@ def report_exception(frame, exc_info, tid, break_type): # so we can get the correct msg. exc_value = exc_type(*exc_value) - excp_text = str(exc_value) + data = { + 'typename': get_exception_name(exc_type), + 'message': str(exc_value), + } + if break_type == 1: + data['breaktype'] = 'unhandled' + if tb_value: + try: + data['trace'] = '\n'.join(','.join(repr(v) for v in line) for line in traceback.extract_tb(tb_value)) + except: + pass + if not DJANGO_DEBUG or get_django_frame_source(frame) is None: + data['excvalue'] = '__exception_info' + i = 0 + while data['excvalue'] in frame.f_locals: + i += 1 + data['excvalue'] = '__exception_info_%d' % i + frame.f_locals[data['excvalue']] = { + 'exception': exc_value, + 'exception_type': exc_type, + 'message': data['message'], + } with _SendLockCtx: - write_bytes(conn, EXCP) - write_string(conn, exc_name) - write_int(conn, tid) - write_int(conn, break_type) - write_string(conn, excp_text) + if RICH_EXCEPTIONS: + write_bytes(conn, EXC2) + write_int(conn, tid) + write_int(conn, len(data)) + for key, value in data.items(): + write_string(conn, key) + write_string(conn, str(value)) + else: + # Old message is fixed format. If RichExceptions is not passed in + # debug options, we'll send this format. + write_bytes(conn, EXCP) + write_string(conn, str(data['typename'])) + write_int(conn, tid) + write_int(conn, 1 if 'breaktype' in data else 0) + write_string(conn, str(data['message'])) def new_module(frame): mod = Module(get_code_filename(frame.f_code)) @@ -2287,6 +2322,7 @@ def attach_process(port_num, debug_id, debug_options, currentPid, report = False def attach_process_from_socket(sock, debug_options, report = False, block = False): global conn, attach_sent_break, DETACHED, DEBUG_STDLIB, BREAK_ON_SYSTEMEXIT_ZERO, DJANGO_DEBUG + global RICH_EXCEPTIONS BREAK_ON_SYSTEMEXIT_ZERO = 'BreakOnSystemExitZero' in debug_options DJANGO_DEBUG = 'DjangoDebugging' in debug_options @@ -2302,6 +2338,8 @@ def attach_process_from_socket(sock, debug_options, report = False, block = Fals wait_on_normal_exit = 'WaitOnNormalExit' in debug_options wait_on_abnormal_exit = 'WaitOnAbnormalExit' in debug_options + RICH_EXCEPTIONS = 'RichExceptions' in debug_options + def _excepthook(exc_type, exc_value, exc_tb): # Display the exception and wait on exit if exc_type is SystemExit: diff --git a/pythonFiles/PythonTools/visualstudio_py_repl.py b/pythonFiles/PythonTools/visualstudio_py_repl.py index 4f7529fec542..74870fd696b3 100644 --- a/pythonFiles/PythonTools/visualstudio_py_repl.py +++ b/pythonFiles/PythonTools/visualstudio_py_repl.py @@ -163,6 +163,7 @@ class ReplBackend(object): _DETC = to_bytes('DETC') _DPNG = to_bytes('DPNG') _DXAM = to_bytes('DXAM') + _CHWD = to_bytes('CHWD') _MERR = to_bytes('MERR') _SERR = to_bytes('SERR') @@ -170,16 +171,16 @@ class ReplBackend(object): _EXIT = to_bytes('EXIT') _DONE = to_bytes('DONE') _MODC = to_bytes('MODC') - - def __init__(self): + + def __init__(self, *args, **kwargs): import threading self.conn = None self.send_lock = SafeSendLock() self.input_event = threading.Lock() - self.input_event.acquire() # lock starts acquired (we use it like a manual reset event) + self.input_event.acquire() # lock starts acquired (we use it like a manual reset event) self.input_string = None self.exit_requested = False - + def connect(self, port): self.conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.conn.connect(('127.0.0.1', port)) @@ -203,7 +204,7 @@ def _repl_loop(self): # the next command. self.flush() self.conn.settimeout(10) - + # 2.x raises SSLError in case of timeout (http://bugs.python.org/issue10272) if SSLError: timeout_exc_types = (socket.timeout, SSLError) @@ -222,7 +223,7 @@ def _repl_loop(self): if inp == '': break self.flush() - + cmd = ReplBackend._COMMANDS.get(inp) if cmd is not None: cmd(self) @@ -230,9 +231,9 @@ def _repl_loop(self): _debug_write('error in repl loop') _debug_write(traceback.format_exc()) self.exit_process() - + time.sleep(2) # try and exit gracefully, then interrupt main if necessary - + if sys.platform == 'cli': # just kill us as fast as possible import System @@ -294,7 +295,7 @@ def _cmd_sigs(self): write_string(self.conn, (doc or '')[:4096]) arg_count = len(args) + (vargs is not None) + (varkw is not None) write_int(self.conn, arg_count) - + def_values = [''] * (len(args) - len(defaults)) + ['=' + d for d in defaults] for arg, def_value in zip(args, def_values): write_string(self.conn, (arg or '') + def_value) @@ -302,7 +303,7 @@ def _cmd_sigs(self): write_string(self.conn, '*' + vargs) if varkw is not None: write_string(self.conn, '**' + varkw) - + def _cmd_setm(self): global exec_mod """sets the current module which code will execute against""" @@ -323,7 +324,7 @@ def _cmd_mods(self): res.sort() except: res = [] - + with self.send_lock: write_bytes(self.conn, ReplBackend._MODS) write_int(self.conn, len(res)) @@ -335,7 +336,7 @@ def _cmd_inpl(self): """handles the input command which returns a string of input""" self.input_string = read_string(self.conn) self.input_event.release() - + def _cmd_excf(self): """handles executing a single file""" filename = read_string(self.conn) @@ -407,19 +408,25 @@ def write_xaml(self, xaml_bytes): write_int(self.conn, len(xaml_bytes)) write_bytes(self.conn, xaml_bytes) - def send_prompt(self, ps1, ps2, update_all = True): + def send_prompt(self, ps1, ps2, allow_multiple_statements): """sends the current prompt to the interactive window""" with self.send_lock: write_bytes(self.conn, ReplBackend._PRPC) write_string(self.conn, ps1) write_string(self.conn, ps2) - write_int(self.conn, update_all) - + write_int(self.conn, 1 if allow_multiple_statements else 0) + + def send_cwd(self): + """sends the current working directory""" + with self.send_lock: + write_bytes(self.conn, ReplBackend._CHWD) + write_string(self.conn, os.getcwd()) + def send_error(self): """reports that an error occured to the interactive window""" with self.send_lock: write_bytes(self.conn, ReplBackend._ERRE) - + def send_exit(self): """reports the that the REPL process has exited to the interactive window""" with self.send_lock: @@ -428,7 +435,7 @@ def send_exit(self): def send_command_executed(self): with self.send_lock: write_bytes(self.conn, ReplBackend._DONE) - + def send_modules_changed(self): with self.send_lock: write_bytes(self.conn, ReplBackend._MODC) @@ -445,7 +452,7 @@ def write_stdout(self, value): with self.send_lock: write_bytes(self.conn, ReplBackend._STDO) write_string(self.conn, value) - + def write_stderr(self, value): """writes a string to standard input in the remote console""" with self.send_lock: @@ -454,15 +461,15 @@ def write_stderr(self, value): ################################################################ # Implementation of execution, etc... - + def execution_loop(self): """starts processing execution requests""" raise NotImplementedError - + def run_command(self, command): """runs the specified command which is a string containing code""" raise NotImplementedError - + def execute_file(self, filename, args): """executes the given filename as the main module""" return self.execute_file_ex('script', filename, args) @@ -474,7 +481,7 @@ def execute_file_ex(self, filetype, filename, args): def interrupt_main(self): """aborts the current running command""" raise NotImplementedError - + def exit_process(self): """exits the REPL process""" raise NotImplementedError @@ -482,7 +489,7 @@ def exit_process(self): def get_members(self, expression): """returns a tuple of the type name, instance members, and type members""" raise NotImplementedError - + def get_signatures(self, expression): """returns doc, args, vargs, varkw, defaults.""" raise NotImplementedError @@ -490,7 +497,7 @@ def get_signatures(self, expression): def set_current_module(self, module): """sets the module which code executes against""" raise NotImplementedError - + def set_current_thread_and_frame(self, thread_id, frame_id, frame_kind): """sets the current thread and frame which code will execute against""" raise NotImplementedError @@ -506,7 +513,7 @@ def flush(self): def attach_process(self, port, debugger_id, debug_options): """starts processing execution requests""" raise NotImplementedError - + def exit_work_item(): sys.exit(0) @@ -530,7 +537,7 @@ class ReplAbortException(Exception): pass from System import DBNull, ParamArrayAttribute builtin_method_descriptor_type = type(list.append) - + import System NamespaceType = type(System) @@ -544,7 +551,7 @@ class BasicReplBackend(ReplBackend): future_bits = 0x3e010 # code flags used to mark future bits """Basic back end which executes all Python code in-proc""" - def __init__(self, mod_name = '__main__', launch_file = None): + def __init__(self, mod_name='__main__'): import threading ReplBackend.__init__(self) if mod_name is not None: @@ -556,7 +563,6 @@ def __init__(self, mod_name = '__main__', launch_file = None): else: self.exec_mod = sys.modules['__main__'] - self.launch_file = launch_file self.code_flags = 0 self.execute_item = None self.execute_item_lock = threading.Lock() @@ -579,7 +585,6 @@ def connect_using_socket(self, socket): ReplBackend.connect_using_socket(self, socket) self.init_connection() - def run_file_as_main(self, filename, args): f = open(filename, 'rb') try: @@ -610,29 +615,29 @@ def python_executor(self, code): def func(): code.Execute(self.exec_mod) return func - + def execute_code_work_item(self): _debug_write('Executing: ' + repr(self.current_code)) stripped_code = self.current_code.strip() - - if sys.platform == 'cli': - code_to_send = '' - for line in stripped_code.split('\n'): - stripped = line.strip() - if (stripped.startswith('#') or not stripped) and not code_to_send: - continue - code_to_send += line + '\n' - - code = python_context.CreateSnippet(code_to_send, None, SourceCodeKind.InteractiveCode) - dispatcher = clr.GetCurrentRuntime().GetLanguage(PythonContext).GetCommandDispatcher() - if dispatcher is not None: - dispatcher(self.python_executor(code)) + if stripped_code: + if sys.platform == 'cli': + code_to_send = '' + for line in stripped_code.split('\n'): + stripped = line.strip() + if (stripped.startswith('#') or not stripped) and not code_to_send: + continue + code_to_send += line + '\n' + + code = python_context.CreateSnippet(code_to_send, None, SourceCodeKind.InteractiveCode) + dispatcher = clr.GetCurrentRuntime().GetLanguage(PythonContext).GetCommandDispatcher() + if dispatcher is not None: + dispatcher(self.python_executor(code)) + else: + code.Execute(self.exec_mod) else: - code.Execute(self.exec_mod) - else: - code = compile(self.current_code, '', 'single', self.code_flags) - self.code_flags |= (code.co_flags & BasicReplBackend.future_bits) - exec(code, self.exec_mod.__dict__, self.exec_mod.__dict__) + code = compile(self.current_code, '', 'single', self.code_flags) + self.code_flags |= (code.co_flags & BasicReplBackend.future_bits) + exec(code, self.exec_mod.__dict__, self.exec_mod.__dict__) self.current_code = None def run_one_command(self, cur_modules, cur_ps1, cur_ps2): @@ -648,33 +653,39 @@ def run_one_command(self, cur_modules, cur_ps1, cur_ps2): except: pass cur_modules = new_modules - + self.execute_item_lock.acquire() + cur_cwd = os.getcwd() if self.check_for_exit_execution_loop(): return True, None, None, None - + if self.execute_item is not None: try: self.execute_item() finally: self.execute_item = None - + try: self.send_command_executed() except SocketError: return True, None, None, None - + try: if cur_ps1 != sys.ps1 or cur_ps2 != sys.ps2: new_ps1 = str(sys.ps1) new_ps2 = str(sys.ps2) - - self.send_prompt(new_ps1, new_ps2) - + + self.send_prompt(new_ps1, new_ps2, allow_multiple_statements=False) + cur_ps1 = new_ps1 cur_ps2 = new_ps2 - except: + except Exception: + pass + try: + if cur_cwd != os.getcwd(): + self.send_cwd() + except Exception: pass except SystemExit: self.send_error() @@ -707,7 +718,7 @@ def run_one_command(self, cur_modules, cur_ps1, cur_ps2): except SocketError: _debug_write('err sending DONE') return True, None, None, None - + return False, cur_modules, cur_ps1, cur_ps2 def skip_internal_frames(self, tb): @@ -725,13 +736,13 @@ def is_internal_frame(self, tb): def execution_loop(self): """loop on the main thread which is responsible for executing code""" - + if sys.platform == 'cli' and sys.version_info[:3] < (2, 7, 1): # IronPython doesn't support thread.interrupt_main until 2.7.1 import System self.main_thread = System.Threading.Thread.CurrentThread - # save our selves so global lookups continue to work (required pre-2.6)... + # save ourselves so global lookups continue to work (required pre-2.6)... cur_modules = set() try: cur_ps1 = sys.ps1 @@ -741,15 +752,7 @@ def execution_loop(self): sys.ps1 = cur_ps1 = '>>> ' sys.ps2 = cur_ps2 = '... ' - self.send_prompt(cur_ps1, cur_ps2) - - # launch the startup script if one has been specified - if self.launch_file: - try: - self.run_file_as_main(self.launch_file, '') - except: - print('error in launching startup script:') - traceback.print_exc() + self.send_prompt(cur_ps1, cur_ps2, allow_multiple_statements=False) while True: exit, cur_modules, cur_ps1, cur_ps2 = self.run_one_command(cur_modules, cur_ps1, cur_ps2) @@ -864,7 +867,7 @@ def get_members(self, expression): else: val = eval(expression, self.exec_mod.__dict__, self.exec_mod.__dict__) members = dir(val) - + return self.collect_members(val, members, getattr_func) def collect_members(self, val, members, getattr_func): @@ -882,7 +885,7 @@ def collect_members(self, val, members, getattr_func): pass # collect the type members - + type_members = {} for mem_name in members: if mem_name not in inst_members: @@ -890,7 +893,7 @@ def collect_members(self, val, members, getattr_func): if mem_t is not None: type_members[mem_name] = mem_t - + return t.__module__ + '.' + t.__name__, inst_members, type_members def get_ipy_sig(self, obj, ctor): @@ -910,7 +913,7 @@ def get_ipy_sig(self, obj, ctor): defaults.append(repr(param.DefaultValue)) return obj.__doc__, args, vargs, varkw, tuple(defaults) - + def get_signatures(self, expression): if sys.platform == 'cli': code = python_context.CreateSnippet(expression, None, SourceCodeKind.AutoDetect) @@ -950,7 +953,7 @@ def collect_signatures(self, val): if remove_self: # remove self for instance methods and types args = args[1:] - + if defaults is not None: defaults = [repr(default) for default in defaults] else: @@ -965,7 +968,7 @@ def set_current_module(self, module): self.exec_mod = clr.GetClrType(type(sys)).GetProperty('Scope', System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(sys, ()) else: self.exec_mod = mod - else: + elif module: _debug_write('Unknown module ' + module) def get_module_names(self): @@ -976,13 +979,16 @@ def get_module_names(self): if sys.platform == 'cli' and type(module) is NamespaceType: self.get_namespaces(name, module, res) else: - filename = getattr(module, '__file__', '') or '' + try: + filename = os.path.abspath(module.__file__) + except Exception: + filename = None res.append((name, filename)) except: pass return res - + def get_namespaces(self, basename, namespace, names): names.append((basename, '')) try: @@ -994,7 +1000,7 @@ def get_namespaces(self, basename, namespace, names): self.get_namespaces(new_name, new_namespace, names) except: pass - + def flush(self): sys.stdout.flush() @@ -1008,10 +1014,10 @@ def execute_attach_process_work_item(): import visualstudio_py_debugger visualstudio_py_debugger.DETACH_CALLBACKS.append(self.do_detach) visualstudio_py_debugger.attach_process(port, debugger_id, debug_options, report=True, block=True) - + self.execute_item = execute_attach_process_work_item self.execute_item_lock.release() - + @staticmethod def get_type_name(val): try: @@ -1042,7 +1048,7 @@ def _get_member_type(inst, name, from_dict, getattr_func = None): class DebugReplBackend(BasicReplBackend): def __init__(self, debugger): - BasicReplBackend.__init__(self, None, None) + BasicReplBackend.__init__(self) self.debugger = debugger self.thread_id = None self.frame_id = None @@ -1092,7 +1098,8 @@ def execute_code_work_item(self): BasicReplBackend.execute_code_work_item(self) else: try: - self.debugger.execute_code_no_report(self.current_code, self.thread_id, self.frame_id, self.frame_kind) + if self.current_code and not self.current_code.isspace(): + self.debugger.execute_code_no_report(self.current_code, self.thread_id, self.frame_id, self.frame_kind) finally: self.current_code = None @@ -1174,6 +1181,8 @@ def check_for_exit_execution_loop(self): class _ReplOutput(object): """file like object which redirects output to the repl window.""" errors = None + closed = False + encoding = 'utf8' def __init__(self, backend, is_stdout, old_out = None): self.name = "" if is_stdout else "" @@ -1185,7 +1194,7 @@ def __init__(self, backend, is_stdout, old_out = None): def flush(self): if self.old_out: self.old_out.flush() - + def fileno(self): if self.pipe is None: self.pipe = os.pipe() @@ -1205,15 +1214,11 @@ def pipe_thread(self): else: self.write(data) - @property - def encoding(self): - return 'utf8' - def writelines(self, lines): for line in lines: self.write(line) self.write('\n') - + def write(self, value): _debug_write('printing ' + repr(value) + '\n') if self.is_stdout: @@ -1222,7 +1227,7 @@ def write(self, value): self.backend.write_stderr(value) if self.old_out: self.old_out.write(value) - + def isatty(self): return True @@ -1234,10 +1239,10 @@ class _ReplInput(object): """file like object which redirects input from the repl window""" def __init__(self, backend): self.backend = backend - + def readline(self): return self.backend.read_line() - + def readlines(self, size = None): res = [] while True: @@ -1246,12 +1251,12 @@ def readlines(self, size = None): res.append(line) else: break - + return res def xreadlines(self): return self - + def write(self, *args): raise IOError("File not open for writing") @@ -1272,7 +1277,7 @@ def next(self): class DotNetOutput(System.IO.TextWriter): def __new__(cls, backend, is_stdout, old_out=None): return System.IO.TextWriter.__new__(cls) - + def __init__(self, backend, is_stdout, old_out=None): self.backend = backend self.is_stdout = is_stdout @@ -1311,7 +1316,7 @@ def WriteLine(self, value, *args): @property def Encoding(self): return System.Text.Encoding.UTF8 - + BACKEND = None @@ -1320,17 +1325,15 @@ def _run_repl(): parser = OptionParser(prog='repl', description='Process REPL options') parser.add_option('--port', dest='port', - help='the port to connect back to') - parser.add_option('--launch_file', dest='launch_file', - help='the script file to run on startup') - parser.add_option('--execution_mode', dest='backend', - help='the backend to use') + help='the port to connect back to') + parser.add_option('--execution-mode', dest='backend', + help='the backend to use') parser.add_option('--enable-attach', dest='enable_attach', - action="store_true", default=False, - help='enable attaching the debugger via $attach') + action="store_true", default=False, + help='enable attaching the debugger via $attach') (options, args) = parser.parse_args() - + # kick off repl # make us available under our "normal" name, not just __main__ which we'll likely replace. sys.modules['visualstudio_py_repl'] = sys.modules['__main__'] @@ -1356,7 +1359,14 @@ def _run_repl(): sys.argv = args or [''] global BACKEND - BACKEND = backend_type(launch_file=options.launch_file) + try: + BACKEND = backend_type() + except UnsupportedReplException: + backend_error = sys.exc_info()[1].reason + BACKEND = BasicReplBackend() + except Exception: + backend_error = traceback.format_exc() + BACKEND = BasicReplBackend() BACKEND.connect(int(options.port)) if options.enable_attach: @@ -1378,4 +1388,4 @@ def _run_repl(): _debug_write(traceback.format_exc()) _debug_write('exiting') input() - raise + raise \ No newline at end of file diff --git a/pythonFiles/PythonTools/visualstudio_py_util.py b/pythonFiles/PythonTools/visualstudio_py_util.py index 49227337b1fb..b3ed951e8718 100644 --- a/pythonFiles/PythonTools/visualstudio_py_util.py +++ b/pythonFiles/PythonTools/visualstudio_py_util.py @@ -87,7 +87,10 @@ def exec_code(code, file, global_variables): except AttributeError: pass - sys.path[0] = os.path.split(file)[0] + if os.path.isdir(sys.path[0]): + sys.path.insert(0, os.path.split(file)[0]) + else: + sys.path[0] = os.path.split(file)[0] code_obj = compile(code, file, 'exec') exec(code_obj, global_variables) @@ -131,7 +134,10 @@ def exec_module(module, global_variables): def read_bytes(conn, count): b = to_bytes('') while len(b) < count: - b += conn.recv(count - len(b)) + received_data = conn.recv(count - len(b)) + if received_data is None: + break + b += received_data return b @@ -622,4 +628,4 @@ def __repr__(self): return 'MyRepr' if __name__ == '__main__': print('Running tests...') - SafeRepr()._selftest() + SafeRepr()._selftest() \ No newline at end of file diff --git a/pythonFiles/completion.py b/pythonFiles/completion.py index 6d9c61bdfbc8..8c7cd09410dc 100644 --- a/pythonFiles/completion.py +++ b/pythonFiles/completion.py @@ -4,9 +4,25 @@ import sys import json import traceback +import platform WORD_RE = re.compile(r'\w') +jediPreview = False +class RedirectStdout(object): + def __init__(self, new_stdout=None): + """If stdout is None, redirect to /dev/null""" + self._new_stdout = new_stdout or open(os.devnull, 'w') + + def __enter__(self): + sys.stdout.flush() + oldstdout_fno = self.oldstdout_fno = os.dup(sys.stdout.fileno()) + os.dup2(self._new_stdout.fileno(), 1) + + def __exit__(self, exc_type, exc_value, traceback): + self._new_stdout.flush() + os.dup2(self.oldstdout_fno, 1) + os.close(self.oldstdout_fno) class JediCompletion(object): basic_types = { @@ -19,18 +35,32 @@ class JediCompletion(object): def __init__(self): self.default_sys_path = sys.path self._input = io.open(sys.stdin.fileno(), encoding='utf-8') + if (os.path.sep == '/') and (platform.uname()[2].find('Microsoft') > -1): + # WSL; does not support UNC paths + self.drive_mount = '/mnt/' + elif sys.platform == 'cygwin': + # cygwin + self.drive_mount = '/cygdrive/' + else: + # Do no normalization, e.g. Windows build of Python. + # Could add additional test: ((os.path.sep == '/') and os.path.isdir('/mnt/c')) + # However, this may have more false positives trying to identify Windows/*nix hybrids + self.drive_mount = '' def _get_definition_type(self, definition): is_built_in = definition.in_builtin_module # if definition.type not in ['import', 'keyword'] and is_built_in(): # return 'builtin' - if definition.type in ['statement'] and definition.name.isupper(): - return 'constant' - return self.basic_types.get(definition.type, definition.type) + try: + if definition.type in ['statement'] and definition.name.isupper(): + return 'constant' + return self.basic_types.get(definition.type, definition.type) + except Exception: + return 'builtin' def _additional_info(self, completion): """Provide additional information about the completion object.""" - if completion._definition is None: + if not hasattr(completion, '_definition') or completion._definition is None: return '' if completion.type == 'statement': nodes_to_display = ['InstanceElement', 'String', 'Node', 'Lambda', @@ -73,6 +103,8 @@ def _get_call_signatures(self, script): call_signatures = script.call_signatures() except KeyError: call_signatures = [] + except : + call_signatures = [] for signature in call_signatures: for pos, param in enumerate(signature.params): if not param.name: @@ -106,8 +138,13 @@ def _get_call_signatures_with_args(self, script): sig = {"name": "", "description": "", "docstring": "", "paramindex": 0, "params": [], "bracketstart": []} sig["description"] = signature.description - sig["docstring"] = signature.docstring() - sig["raw_docstring"] = signature.docstring(raw=True) + try: + sig["docstring"] = signature.docstring() + sig["raw_docstring"] = signature.docstring(raw=True) + except Exception: + sig["docstring"] = '' + sig["raw_docstring"] = '' + sig["name"] = signature.name sig["paramindex"] = signature.index sig["bracketstart"].append(signature.index) @@ -128,8 +165,13 @@ def _get_call_signatures_with_args(self, script): # if name.startswith('*'): # continue #_signatures.append((signature, name, value)) - sig["params"].append({"name": name, "value": value, "docstring": param.docstring( - ), "description": param.description}) + paramDocstring = '' + try: + paramDocstring = param.docstring() + except Exception: + paramDocstring = '' + + sig["params"].append({"name": name, "value": value, "docstring": paramDocstring, "description": param.description}) return _signatures def _serialize_completions(self, script, identifier=None, prefix=''): @@ -155,6 +197,9 @@ def _serialize_completions(self, script, identifier=None, prefix=''): 'raw_type': '', 'rightLabel': self._additional_info(signature) } + _completion['description'] = '' + _completion['raw_docstring'] = '' + # we pass 'text' here only for fuzzy matcher if value: _completion['snippet'] = '%s=${1:%s}$0' % (name, value) @@ -164,8 +209,12 @@ def _serialize_completions(self, script, identifier=None, prefix=''): _completion['text'] = name _completion['displayText'] = name if self.show_doc_strings: - _completion['description'] = signature.docstring() - _completion['raw_docstring'] = signature.docstring(raw=True) + try: + _completion['description'] = signature.docstring() + _completion['raw_docstring'] = signature.docstring(raw=True) + except Exception: + _completion['description'] = '' + _completion['raw_docstring'] = '' else: _completion['description'] = self._generate_signature( signature) @@ -175,19 +224,39 @@ def _serialize_completions(self, script, identifier=None, prefix=''): completions = script.completions() except KeyError: completions = [] + except : + completions = [] for completion in completions: if self.show_doc_strings: - description = completion.docstring() + try: + description = completion.docstring() + except Exception: + description = '' else: description = self._generate_signature(completion) - _completion = { - 'text': completion.name, - 'type': self._get_definition_type(completion), - 'raw_type': completion.type, - 'description': description, - 'raw_docstring': completion.docstring(raw=True), - 'rightLabel': self._additional_info(completion) - } + + try: + rawDocstring = completion.docstring(raw=True) + _completion = { + 'text': completion.name, + 'type': self._get_definition_type(completion), + 'raw_type': completion.type, + 'description': description, + 'raw_docstring': rawDocstring, + 'rightLabel': self._additional_info(completion) + } + except Exception: + continue + + for c in _completions: + if c['text'] == _completion['text']: + c['type'] = _completion['type'] + c['raw_type'] = _completion['raw_type'] + if len(c['description']) == 0 and len(c['raw_docstring']) == 0: + c['description'] = _completion['description'] + c['raw_docstring'] = _completion['description'] + + if any([c['text'].split('=')[0] == _completion['text'] for c in _completions]): # ignore function arguments we already have @@ -249,6 +318,160 @@ def _top_definition(self, definition): return d return definition + def _extract_range_jedi_0_9_0(self, definition): + from jedi import common + from jedi.parser.utils import load_parser + # get the scope range + try: + if definition.type in ['class', 'function'] and hasattr(definition, '_definition'): + scope = definition._definition + start_line = scope.start_pos[0] - 1 + start_column = scope.start_pos[1] + end_line = scope.end_pos[0] - 1 + end_column = scope.end_pos[1] + # get the lines + path = definition._definition.get_parent_until().path + parser = load_parser(path) + lines = common.splitlines(parser.source) + lines[end_line] = lines[end_line][:end_column] + # trim the lines + lines = lines[start_line:end_line + 1] + lines = '\n'.join(lines).rstrip().split('\n') + end_line = start_line + len(lines) - 1 + end_column = len(lines[-1]) - 1 + else: + symbol = definition._name + start_line = symbol.start_pos[0] - 1 + start_column = symbol.start_pos[1] + end_line = symbol.end_pos[0] - 1 + end_column = symbol.end_pos[1] + return { + 'start_line': start_line, + 'start_column': start_column, + 'end_line': end_line, + 'end_column': end_column + } + except Exception as e: + return { + 'start_line': definition.line - 1, + 'start_column': definition.column, + 'end_line': definition.line - 1, + 'end_column': definition.column + } + + def _extract_range_jedi_0_10_1(self, definition): + from jedi import common + from jedi.parser.python import parse + # get the scope range + try: + if definition.type in ['class', 'function']: + tree_name = definition._name.tree_name + scope = tree_name.get_definition() + start_line = scope.start_pos[0] - 1 + start_column = scope.start_pos[1] + # get the lines + code = scope.get_code(include_prefix=False) + lines = common.splitlines(code) + # trim the lines + lines = '\n'.join(lines).rstrip().split('\n') + end_line = start_line + len(lines) - 1 + end_column = len(lines[-1]) - 1 + else: + symbol = definition._name.tree_name + start_line = symbol.start_pos[0] - 1 + start_column = symbol.start_pos[1] + end_line = symbol.end_pos[0] - 1 + end_column = symbol.end_pos[1] + return { + 'start_line': start_line, + 'start_column': start_column, + 'end_line': end_line, + 'end_column': end_column + } + except Exception as e: + return { + 'start_line': definition.line - 1, + 'start_column': definition.column, + 'end_line': definition.line - 1, + 'end_column': definition.column + } + + def _extract_range(self, definition): + """Provides the definition range of a given definition + + For regular symbols it returns the start and end location of the + characters making up the symbol. + + For scoped containers it will return the entire definition of the + scope. + + The scope that jedi provides ends with the first character of the next + scope so it's not ideal. For vscode we need the scope to end with the + last character of actual code. That's why we extract the lines that + make up our scope and trim the trailing whitespace. + """ + if jedi.__version__ in ('0.9.0', '0.10.0'): + return self._extract_range_jedi_0_9_0(definition) + else: + return self._extract_range_jedi_0_10_1(definition) + + def _get_definitionsx(self, definitions, identifier=None, ignoreNoModulePath=False): + """Serialize response to be read from VSCode. + + Args: + definitions: List of jedi.api.classes.Definition objects. + identifier: Unique completion identifier to pass back to VSCode. + + Returns: + Serialized string to send to VSCode. + """ + _definitions = [] + for definition in definitions: + try: + if definition.type == 'import': + definition = self._top_definition(definition) + definitionRange = { + 'start_line': 0, + 'start_column': 0, + 'end_line': 0, + 'end_column': 0 + } + module_path = '' + if hasattr(definition, 'module_path') and definition.module_path: + module_path = definition.module_path + definitionRange = self._extract_range(definition) + else: + if not ignoreNoModulePath: + continue + try: + parent = definition.parent() + container = parent.name if parent.type != 'module' else '' + except Exception: + container = '' + + try: + docstring = definition.docstring() + rawdocstring = definition.docstring(raw=True) + except Exception: + docstring = '' + rawdocstring = '' + _definition = { + 'text': definition.name, + 'type': self._get_definition_type(definition), + 'raw_type': definition.type, + 'fileName': module_path, + 'container': container, + 'range': definitionRange, + 'description': definition.description, + 'docstring': docstring, + 'raw_docstring': rawdocstring, + 'signature': self._generate_signature(definition) + } + _definitions.append(_definition) + except Exception as e: + pass + return _definitions + def _serialize_definitions(self, definitions, identifier=None): """Serialize response to be read from VSCode. @@ -267,13 +490,28 @@ def _serialize_definitions(self, definitions, identifier=None): definition = self._top_definition(definition) if not definition.module_path: continue + try: + parent = definition.parent() + container = parent.name if parent.type != 'module' else '' + except Exception: + container = '' + + try: + docstring = definition.docstring() + rawdocstring = definition.docstring(raw=True) + except Exception: + docstring = '' + rawdocstring = '' _definition = { 'text': definition.name, 'type': self._get_definition_type(definition), 'raw_type': definition.type, 'fileName': definition.module_path, - 'line': definition.line - 1, - 'column': definition.column + 'container': container, + 'range': self._extract_range(definition), + 'description': definition.description, + 'docstring': docstring, + 'raw_docstring': rawdocstring } _definitions.append(_definition) except Exception as e: @@ -283,27 +521,34 @@ def _serialize_definitions(self, definitions, identifier=None): def _serialize_tooltip(self, definitions, identifier=None): _definitions = [] for definition in definitions: - if definition.module_path: - if definition.type == 'import': - definition = self._top_definition(definition) - if not definition.module_path: - continue - - description = definition.docstring() - if description is not None: - description = description.strip() - if not description: - description = self._additional_info(definition) - _definition = { - 'text': definition.name, - 'type': self._get_definition_type(definition), - 'fileName': definition.module_path, - 'description': description, - 'line': definition.line - 1, - 'column': definition.column - } - _definitions.append(_definition) - break + signature = definition.name + description = None + if definition.type in ['class', 'function']: + signature = self._generate_signature(definition) + try: + description = definition.docstring(raw=True).strip() + except Exception: + description = '' + if not description and not hasattr(definition, 'get_line_code'): + # jedi returns an empty string for compiled objects + description = definition.docstring().strip() + if definition.type == 'module': + signature = definition.full_name + try: + description = definition.docstring(raw=True).strip() + except Exception: + description = '' + if not description and hasattr(definition, 'get_line_code'): + # jedi returns an empty string for compiled objects + description = definition.docstring().strip() + _definition = { + 'type': self._get_definition_type(definition), + 'text': definition.name, + 'description': description, + 'docstring': description, + 'signature': signature + } + _definitions.append(_definition) return json.dumps({'id': identifier, 'results': _definitions}) def _serialize_usages(self, usages, identifier=None): @@ -349,6 +594,25 @@ def _set_request_config(self, config): if path and path not in sys.path: sys.path.insert(0, path) + def _normalize_request_path(self, request): + """Normalize any Windows paths received by a *nix build of + Python. Does not alter the reverse os.path.sep=='\\', + i.e. *nix paths received by a Windows build of Python. + """ + if 'path' in request: + if not self.drive_mount: + return + newPath = request['path'].replace('\\', '/') + if newPath[0:1] == '/': + # is absolute path with no drive letter + request['path'] = newPath + elif newPath[1:2] == ':': + # is path with drive letter, only absolute can be mapped + request['path'] = self.drive_mount + newPath[0:1].lower() + newPath[2:] + else: + # is relative path + request['path'] = newPath + def _process_request(self, request): """Accept serialized request from VSCode and write response. """ @@ -356,43 +620,71 @@ def _process_request(self, request): self._set_request_config(request.get('config', {})) + self._normalize_request_path(request) path = self._get_top_level_module(request.get('path', '')) if path not in sys.path: sys.path.insert(0, path) lookup = request.get('lookup', 'completions') if lookup == 'names': - return self._write_response(self._serialize_definitions( + return self._serialize_definitions( jedi.api.names( source=request.get('source', None), path=request.get('path', ''), all_scopes=True), - request['id'])) + request['id']) script = jedi.api.Script( source=request.get('source', None), line=request['line'] + 1, column=request['column'], path=request.get('path', '')) - + if lookup == 'definitions': - return self._write_response(self._serialize_definitions( - script.goto_assignments(), request['id'])) + defs = [] + try: + defs = self._get_definitionsx(script.goto_assignments(follow_imports=False), request['id']) + except: + pass + try: + if len(defs) == 0: + defs = self._get_definitionsx(script.goto_definitions(), request['id']) + except: + pass + try: + if len(defs) == 0: + defs = self._get_definitionsx(script.goto_assignments(), request['id']) + except: + pass + return json.dumps({'id': request['id'], 'results': defs}) if lookup == 'tooltip': - return self._write_response(self._serialize_tooltip( - script.goto_assignments(), request['id'])) + if jediPreview: + defs = [] + try: + defs = self._get_definitionsx(script.goto_definitions(), request['id'], True) + except: + pass + try: + if len(defs) == 0: + defs = self._get_definitionsx(script.goto_assignments(), request['id'], True) + except: + pass + return json.dumps({'id': request['id'], 'results': defs}) + else: + try: + return self._serialize_tooltip(script.goto_definitions(), request['id']) + except: + return json.dumps({'id': request['id'], 'results': []}) elif lookup == 'arguments': - return self._write_response(self._serialize_arguments( - script, request['id'])) + return self._serialize_arguments( + script, request['id']) elif lookup == 'usages': - return self._write_response(self._serialize_usages( - script.usages(), request['id'])) + return self._serialize_usages( + script.usages(), request['id']) elif lookup == 'methods': - return self._write_response( - self._serialize_methods(script, request['id'], - request.get('prefix', ''))) + return self._serialize_methods(script, request['id'], + request.get('prefix', '')) else: - return self._write_response( - self._serialize_completions(script, request['id'], - request.get('prefix', ''))) + return self._serialize_completions(script, request['id'], + request.get('prefix', '')) def _write_response(self, response): sys.stdout.write(response + '\n') @@ -401,28 +693,47 @@ def _write_response(self, response): def watch(self): while True: try: - self._process_request(self._input.readline()) + rq = self._input.readline() + if len(rq) == 0: + # Reached EOF - indication our parent process is gone. + sys.stderr.write('Received EOF from the standard input,exiting' + '\n') + sys.stderr.flush() + return + with RedirectStdout(): + response = self._process_request(rq) + self._write_response(response) + except Exception: sys.stderr.write(traceback.format_exc() + '\n') sys.stderr.flush() if __name__ == '__main__': - jediPreview = False cachePrefix = 'v' + modulesToLoad = '' if len(sys.argv) > 0 and sys.argv[1] == 'preview': jediPath = os.path.join(os.path.dirname(__file__), 'preview') jediPreview = True + if len(sys.argv) > 2: + modulesToLoad = sys.argv[2] elif len(sys.argv) > 0 and sys.argv[1] == 'custom': jediPath = sys.argv[2] jediPreview = True cachePrefix = 'custom_v' + if len(sys.argv) > 3: + modulesToLoad = sys.argv[3] else: + #std jediPath = os.path.join(os.path.dirname(__file__), 'release') + if len(sys.argv) > 2: + modulesToLoad = sys.argv[2] + sys.path.insert(0, jediPath) - import jedi + import jedi if jediPreview: jedi.settings.cache_directory = os.path.join( jedi.settings.cache_directory, cachePrefix + jedi.__version__.replace('.', '')) # remove jedi from path after we import it so it will not be completed sys.path.pop(0) + if len(modulesToLoad) > 0: + jedi.preload_module(*modulesToLoad.split(',')) JediCompletion().watch() diff --git a/pythonFiles/completionServer.py b/pythonFiles/completionServer.py new file mode 100644 index 000000000000..9c380e493f73 --- /dev/null +++ b/pythonFiles/completionServer.py @@ -0,0 +1,587 @@ + +import sys +import socket +import select +import time +import re +import json +import struct +import imp +import traceback +import random +import os +import io +import inspect +import types +from collections import deque +import os +import warnings +from encodings import utf_8, ascii +try: + import thread +except ImportError: + # Renamed in Python3k + import _thread as thread + +# Reference material +# http://jupyter-client.readthedocs.io/en/latest/messaging.html +# http://pydoc.net/Python/magni/1.4.0/magni.tests.ipynb_examples/ +# http://www.xavierdupre.fr/app/pyquickhelper/helpsphinx/_modules/pyquickhelper/ipythonhelper/notebook_runner.html + +import visualstudio_py_util as _vspu + +to_bytes = _vspu.to_bytes +read_bytes = _vspu.read_bytes +read_int = _vspu.read_int +read_string = _vspu.read_string +write_bytes = _vspu.write_bytes +write_int = _vspu.write_int +write_string = _vspu.write_string + +try: + unicode +except NameError: + unicode = str + +try: + BaseException +except NameError: + # BaseException not defined until Python 2.5 + BaseException = Exception + +try: + from Queue import Empty, Queue # Python 2 +except ImportError: + from queue import Empty, Queue # Python 3 + +DEBUG = os.environ.get('DEBUG_EXTENSION_IPYTHON', '0') == '1' +TEST = os.environ.get('VSC_PYTHON_CI_TEST', '0') == '1' + +def _debug_write(out): + if DEBUG: + sys.__stdout__.write(out) + sys.__stdout__.write("\n") + sys.__stdout__.flush() + +class SafeSendLock(object): + """a lock which ensures we're released if we take a KeyboardInterrupt exception acquiring it""" + + def __init__(self): + self.lock = thread.allocate_lock() + + def __enter__(self): + self.acquire() + + def __exit__(self, exc_type, exc_value, tb): + self.release() + + def acquire(self): + try: + self.lock.acquire() + except KeyboardInterrupt: + try: + self.lock.release() + except: + pass + raise + + def release(self): + self.lock.release() + + +class jediSocketServer(object): + """back end for executing code in kernel. Handles all of the communication with the remote process.""" + + """Messages sent back as responses""" + _PONG = to_bytes('PONG') + _EXIT = to_bytes('EXIT') + _LSKS = to_bytes('LSKS') + _EROR = to_bytes('EROR') + _TEST = to_bytes('TEST') + _STRK = to_bytes('STRK') + _STPK = to_bytes('STPK') + _RSTK = to_bytes('RSTK') + _ITPK = to_bytes('ITPK') + _RUN = to_bytes('RUN ') + _SHEL = to_bytes('SHEL') + _IOPB = to_bytes('IOPB') + + def __init__(self): + import threading + self.conn = None + self.send_lock = SafeSendLock() + self.input_event = threading.Lock() + # lock starts acquired (we use it like a manual reset event) + self.input_event.acquire() + self.input_string = None + self.exit_requested = False + self.kernelMonitor = None + self.shell_channel = None + + def connect(self, port): + # start a new thread for communicating w/ the remote process + _debug_write('Connecting to socket port: ' + str(port)) + self.conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.conn.connect(('127.0.0.1', port)) + _debug_write('Connected to socket') + + # perform the handshake + with self.send_lock: + write_string(self.conn, "Some Guid") + write_int(self.conn, os.getpid()) + + _debug_write('Handshake information sent') + + thread.start_new_thread(self.start_processing, ()) + + def start_processing(self): + """loop on created thread which processes communicates with the REPL window""" + + _debug_write('Started processing thread') + try: + while True: + if self.check_for_exit_socket_loop(): + break + + # we receive a series of 4 byte commands. Each command then + # has it's own format which we must parse before continuing to + # the next command. + self.flush() + self.conn.settimeout(10) + try: + inp = read_bytes(self.conn, 4) + self.conn.settimeout(None) + cmd = jediSocketServer._COMMANDS.get(inp) + if inp and cmd is not None: + id = "" + try: + if jediSocketServer._COMMANDS_WITH_IDS.get(inp) == True: + while True: + try: + id = read_string(self.conn) + break + except socket.timeout: + pass + cmd(self, id) + else: + cmd(self) + except: + commandName = utf_8.decode(inp)[0] + try: + commandName = ascii.Codec.encode(commandName)[ + 0] + except UnicodeEncodeError: + pass + self.replyWithError(commandName, id) + else: + if inp: + print ('unknown command', inp) + break + except socket.timeout: + pass + + except IPythonExitException: + _debug_write('IPythonExitException') + _debug_write(traceback.format_exc()) + pass + except socket.error: + _debug_write('socket error') + _debug_write(traceback.format_exc()) + pass + except: + _debug_write('error in repl loop') + _debug_write(traceback.format_exc()) + + # try and exit gracefully, then interrupt main if necessary + time.sleep(2) + traceback.print_exc() + self.exit_process() + + def check_for_exit_socket_loop(self): + return self.exit_requested + + def replyWithError(self, commandName, id): + with self.send_lock: + traceMessage = traceback.format_exc() + _debug_write('Replying with error:' + traceMessage) + + write_bytes(self.conn, jediSocketServer._EROR) + write_string(self.conn, commandName) + write_string(self.conn, "" if id is None else id) + write_string(self.conn, traceMessage) + + def _cmd_exit(self): + """exits the interactive process""" + self.exit_requested = True + self.exit_process() + + def _cmd_ping(self, id): + """ping""" + _debug_write('Ping received') + while True: + try: + message = read_string(self.conn) + break + except socket.timeout: + pass + with self.send_lock: + _debug_write('Pong response being sent out') + write_bytes(self.conn, jediSocketServer._PONG) + write_string(self.conn, id) + write_string(self.conn, message) + + def _cmd_lstk(self, id): + """List kernel specs""" + _debug_write('Listing kernel specs') + kernelspecs = json.dumps(listKernelSpecs()) + with self.send_lock: + _debug_write('Replying with kernel Specs') + write_bytes(self.conn, jediSocketServer._LSKS) + write_string(self.conn, id) + write_string(self.conn, kernelspecs) + + def _cmd_strk(self, id): + """Start a kernel by name""" + _debug_write('Listing kernel specs') + while True: + try: + kernelName = read_string(self.conn) + break + except socket.timeout: + pass + kernelUUID = multiKernelManager.start_kernel(kernel_name=kernelName) + self._postStartKernel(kernelUUID) + + # get the config and the connection FileExistsError + try: + config = kernel_manager.config + except: + config = {} + try: + connection_file = kernel_manager.connection_file + except: + connection_file = "" + + with self.send_lock: + _debug_write('Replying with kernel Specs= ' + str(kernelUUID)) + write_bytes(self.conn, jediSocketServer._STRK) + write_string(self.conn, id) + write_string(self.conn, str(kernelUUID)) + write_string(self.conn, json.dumps(config)) + write_string(self.conn, connection_file) + + def _postStartKernel(self, kernelUUID): + kernel_manager = multiKernelManager.get_kernel(kernelUUID) + kernel_client = kernel_manager.client() + kernel_client.start_channels() + + try: + # IPython 3.x + kernel_client.wait_for_ready() + iopub = kernel_client + shell = kernel_client + # todo: get_stdin_msg + except AttributeError: + # Ipython 2.x + # Based on https://github.com/paulgb/runipy/pull/49/files + iopub = kernel_client.iopub_channel + shell = kernel_client.shell_channel + shell.get_shell_msg = shell.get_msg + iopub.get_iopub_msg = iopub.get_msg + # todo: get_stdin_msg + + self.shell_channel = shell + self.kernelMonitor = iPythonKernelResponseMonitor( + kernelUUID, self.conn, self.send_lock, shell, iopub) + + def stopKernel(self, kernelUUID): + """Shutdown a kernel by UUID""" + try: + if self.kernelMonitor is not None: + self.kernelMonitor.stop() + finally: + pass + + try: + kernel_manager = multiKernelManager.get_kernel(kernelUUID) + kernel_client = kernel_manager.client() + kernel_client.stop_channels() + finally: + pass + + try: + kernel_manager = multiKernelManager.get_kernel(kernelUUID) + kernel_manager.shutdown_kernel() + except: + pass + finally: + self.shell_channel = None + self.kernelMonitor = None + + def _cmd_stpk(self, id): + """Shutdown a kernel by UUID""" + while True: + try: + kernelUUID = read_string(self.conn) + break + except socket.timeout: + pass + + self.stopKernel(kernelUUID) + + with self.send_lock: + write_bytes(self.conn, jediSocketServer._STPK) + write_string(self.conn, id) + + def _cmd_kill(self, id): + """Shutdown a kernel by UUID""" + while True: + try: + kernelUUID = read_string(self.conn) + break + except socket.timeout: + pass + + try: + if self.kernelMonitor is not None: + self.kernelMonitor.stop() + finally: + pass + + try: + kernel_manager = multiKernelManager.get_kernel(kernelUUID) + kernel_client = kernel_manager.client() + kernel_client.stop_channels() + finally: + pass + + try: + kernel_manager = multiKernelManager.get_kernel(kernelUUID) + kernel_manager.shutdown_kernel() + except: + pass + + def _cmd_rstk(self, id): + """Restart a kernel by UUID""" + while True: + try: + kernelUUID = read_string(self.conn) + break + except socket.timeout: + pass + + kernel_manager = multiKernelManager.get_kernel(kernelUUID) + kernel_manager.restart_kernel(now=True) + + with self.send_lock: + write_bytes(self.conn, jediSocketServer._RSTK) + write_string(self.conn, id) + + def _cmd_itpk(self, id): + """Interrupt a kernel by UUID""" + while True: + try: + kernelUUID = read_string(self.conn) + break + except socket.timeout: + pass + kernel_manager = multiKernelManager.get_kernel(kernelUUID) + kernel_manager.interrupt_kernel() + with self.send_lock: + write_bytes(self.conn, jediSocketServer._ITPK) + write_string(self.conn, id) + + def _cmd_run(self, id): + """runs the received snippet of code (kernel is expected to have been started)""" + while True: + try: + code = read_string(self.conn) + break + except socket.timeout: + pass + msg_id = self.shell_channel.execute(code) + _debug_write('msg_id = ' + msg_id) + with self.send_lock: + write_bytes(self.conn, jediSocketServer._RUN) + write_string(self.conn, id) + write_string(self.conn, msg_id) + + def _cmd_abrt(self): + """aborts the current running command""" + # abort command, interrupts execution of the main thread. + pass + + def _cmd_inpl(self): + """handles the input command which returns a string of input""" + self.input_string = read_string(self.conn) + self.input_event.release() + + def send_prompt(self, ps1, ps2, update_all=True): + """sends the current prompt to the interactive window""" + # with self.send_lock: + # write_bytes(self.conn, jediSocketServer._PRPC) + # write_string(self.conn, ps1) + # write_string(self.conn, ps2) + # write_int(self.conn, update_all) + pass + + def send_error(self): + """reports that an error occured to the interactive window""" + with self.send_lock: + write_bytes(self.conn, jediSocketServer._ERRE) + + def send_exit(self): + """reports the that the REPL process has exited to the interactive window""" + with self.send_lock: + write_bytes(self.conn, jediSocketServer._EXIT) + + def send_command_executed(self): + with self.send_lock: + write_bytes(self.conn, jediSocketServer._DONE) + + def read_line(self): + """reads a line of input from standard input""" + with self.send_lock: + write_bytes(self.conn, jediSocketServer._RDLN) + self.input_event.acquire() + return self.input_string + + def write_stdout(self, value): + """writes a string to standard output in the remote console""" + with self.send_lock: + write_bytes(self.conn, jediSocketServer._STDO) + write_string(self.conn, value) + + def write_stderr(self, value): + """writes a string to standard input in the remote console""" + with self.send_lock: + write_bytes(self.conn, jediSocketServer._STDE) + write_string(self.conn, value) + + ################################################################ + # Implementation of execution, etc... + + def execution_loop(self): + """loop on the main thread which is responsible for executing code""" + while True: + exit = self.run_one_command(cur_modules, cur_ps1, cur_ps2) + if exit: + return + + def run_command(self, command): + """runs the specified command which is a string containing code""" + pass + + def interrupt_main(self): + """aborts the current running command""" + pass + + def exit_process(self): + """exits the REPL process""" + sys.exit(0) + + def flush(self): + """flushes the stdout/stderr buffers""" + pass + + _COMMANDS = { + to_bytes('run '): _cmd_run, + to_bytes('abrt'): _cmd_abrt, + to_bytes('exit'): _cmd_exit, + to_bytes('ping'): _cmd_ping, + to_bytes('inpl'): _cmd_inpl, + to_bytes('lsks'): _cmd_lstk, + to_bytes('strk'): _cmd_strk, + to_bytes('stpk'): _cmd_stpk, + to_bytes('rstk'): _cmd_rstk, + to_bytes('itpk'): _cmd_itpk, + to_bytes('kill'): _cmd_kill, + } + + _COMMANDS_WITH_IDS = { + to_bytes('lsks'): True, + to_bytes('ping'): True, + to_bytes('strk'): True, + to_bytes('stpk'): True, + to_bytes('rstk'): True, + to_bytes('itpk'): True, + to_bytes('run '): True, + } + + + + +def exit_work_item(): + sys.exit(0) + + +class jediReadLine(object): + + def __init__(self): + self._input = io.open(sys.stdin.fileno(), encoding='utf-8') + + def _deserialize(self, request): + """Deserialize request from VSCode. + + Args: + request: String with raw request from VSCode. + + Returns: + Python dictionary with request data. + """ + return json.loads(request) + + def _set_request_config(self, config): + self.use_snippets = config.get('useSnippets') + self.show_doc_strings = config.get('showDescriptions', True) + self.fuzzy_matcher = config.get('fuzzyMatcher', False) + + def _process_request(self, request): + """Accept serialized request from VSCode and write response. + """ + request = self._deserialize(request) + + self._set_request_config(request.get('config', {})) + + lookup = request.get('lookup', 'completions') + + if lookup == 'definitions': + return self._write_response('defs') + elif lookup == 'arguments': + return self._write_response('arguments') + elif lookup == 'usages': + return self._write_response('usages') + else: + return self._write_response('Dont Know') + + def _write_response(self, response): + sys.stdout.write(response + '\n') + sys.stdout.flush() + + def watch(self): + port = int(sys.argv[1]) + _debug_write('Socket port received: ' + str(port)) + server = jediSocketServer() + server.connect(port) + sys.__stdout__.write('Started') + sys.__stdout__.write("\n") + sys.__stdout__.flush() + while True: + try: + kernelUUID = self._input.readline() + sys.__stdout__.write('about to die\n') + sys.__stdout__.flush() + if (len(kernelUUID) > 0): + try: + server.stopKernel(kernelUUID) + except: + pass + server.exit_requested = True + sys.__stdout__.write('adios\n') + sys.__stdout__.flush() + except Exception: + sys.stderr.write(traceback.format_exc() + '\n') + sys.stderr.flush() + +if __name__ == '__main__': + jediReadLine().watch() diff --git a/pythonFiles/isort/__init__.py b/pythonFiles/isort/__init__.py index 082128cdedc1..3063d1ed92d8 100644 --- a/pythonFiles/isort/__init__.py +++ b/pythonFiles/isort/__init__.py @@ -25,4 +25,4 @@ from . import settings from .isort import SortImports -__version__ = "4.2.5" +__version__ = "4.2.15" diff --git a/pythonFiles/isort/__main__ b/pythonFiles/isort/__main__.py similarity index 100% rename from pythonFiles/isort/__main__ rename to pythonFiles/isort/__main__.py diff --git a/pythonFiles/isort/isort.py b/pythonFiles/isort/isort.py index 01d0f9cc7ed9..cecd5af991a1 100644 --- a/pythonFiles/isort/isort.py +++ b/pythonFiles/isort/isort.py @@ -37,12 +37,10 @@ from difflib import unified_diff from fnmatch import fnmatch from glob import glob -from sys import path as PYTHONPATH -from sys import stdout from . import settings from .natural import nsorted -from .pie_slice import * +from .pie_slice import OrderedDict, OrderedSet, input, itemsview KNOWN_SECTION_MAPPING = { 'STDLIB': 'STANDARD_LIBRARY', @@ -57,7 +55,7 @@ class SortImports(object): skipped = False def __init__(self, file_path=None, file_contents=None, write_to_stdout=False, check=False, - show_diff=False, settings_path=None, ask_to_apply=False, **setting_overrides): + show_diff=False, settings_path=None, ask_to_apply=False, **setting_overrides): if not settings_path and file_path: settings_path = os.path.dirname(os.path.abspath(file_path)) settings_path = settings_path or os.getcwd() @@ -74,7 +72,7 @@ def __init__(self, file_path=None, file_contents=None, write_to_stdout=False, ch else: self.config[key] = value - if self.config.get('force_alphabetical_sort', False): + if self.config['force_alphabetical_sort']: self.config.update({'force_alphabetical_sort_within_sections': True, 'no_sections': True, 'lines_between_types': 1, @@ -91,8 +89,8 @@ def __init__(self, file_path=None, file_contents=None, write_to_stdout=False, ch self.place_imports = {} self.import_placements = {} - self.remove_imports = [self._format_simplified(removal) for removal in self.config.get('remove_imports', [])] - self.add_imports = [self._format_natural(addition) for addition in self.config.get('add_imports', [])] + self.remove_imports = [self._format_simplified(removal) for removal in self.config['remove_imports']] + self.add_imports = [self._format_natural(addition) for addition in self.config['add_imports']] self._section_comments = ["# " + value for key, value in itemsview(self.config) if key.startswith('import_heading') and value] @@ -118,20 +116,29 @@ def __init__(self, file_path=None, file_contents=None, write_to_stdout=False, ch self.in_lines = file_contents.split("\n") self.original_length = len(self.in_lines) - if (self.original_length > 1 or self.in_lines[:1] not in ([], [""])) or self.config.get('force_adds', False): + if (self.original_length > 1 or self.in_lines[:1] not in ([], [""])) or self.config['force_adds']: for add_import in self.add_imports: self.in_lines.append(add_import) self.number_of_lines = len(self.in_lines) self.out_lines = [] self.comments = {'from': {}, 'straight': {}, 'nested': {}, 'above': {'straight': {}, 'from': {}}} - self.imports = {} + self.imports = OrderedDict() self.as_map = {} - section_names = self.config.get('sections') + section_names = self.config['sections'] self.sections = namedtuple('Sections', section_names)(*[name for name in section_names]) for section in itertools.chain(self.sections, self.config['forced_separate']): - self.imports[section] = {'straight': set(), 'from': {}} + self.imports[section] = {'straight': OrderedSet(), 'from': OrderedDict()} + + self.known_patterns = [] + for placement in reversed(self.sections): + known_placement = KNOWN_SECTION_MAPPING.get(placement, placement) + config_key = 'known_{0}'.format(known_placement.lower()) + known_patterns = self.config.get(config_key, []) + for known_pattern in known_patterns: + self.known_patterns.append((re.compile('^' + known_pattern.replace('*', '.*').replace('?', '.?') + '$'), + placement)) self.index = 0 self.import_index = -1 @@ -147,7 +154,7 @@ def __init__(self, file_path=None, file_contents=None, write_to_stdout=False, ch self.out_lines.append("") self.output = "\n".join(self.out_lines) - if self.config.get('atomic', False): + if self.config['atomic']: try: compile(self._strip_top_comments(self.out_lines), self.file_path, 'exec', 0, 1) except SyntaxError: @@ -164,7 +171,7 @@ def __init__(self, file_path=None, file_contents=None, write_to_stdout=False, ch if check: check_output = self.output check_against = file_contents - if not self.config.get('enforce_white_space', False): + if self.config['ignore_whitespace']: check_output = check_output.replace("\n", "").replace(" ", "") check_against = check_against.replace("\n", "").replace(" ", "") @@ -175,14 +182,11 @@ def __init__(self, file_path=None, file_contents=None, write_to_stdout=False, ch print("ERROR: {0} Imports are incorrectly sorted.".format(self.file_path)) self.incorrectly_sorted = True - if show_diff or self.config.get('show_diff', False) is True: - self._show_diff(file_contents) - - if show_diff or self.config.get('show_diff', False) is True: + if show_diff or self.config['show_diff']: self._show_diff(file_contents) elif write_to_stdout: - stdout.write(self.output) - elif file_name: + sys.stdout.write(self.output) + elif file_name and not check: if ask_to_apply: if self.output == file_contents: return @@ -207,7 +211,7 @@ def _show_diff(self, file_contents): if self.file_path else datetime.now()), tofiledate=str(datetime.now()) ): - stdout.write(line) + sys.stdout.write(line) @staticmethod def _strip_top_comments(lines): @@ -239,13 +243,15 @@ def place_module(self, module_name): parts = module_name.split('.') module_names_to_check = ['.'.join(parts[:first_k]) for first_k in range(len(parts), 0, -1)] for module_name_to_check in module_names_to_check: - for placement in reversed(self.sections): - known_placement = KNOWN_SECTION_MAPPING.get(placement, placement) - config_key = 'known_{0}'.format(known_placement.lower()) - if module_name_to_check in self.config.get(config_key, []): + for pattern, placement in self.known_patterns: + if pattern.match(module_name_to_check): return placement - paths = PYTHONPATH + # Use a copy of sys.path to avoid any unintended modifications + # to it - e.g. `+=` used below will change paths in place and + # if not copied, consequently sys.path, which will grow unbounded + # with duplicates on every call to this method. + paths = list(sys.path) virtual_env = self.config.get('virtual_env') or os.environ.get('VIRTUAL_ENV') virtual_env_src = False if virtual_env: @@ -254,15 +260,20 @@ def place_module(self, module_name): paths += [path for path in glob('{0}/src/*'.format(virtual_env)) if os.path.isdir(path)] virtual_env_src = '{0}/src/'.format(virtual_env) + # handle case-insensitive paths on windows + stdlib_lib_prefix = os.path.normcase(get_stdlib_path()) + for prefix in paths: module_path = "/".join((prefix, module_name.replace(".", "/"))) package_path = "/".join((prefix, module_name.split(".")[0])) - if (os.path.exists(module_path + ".py") or os.path.exists(module_path + ".so") or - (os.path.exists(package_path) and os.path.isdir(package_path))): + is_module = (exists_case_sensitive(module_path + ".py") or + exists_case_sensitive(module_path + ".so")) + is_package = exists_case_sensitive(package_path) and os.path.isdir(package_path) + if is_module or is_package: if ('site-packages' in prefix or 'dist-packages' in prefix or - (virtual_env and virtual_env_src in prefix)): + (virtual_env and virtual_env_src in prefix)): return self.sections.THIRDPARTY - elif 'python2' in prefix.lower() or 'python3' in prefix.lower(): + elif os.path.normcase(prefix).startswith(stdlib_lib_prefix): return self.sections.STDLIB else: return self.config['default_section'] @@ -319,9 +330,9 @@ def _wrap(self, line): """ Returns an import wrapped to the specified line-length, if possible. """ - wrap_mode = self.config.get('multi_line_output', 0) + wrap_mode = self.config['multi_line_output'] if len(line) > self.config['line_length'] and wrap_mode != settings.WrapModes.NOQA: - for splitter in ("import", "."): + for splitter in ("import", ".", "as"): exp = r"\b" + re.escape(splitter) + r"\b" if re.search(exp, line) and not line.strip().startswith(splitter): line_parts = re.split(exp, line) @@ -334,7 +345,18 @@ def _wrap(self, line): cont_line = self._wrap(self.config['indent'] + splitter.join(next_line).lstrip()) if self.config['use_parentheses']: - return "{0}{1} (\n{2})".format(line, splitter, cont_line) + output = "{0}{1} (\n{2}{3}{4})".format( + line, splitter, cont_line, + "," if self.config['include_trailing_comma'] else "", + "\n" if wrap_mode in ( + settings.WrapModes.VERTICAL_HANGING_INDENT, + settings.WrapModes.VERTICAL_GRID_GROUPED, + ) else "") + lines = output.split('\n') + if ' #' in lines[-1] and lines[-1].endswith(')'): + line, comment = lines[-1].split(' #', 1) + lines[-1] = line + ') #' + comment[:-1] + return '\n'.join(lines) return "{0}{1} \\\n{2}".format(line, splitter, cont_line) elif len(line) > self.config['line_length'] and wrap_mode == settings.WrapModes.NOQA: if "# NOQA" not in line: @@ -363,7 +385,7 @@ def _add_from_imports(self, from_modules, section, section_output, ignore_case): continue import_start = "from {0} import ".format(module) - from_imports = list(self.imports[section]['from'][module]) + from_imports = self.imports[section]['from'][module] from_imports = nsorted(from_imports, key=lambda key: self._module_key(key, self.config, True, ignore_case)) if self.remove_imports: from_imports = [line for line in from_imports if not "{0}.{1}".format(module, line) in @@ -378,11 +400,13 @@ def _add_from_imports(self, from_modules, section, section_output, ignore_case): self.config['combine_star']): from_imports[from_imports.index(from_import)] = import_definition else: - import_statement = self._wrap(import_start + import_definition) + import_statement = import_start + import_definition + force_grid_wrap = self.config['force_grid_wrap'] comments = self.comments['straight'].get(submodule) - import_statement = self._add_comments(comments, import_statement) - section_output.append(import_statement) + import_statement = self._add_comments(comments, self._wrap(import_statement)) from_imports.remove(from_import) + section_output.append(import_statement) + if from_imports: comments = self.comments['from'].pop(module, ()) @@ -427,42 +451,20 @@ def _add_from_imports(self, from_modules, section, section_output, ignore_case): do_multiline_reformat = False - if self.config.get('force_grid_wrap') and len(from_imports) > 1: + force_grid_wrap = self.config['force_grid_wrap'] + if force_grid_wrap and len(from_imports) >= force_grid_wrap: do_multiline_reformat = True if len(import_statement) > self.config['line_length'] and len(from_imports) > 1: do_multiline_reformat = True # If line too long AND have imports AND we are NOT using GRID or VERTICAL wrap modes - if (len(import_statement) > self.config['line_length'] and len(from_imports) > 0 - and self.config.get('multi_line_output', 0) not in (1, 0)): + if (len(import_statement) > self.config['line_length'] and len(from_imports) > 0 and + self.config['multi_line_output'] not in (1, 0)): do_multiline_reformat = True if do_multiline_reformat: - output_mode = settings.WrapModes._fields[self.config.get('multi_line_output', - 0)].lower() - formatter = getattr(self, "_output_" + output_mode, self._output_grid) - dynamic_indent = " " * (len(import_start) + 1) - indent = self.config['indent'] - line_length = self.config['wrap_length'] or self.config['line_length'] - import_statement = formatter(import_start, copy.copy(from_imports), - dynamic_indent, indent, line_length, comments) - if self.config['balanced_wrapping']: - lines = import_statement.split("\n") - line_count = len(lines) - if len(lines) > 1: - minimum_length = min([len(line) for line in lines[:-1]]) - else: - minimum_length = 0 - new_import_statement = import_statement - while (len(lines[-1]) < minimum_length and - len(lines) == line_count and line_length > 10): - import_statement = new_import_statement - line_length -= 1 - new_import_statement = formatter(import_start, copy.copy(from_imports), - dynamic_indent, indent, line_length, comments) - lines = new_import_statement.split("\n") - + import_statement = self._multi_line_reformat(import_start, from_imports, comments) if not do_multiline_reformat and len(import_statement) > self.config['line_length']: import_statement = self._wrap(import_statement) @@ -472,16 +474,43 @@ def _add_from_imports(self, from_modules, section, section_output, ignore_case): section_output.extend(above_comments) section_output.append(import_statement) + def _multi_line_reformat(self, import_start, from_imports, comments): + output_mode = settings.WrapModes._fields[self.config['multi_line_output']].lower() + formatter = getattr(self, "_output_" + output_mode, self._output_grid) + dynamic_indent = " " * (len(import_start) + 1) + indent = self.config['indent'] + line_length = self.config['wrap_length'] or self.config['line_length'] + import_statement = formatter(import_start, copy.copy(from_imports), + dynamic_indent, indent, line_length, comments) + if self.config['balanced_wrapping']: + lines = import_statement.split("\n") + line_count = len(lines) + if len(lines) > 1: + minimum_length = min([len(line) for line in lines[:-1]]) + else: + minimum_length = 0 + new_import_statement = import_statement + while (len(lines[-1]) < minimum_length and + len(lines) == line_count and line_length > 10): + import_statement = new_import_statement + line_length -= 1 + new_import_statement = formatter(import_start, copy.copy(from_imports), + dynamic_indent, indent, line_length, comments) + lines = new_import_statement.split("\n") + if import_statement.count('\n') == 0: + return self._wrap(import_statement) + return import_statement + def _add_formatted_imports(self): """Adds the imports back to the file. (at the index of the first import) sorted alphabetically and split between groups """ - sort_ignore_case = self.config.get('force_alphabetical_sort_within_sections', False) + sort_ignore_case = self.config['force_alphabetical_sort_within_sections'] sections = itertools.chain(self.sections, self.config['forced_separate']) - if self.config.get('no_sections', False): + if self.config['no_sections']: self.imports['no_sections'] = {'straight': [], 'from': {}} for section in sections: self.imports['no_sections']['straight'].extend(self.imports[section].get('straight', [])) @@ -490,30 +519,36 @@ def _add_formatted_imports(self): output = [] for section in sections: - straight_modules = list(self.imports[section]['straight']) + straight_modules = self.imports[section]['straight'] straight_modules = nsorted(straight_modules, key=lambda key: self._module_key(key, self.config)) - from_modules = sorted(list(self.imports[section]['from'].keys())) - from_modules = nsorted(from_modules, key=lambda key: self._module_key(key, self.config, )) + from_modules = self.imports[section]['from'] + from_modules = nsorted(from_modules, key=lambda key: self._module_key(key, self.config)) section_output = [] - if self.config.get('from_first', False): + if self.config['from_first']: self._add_from_imports(from_modules, section, section_output, sort_ignore_case) - if self.config.get('lines_between_types', 0) and from_modules and straight_modules: + if self.config['lines_between_types'] and from_modules and straight_modules: section_output.extend([''] * self.config['lines_between_types']) self._add_straight_imports(straight_modules, section, section_output) else: self._add_straight_imports(straight_modules, section, section_output) - if self.config.get('lines_between_types', 0) and from_modules and straight_modules: + if self.config['lines_between_types'] and from_modules and straight_modules: section_output.extend([''] * self.config['lines_between_types']) self._add_from_imports(from_modules, section, section_output, sort_ignore_case) - if self.config.get('force_sort_within_sections', False): + if self.config['force_sort_within_sections']: def by_module(line): + section = 'B' + if line.startswith('#'): + return 'AA' + line = re.sub('^from ', '', line) line = re.sub('^import ', '', line) + if line.split(' ')[0] in self.config['force_to_top']: + section = 'A' if not self.config['order_by_type']: line = line.lower() - return line + return '{0}{1}'.format(section, line) section_output = nsorted(section_output, key=by_module) if section_output: @@ -525,7 +560,7 @@ def by_module(line): section_title = self.config.get('import_heading_' + str(section_name).lower(), '') if section_title: section_comment = "# {0}".format(section_title) - if not section_comment in self.out_lines[0:1]: + if not section_comment in self.out_lines[0:1] and not section_comment in self.in_lines[0:1]: section_output.insert(0, section_comment) output += section_output + ([''] * self.config['lines_between_sections']) @@ -546,15 +581,18 @@ def by_module(line): if len(self.out_lines) > imports_tail: next_construct = "" self._in_quote = False - for line in self.out_lines[imports_tail:]: - if not self._skip_line(line) and not line.strip().startswith("#") and line.strip(): + tail = self.out_lines[imports_tail:] + for index, line in enumerate(tail): + if not self._skip_line(line) and line.strip(): + if line.strip().startswith("#") and len(tail) > (index + 1) and tail[index + 1].strip(): + continue next_construct = line break if self.config['lines_after_imports'] != -1: self.out_lines[imports_tail:0] = ["" for line in range(self.config['lines_after_imports'])] elif next_construct.startswith("def") or next_construct.startswith("class") or \ - next_construct.startswith("@"): + next_construct.startswith("@") or next_construct.startswith("async def"): self.out_lines[imports_tail:0] = ["", ""] else: self.out_lines[imports_tail:0] = [""] @@ -575,8 +613,16 @@ def _output_grid(self, statement, imports, white_space, indent, line_length, com next_import = imports.pop(0) next_statement = self._add_comments(comments, statement + ", " + next_import) if len(next_statement.split("\n")[-1]) + 1 > line_length: + lines = ['{0}{1}'.format(white_space, next_import.split(" ")[0])] + for part in next_import.split(" ")[1:]: + new_line = '{0} {1}'.format(lines[-1], part) + if len(new_line) + 1 > line_length: + lines.append('{0}{1}'.format(white_space, part)) + else: + lines[-1] = new_line + next_import = '\n'.join(lines) statement = (self._add_comments(comments, "{0},".format(statement)) + - "\n{0}{1}".format(white_space, next_import)) + "\n{0}".format(next_import)) comments = None else: statement += ", " + next_import @@ -693,7 +739,7 @@ def _skip_line(self, line): elif self._in_top_comment: if not line.startswith("#"): self._in_top_comment = False - self._first_comment_index_end = self.index + self._first_comment_index_end = self.index - 1 if '"' in line or "'" in line: index = 0 @@ -767,7 +813,7 @@ def _parse(self): self.out_lines.append(line) continue - line = line.replace("\t", " ") + line = line.replace("\t", " ").replace('import*', 'import *') if self.import_index == -1: self.import_index = self.index - 1 @@ -778,7 +824,7 @@ def _parse(self): if import_type == "from" and len(stripped_line) == 2 and stripped_line[1] != "*" and new_comments: nested_comments[stripped_line[-1]] = comments[0] - if "(" in line and not self._at_end(): + if "(" in line.split("#")[0] and not self._at_end(): while not line.strip().endswith(")") and not self._at_end(): line, comments, new_comments = self._strip_comments(self._get_line(), comments) stripped_line = self._strip_syntax(line).strip() @@ -834,12 +880,12 @@ def _parse(self): if comments: self.comments['from'].setdefault(import_from, []).extend(comments) - if len(self.out_lines) > max(self.import_index, self._first_comment_index_end, 1) - 1: + if len(self.out_lines) > max(self.import_index, self._first_comment_index_end + 1, 1) - 1: last = self.out_lines and self.out_lines[-1].rstrip() or "" while (last.startswith("#") and not last.endswith('"""') and not last.endswith("'''") and not 'isort:imports-' in last): self.comments['above']['from'].setdefault(import_from, []).insert(0, self.out_lines.pop(-1)) - if len(self.out_lines) > max(self.import_index - 1, self._first_comment_index_end, 1) - 1: + if len(self.out_lines) > max(self.import_index - 1, self._first_comment_index_end + 1, 1) - 1: last = self.out_lines[-1].rstrip() else: last = "" @@ -849,21 +895,21 @@ def _parse(self): if root.get(import_from, False): root[import_from].update(imports) else: - root[import_from] = set(imports) + root[import_from] = OrderedSet(imports) else: for module in imports: if comments: self.comments['straight'][module] = comments comments = None - if len(self.out_lines) > max(self.import_index, self._first_comment_index_end, 1) - 1: + if len(self.out_lines) > max(self.import_index, self._first_comment_index_end + 1, 1) - 1: + last = self.out_lines and self.out_lines[-1].rstrip() or "" - while (last.startswith("#") and not last.endswith('"""') and not last.endswith("'''") - and not 'isort:imports-' in last): + while (last.startswith("#") and not last.endswith('"""') and not last.endswith("'''") and + not 'isort:imports-' in last): self.comments['above']['straight'].setdefault(module, []).insert(0, self.out_lines.pop(-1)) - if len(self.out_lines) > max(self.import_index - 1, self._first_comment_index_end, - 1) - 1: + if len(self.out_lines) > 0: last = self.out_lines[-1].rstrip() else: last = "" @@ -894,3 +940,30 @@ def coding_check(fname, default='utf-8'): break return coding + + +def get_stdlib_path(): + """Returns the path to the standard lib for the current path installation. + + This function can be dropped and "sysconfig.get_paths()" used directly once Python 2.6 support is dropped. + """ + if sys.version_info >= (2, 7): + import sysconfig + return sysconfig.get_paths()['stdlib'] + else: + return os.path.join(sys.prefix, 'lib') + + +def exists_case_sensitive(path): + """ + Returns if the given path exists and also matches the case on Windows. + + When finding files that can be imported, it is important for the cases to match because while + file os.path.exists("module.py") and os.path.exists("MODULE.py") both return True on Windows, Python + can only import using the case of the real file. + """ + result = os.path.exists(path) + if sys.platform.startswith('win') and result: + directory, basename = os.path.split(path) + result = basename in os.listdir(directory) + return result diff --git a/pythonFiles/isort/main.py b/pythonFiles/isort/main.py index b5ca93d9207f..eae7afa53409 100755 --- a/pythonFiles/isort/main.py +++ b/pythonFiles/isort/main.py @@ -30,11 +30,11 @@ from isort import SortImports, __version__ from isort.settings import DEFAULT_SECTIONS, default, from_path, should_skip -from .pie_slice import * +from .pie_slice import itemsview -INTRO = """ -/#######################################################################\\ +INTRO = r""" +/#######################################################################\ `sMMy` .yyyy- ` @@ -130,7 +130,7 @@ def run(self): if incorrectly_sorted: wrong_sorted_files = True except IOError as e: - print("WARNING: Unable to parse file {0} due to {1}".format(file_name, e)) + print("WARNING: Unable to parse file {0} due to {1}".format(python_file, e)) if wrong_sorted_files: exit(1) @@ -164,37 +164,35 @@ def create_parser(): help='Force sortImports to recognize a module as being part of the current python project.') parser.add_argument('--virtual-env', dest='virtual_env', help='Virtual environment to use for determining whether a package is third-party') - parser.add_argument('-m', '--multi_line', dest='multi_line_output', type=int, choices=[0, 1, 2, 3, 4, 5], + parser.add_argument('-m', '--multi-line', dest='multi_line_output', type=int, choices=[0, 1, 2, 3, 4, 5], help='Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, ' '5-vert-grid-grouped).') parser.add_argument('-i', '--indent', help='String to place for indents defaults to " " (4 spaces).', dest='indent', type=str) - parser.add_argument('-a', '--add_import', dest='add_imports', action='append', + parser.add_argument('-a', '--add-import', dest='add_imports', action='append', help='Adds the specified import line to all files, ' 'automatically determining correct placement.') - parser.add_argument('-af', '--force_adds', dest='force_adds', action='store_true', + parser.add_argument('-af', '--force-adds', dest='force_adds', action='store_true', help='Forces import adds even if the original file is empty.') - parser.add_argument('-r', '--remove_import', dest='remove_imports', action='append', + parser.add_argument('-r', '--remove-import', dest='remove_imports', action='append', help='Removes the specified import from all files.') - parser.add_argument('-ls', '--length_sort', help='Sort imports by their string length.', - dest='length_sort', action='store_true', default=False) + parser.add_argument('-ls', '--length-sort', help='Sort imports by their string length.', + dest='length_sort', action='store_true') parser.add_argument('-d', '--stdout', help='Force resulting output to stdout, instead of in-place.', dest='write_to_stdout', action='store_true') - parser.add_argument('-c', '--check-only', action='store_true', default=False, dest="check", + parser.add_argument('-c', '--check-only', action='store_true', dest="check", help='Checks the file for unsorted / unformatted imports and prints them to the ' 'command line without modifying the file.') - parser.add_argument('-ws', '--enforce-white-space', action='store_true', default=False, dest="enforce_white_space", - help='Tells isort to enforce white space difference when --check-only is being used.') + parser.add_argument('-ws', '--ignore-whitespace', action='store_true', dest="ignore_whitespace", + help='Tells isort to ignore whitespace differences when --check-only is being used.') parser.add_argument('-sl', '--force-single-line-imports', dest='force_single_line', action='store_true', help='Forces all from imports to appear on their own line') - parser.add_argument('--force_single_line_imports', dest='force_single_line', action='store_true', - help=argparse.SUPPRESS) parser.add_argument('-ds', '--no-sections', help='Put all imports into the same section bucket', dest='no_sections', action='store_true') parser.add_argument('-sd', '--section-default', dest='default_section', help='Sets the default section for imports (by default FIRSTPARTY) options: ' + str(DEFAULT_SECTIONS)) - parser.add_argument('-df', '--diff', dest='show_diff', default=False, action='store_true', + parser.add_argument('-df', '--diff', dest='show_diff', action='store_true', help="Prints a diff of all the changes isort would make to a file, instead of " "changing it in place") parser.add_argument('-e', '--balanced', dest='balanced_wrapping', action='store_true', @@ -218,22 +216,25 @@ def create_parser(): help='Shows verbose output, such as when files are skipped or when a check is successful.') parser.add_argument('-q', '--quiet', action='store_true', dest="quiet", help='Shows extra quiet output, only errors are outputted.') - parser.add_argument('-sp', '--settings-path', dest="settings_path", + parser.add_argument('-sp', '--settings-path', dest="settings_path", help='Explicitly set the settings path instead of auto determining based on file location.') parser.add_argument('-ff', '--from-first', dest='from_first', help="Switches the typical ordering preference, showing from imports first then straight ones.") parser.add_argument('-wl', '--wrap-length', dest='wrap_length', help="Specifies how long lines that are wrapped should be, if not set line_length is used.") - parser.add_argument('-fgw', '--force-grid-wrap', action='store_true', dest="force_grid_wrap", - help='Force from imports to be grid wrapped regardless of line length') - parser.add_argument('-fass', '--force-alphabetical-sort-within-sections', action='store_true', + parser.add_argument('-fgw', '--force-grid-wrap', nargs='?', const=2, type=int, dest="force_grid_wrap", + help='Force number of from imports (defaults to 2) to be grid wrapped regardless of line ' + 'length') + parser.add_argument('-fass', '--force-alphabetical-sort-within-sections', action='store_true', dest="force_alphabetical_sort", help='Force all imports to be sorted alphabetically within a ' 'section') - parser.add_argument('-fas', '--force-alphabetical-sort', action='store_true', dest="force_alphabetical_sort", + parser.add_argument('-fas', '--force-alphabetical-sort', action='store_true', dest="force_alphabetical_sort", help='Force all imports to be sorted as a single section') - parser.add_argument('-fss', '--force-sort-within-sections', action='store_true', dest="force_sort_within_sections", + parser.add_argument('-fss', '--force-sort-within-sections', action='store_true', dest="force_sort_within_sections", help='Force imports to be sorted by module, independent of import_type') parser.add_argument('-lbt', '--lines-between-types', dest='lines_between_types', type=int) + parser.add_argument('-up', '--use-parentheses', dest='use_parentheses', action='store_true', + help='Use parenthesis for line continuation on lenght limit instead of slashes.') arguments = dict((key, value) for (key, value) in itemsview(vars(parser.parse_args())) if value) if 'dont_order_by_type' in arguments: @@ -267,7 +268,7 @@ def main(): if arguments.get('recursive', False): file_names = iter_source_code(file_names, config, skipped) num_skipped = 0 - if config.get('verbose', False) or config.get('show_logo', False): + if config['verbose'] or config.get('show_logo', False): print(INTRO) for file_name in file_names: try: diff --git a/pythonFiles/isort/natural.py b/pythonFiles/isort/natural.py index 0529fa60bb95..aac8c4a36157 100644 --- a/pythonFiles/isort/natural.py +++ b/pythonFiles/isort/natural.py @@ -33,7 +33,7 @@ def _atoi(text): def _natural_keys(text): - return [_atoi(c) for c in re.split('(\d+)', text)] + return [_atoi(c) for c in re.split(r'(\d+)', text)] def nsorted(to_sort, key=None): diff --git a/pythonFiles/isort/pie_slice.py b/pythonFiles/isort/pie_slice.py index d8a35769ece9..bfb341a7f7ec 100644 --- a/pythonFiles/isort/pie_slice.py +++ b/pythonFiles/isort/pie_slice.py @@ -22,6 +22,7 @@ from __future__ import absolute_import import abc +import collections import functools import sys from numbers import Integral @@ -67,6 +68,7 @@ class Form(with_metaclass(FormType, BaseForm)): class metaclass(meta): __call__ = type.__call__ __init__ = type.__init__ + def __new__(cls, name, this_bases, d): if this_bases is None: return type.__new__(cls, name, (), d) @@ -118,6 +120,7 @@ def __instancecheck__(cls, instance): import builtins from urllib import parse + input = input integer_types = (int, ) def u(string): @@ -428,7 +431,7 @@ def __eq__(self, other): if isinstance(other, OrderedDict): if len(self) != len(other): return False - for p, q in zip(self.items(), other.items()): + for p, q in zip(self.items(), other.items()): if p != q: return False return True @@ -526,3 +529,66 @@ def cache_clear(): else: from functools import lru_cache + + +class OrderedSet(collections.MutableSet): + + def __init__(self, iterable=None): + self.end = end = [] + end += [None, end, end] + self.map = {} + if iterable is not None: + self |= iterable + + def __len__(self): + return len(self.map) + + def __contains__(self, key): + return key in self.map + + def add(self, key): + if key not in self.map: + end = self.end + curr = end[1] + curr[2] = end[1] = self.map[key] = [key, curr, end] + + def discard(self, key): + if key in self.map: + key, prev, next = self.map.pop(key) + prev[2] = next + next[1] = prev + + def __iter__(self): + end = self.end + curr = end[2] + while curr is not end: + yield curr[0] + curr = curr[2] + + def __reversed__(self): + end = self.end + curr = end[1] + while curr is not end: + yield curr[0] + curr = curr[1] + + def pop(self, last=True): + if not self: + raise KeyError('set is empty') + key = self.end[1][0] if last else self.end[2][0] + self.discard(key) + return key + + def __repr__(self): + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, list(self)) + + def __eq__(self, other): + if isinstance(other, OrderedSet): + return len(self) == len(other) and list(self) == list(other) + return set(self) == set(other) + + def update(self, other): + for item in other: + self.add(item) diff --git a/pythonFiles/isort/settings.py b/pythonFiles/isort/settings.py index d17ba58aa8b4..15cdb21094ac 100644 --- a/pythonFiles/isort/settings.py +++ b/pythonFiles/isort/settings.py @@ -26,9 +26,10 @@ import fnmatch import os +import posixpath from collections import namedtuple -from .pie_slice import * +from .pie_slice import itemsview, lru_cache, native_str try: import configparser @@ -36,7 +37,7 @@ import ConfigParser as configparser MAX_CONFIG_SEARCH_DEPTH = 25 # The number of parent directories isort will look for a config file within -DEFAULT_SECTIONS = ("FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER") +DEFAULT_SECTIONS = ('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER') WrapModes = ('GRID', 'VERTICAL', 'HANGING_INDENT', 'VERTICAL_HANGING_INDENT', 'VERTICAL_GRID', 'VERTICAL_GRID_GROUPED', 'NOQA') WrapModes = namedtuple('WrapModes', WrapModes)(*range(len(WrapModes))) @@ -50,26 +51,51 @@ 'sections': DEFAULT_SECTIONS, 'no_sections': False, 'known_future_library': ['__future__'], - 'known_standard_library': ["abc", "anydbm", "argparse", "array", "asynchat", "asyncore", "atexit", "base64", - "BaseHTTPServer", "bisect", "bz2", "calendar", "cgitb", "cmd", "codecs", - "collections", "commands", "compileall", "ConfigParser", "contextlib", "Cookie", - "copy", "cPickle", "cProfile", "cStringIO", "csv", "datetime", "dbhash", "dbm", - "decimal", "difflib", "dircache", "dis", "doctest", "dumbdbm", "EasyDialogs", - "errno", "exceptions", "filecmp", "fileinput", "fnmatch", "fractions", - "functools", "gc", "gdbm", "getopt", "getpass", "gettext", "glob", "grp", "gzip", - "hashlib", "heapq", "hmac", "imaplib", "imp", "inspect", "io", "itertools", "json", - "linecache", "locale", "logging", "mailbox", "math", "mhlib", "mmap", - "multiprocessing", "operator", "optparse", "os", "pdb", "pickle", "pipes", - "pkgutil", "platform", "plistlib", "pprint", "profile", "pstats", "pwd", "pyclbr", - "pydoc", "Queue", "random", "re", "readline", "resource", "rlcompleter", - "robotparser", "sched", "select", "shelve", "shlex", "shutil", "signal", - "SimpleXMLRPCServer", "site", "sitecustomize", "smtpd", "smtplib", "socket", - "SocketServer", "sqlite3", "string", "StringIO", "struct", "subprocess", "sys", - "sysconfig", "tabnanny", "tarfile", "tempfile", "textwrap", "threading", "time", - "timeit", "trace", "traceback", "unittest", "urllib", "urllib2", "urlparse", - "usercustomize", "uuid", "warnings", "weakref", "webbrowser", "whichdb", "xml", - "xmlrpclib", "zipfile", "zipimport", "zlib", 'builtins', '__builtin__', 'thread', - "binascii", "statistics", "unicodedata", "fcntl", 'pathlib'], + 'known_standard_library': ['AL', 'BaseHTTPServer', 'Bastion', 'CGIHTTPServer', 'Carbon', 'ColorPicker', + 'ConfigParser', 'Cookie', 'DEVICE', 'DocXMLRPCServer', 'EasyDialogs', 'FL', + 'FrameWork', 'GL', 'HTMLParser', 'MacOS', 'MimeWriter', 'MiniAEFrame', 'Nav', + 'PixMapWrapper', 'Queue', 'SUNAUDIODEV', 'ScrolledText', 'SimpleHTTPServer', + 'SimpleXMLRPCServer', 'SocketServer', 'StringIO', 'Tix', 'Tkinter', 'UserDict', + 'UserList', 'UserString', 'W', '__builtin__', 'abc', 'aepack', 'aetools', + 'aetypes', 'aifc', 'al', 'anydbm', 'applesingle', 'argparse', 'array', 'ast', + 'asynchat', 'asyncio', 'asyncore', 'atexit', 'audioop', 'autoGIL', 'base64', + 'bdb', 'binascii', 'binhex', 'bisect', 'bsddb', 'buildtools', 'builtins', + 'bz2', 'cPickle', 'cProfile', 'cStringIO', 'calendar', 'cd', 'cfmfile', 'cgi', + 'cgitb', 'chunk', 'cmath', 'cmd', 'code', 'codecs', 'codeop', 'collections', + 'colorsys', 'commands', 'compileall', 'compiler', 'concurrent', 'configparser', + 'contextlib', 'cookielib', 'copy', 'copy_reg', 'copyreg', 'crypt', 'csv', + 'ctypes', 'curses', 'datetime', 'dbhash', 'dbm', 'decimal', 'difflib', + 'dircache', 'dis', 'distutils', 'dl', 'doctest', 'dumbdbm', 'dummy_thread', + 'dummy_threading', 'email', 'encodings', 'ensurepip', 'enum', 'errno', + 'exceptions', 'faulthandler', 'fcntl', 'filecmp', 'fileinput', 'findertools', + 'fl', 'flp', 'fm', 'fnmatch', 'formatter', 'fpectl', 'fpformat', 'fractions', + 'ftplib', 'functools', 'future_builtins', 'gc', 'gdbm', 'gensuitemodule', + 'getopt', 'getpass', 'gettext', 'gl', 'glob', 'grp', 'gzip', 'hashlib', + 'heapq', 'hmac', 'hotshot', 'html', 'htmlentitydefs', 'htmllib', 'http', + 'httplib', 'ic', 'icopen', 'imageop', 'imaplib', 'imgfile', 'imghdr', 'imp', + 'importlib', 'imputil', 'inspect', 'io', 'ipaddress', 'itertools', 'jpeg', + 'json', 'keyword', 'lib2to3', 'linecache', 'locale', 'logging', 'lzma', + 'macerrors', 'macostools', 'macpath', 'macresource', 'mailbox', 'mailcap', + 'marshal', 'math', 'md5', 'mhlib', 'mimetools', 'mimetypes', 'mimify', 'mmap', + 'modulefinder', 'msilib', 'msvcrt', 'multifile', 'multiprocessing', 'mutex', + 'netrc', 'new', 'nis', 'nntplib', 'numbers', 'operator', 'optparse', 'os', + 'ossaudiodev', 'parser', 'pathlib', 'pdb', 'pickle', 'pickletools', 'pipes', + 'pkgutil', 'platform', 'plistlib', 'popen2', 'poplib', 'posix', 'posixfile', + 'pprint', 'profile', 'pstats', 'pty', 'pwd', 'py_compile', 'pyclbr', 'pydoc', + 'queue', 'quopri', 'random', 're', 'readline', 'reprlib', 'resource', 'rexec', + 'rfc822', 'rlcompleter', 'robotparser', 'runpy', 'sched', 'secrets', 'select', + 'selectors', 'sets', 'sgmllib', 'sha', 'shelve', 'shlex', 'shutil', 'signal', + 'site', 'sitecustomize', 'smtpd', 'smtplib', 'sndhdr', 'socket', 'socketserver', + 'spwd', 'sqlite3', 'ssl', 'stat', 'statistics', 'statvfs', 'string', 'stringprep', + 'struct', 'subprocess', 'sunau', 'sunaudiodev', 'symbol', 'symtable', 'sys', + 'sysconfig', 'syslog', 'tabnanny', 'tarfile', 'telnetlib', 'tempfile', 'termios', + 'test', 'textwrap', 'this', 'thread', 'threading', 'time', 'timeit', 'tkinter', + 'token', 'tokenize', 'trace', 'traceback', 'tracemalloc', 'ttk', 'tty', 'turtle', + 'turtledemo', 'types', 'typing', 'unicodedata', 'unittest', 'urllib', 'urllib2', + 'urlparse', 'user', 'usercustomize', 'uu', 'uuid', 'venv', 'videoreader', + 'warnings', 'wave', 'weakref', 'webbrowser', 'whichdb', 'winreg', 'winsound', + 'wsgiref', 'xdrlib', 'xml', 'xmlrpc', 'xmlrpclib', 'zipapp', 'zipfile', + 'zipimport', 'zlib'], 'known_third_party': ['google.appengine.api'], 'known_first_party': [], 'multi_line_output': WrapModes.GRID, @@ -101,10 +127,10 @@ 'force_adds': False, 'force_alphabetical_sort_within_sections': False, 'force_alphabetical_sort': False, - 'force_grid_wrap': False, + 'force_grid_wrap': 0, 'force_sort_within_sections': False, 'show_diff': False, - 'enforce_white_space': False} + 'ignore_whitespace': False} @lru_cache() @@ -113,6 +139,7 @@ def from_path(path): _update_settings_with_config(path, '.editorconfig', '~/.editorconfig', ('*', '*.py', '**.py'), computed_settings) _update_settings_with_config(path, '.isort.cfg', '~/.isort.cfg', ('settings', 'isort'), computed_settings) _update_settings_with_config(path, 'setup.cfg', None, ('isort', ), computed_settings) + _update_settings_with_config(path, 'tox.ini', None, ('isort', ), computed_settings) return computed_settings @@ -141,17 +168,17 @@ def _update_with_config_file(file_path, sections, computed_settings): if not settings: return - if file_path.endswith(".editorconfig"): - indent_style = settings.pop('indent_style', "").strip() - indent_size = settings.pop('indent_size', "").strip() - if indent_style == "space": - computed_settings['indent'] = " " * (indent_size and int(indent_size) or 4) - elif indent_style == "tab": - computed_settings['indent'] = "\t" * (indent_size and int(indent_size) or 1) + if file_path.endswith('.editorconfig'): + indent_style = settings.pop('indent_style', '').strip() + indent_size = settings.pop('indent_size', '').strip() + if indent_style == 'space': + computed_settings['indent'] = ' ' * (indent_size and int(indent_size) or 4) + elif indent_style == 'tab': + computed_settings['indent'] = '\t' * (indent_size and int(indent_size) or 1) - max_line_length = settings.pop('max_line_length', "").strip() + max_line_length = settings.pop('max_line_length', '').strip() if max_line_length: - computed_settings['line_length'] = int(max_line_length) + computed_settings['line_length'] = float('inf') if max_line_length == 'off' else int(max_line_length) for key, value in itemsview(settings): access_key = key.replace('not_', '').lower() @@ -166,27 +193,34 @@ def _update_with_config_file(file_path, sections, computed_settings): computed_settings[access_key] = list(existing_data.difference(_as_list(value))) else: computed_settings[access_key] = list(existing_data.union(_as_list(value))) - elif existing_value_type == bool and value.lower().strip() == "false": + elif existing_value_type == bool and value.lower().strip() == 'false': computed_settings[access_key] = False elif key.startswith('known_'): computed_settings[access_key] = list(_as_list(value)) + elif key == 'force_grid_wrap': + try: + result = existing_value_type(value) + except ValueError: + # backwards compat + result = default.get(access_key) if value.lower().strip() == 'false' else 2 + computed_settings[access_key] = result else: computed_settings[access_key] = existing_value_type(value) def _as_list(value): - return filter(bool, [item.strip() for item in value.replace('\n', ',').split(",")]) + return filter(bool, [item.strip() for item in value.replace('\n', ',').split(',')]) @lru_cache() def _get_config_data(file_path, sections): with open(file_path, 'rU') as config_file: - if file_path.endswith(".editorconfig"): - line = "\n" + if file_path.endswith('.editorconfig'): + line = '\n' last_position = config_file.tell() while line: line = config_file.readline() - if "[" in line: + if '[' in line: config_file.seek(last_position) break last_position = config_file.tell() @@ -206,7 +240,7 @@ def _get_config_data(file_path, sections): def should_skip(filename, config, path='/'): """Returns True if the file should be skipped based on the passed in settings.""" for skip_path in config['skip']: - if os.path.join(path, filename).endswith('/' + skip_path.lstrip('/')): + if posixpath.abspath(posixpath.join(path, filename)) == posixpath.abspath(skip_path.replace('\\', '/')): return True position = os.path.split(filename) diff --git a/pythonFiles/preview/jedi/_compatibility.py b/pythonFiles/preview/jedi/_compatibility.py index 3c14b6f26f48..352c2d6b5da9 100644 --- a/pythonFiles/preview/jedi/_compatibility.py +++ b/pythonFiles/preview/jedi/_compatibility.py @@ -12,11 +12,14 @@ except ImportError: pass +# Cannot use sys.version.major and minor names, because in Python 2.6 it's not +# a namedtuple. is_py3 = sys.version_info[0] >= 3 -is_py33 = is_py3 and sys.version_info.minor >= 3 -is_py34 = is_py3 and sys.version_info.minor >= 4 -is_py35 = is_py3 and sys.version_info.minor >= 5 +is_py33 = is_py3 and sys.version_info[1] >= 3 +is_py34 = is_py3 and sys.version_info[1] >= 4 +is_py35 = is_py3 and sys.version_info[1] >= 5 is_py26 = not is_py3 and sys.version_info[1] < 7 +py_version = int(str(sys.version_info[0]) + str(sys.version_info[1])) class DummyFile(object): @@ -205,7 +208,8 @@ def u(string): """ if is_py3: return str(string) - elif not isinstance(string, unicode): + + if not isinstance(string, unicode): return unicode(str(string), 'UTF-8') return string diff --git a/pythonFiles/preview/jedi/api/completion.py b/pythonFiles/preview/jedi/api/completion.py index 2c0c44db8447..cc362dacbfa8 100644 --- a/pythonFiles/preview/jedi/api/completion.py +++ b/pythonFiles/preview/jedi/api/completion.py @@ -158,7 +158,7 @@ def _get_context_completions(self): # No completions for ``with x as foo`` and ``import x as foo``. # Also true for defining names as a class or function. return list(self._get_class_context_completions(is_function=True)) - elif symbol_names[-1] == 'trailer' and nodes[-1] == '.': + elif symbol_names[-1] in ('trailer', 'dotted_name') and nodes[-1] == '.': dot = self._module.get_leaf_for_position(self._position) atom_expr = call_of_leaf(dot.get_previous_leaf()) completion_names += self._trailer_completions(atom_expr) diff --git a/pythonFiles/preview/jedi/evaluate/__init__.py b/pythonFiles/preview/jedi/evaluate/__init__.py index 9a6ad64a377e..fe2c70237afd 100644 --- a/pythonFiles/preview/jedi/evaluate/__init__.py +++ b/pythonFiles/preview/jedi/evaluate/__init__.py @@ -162,7 +162,7 @@ def eval_statement(self, stmt, seek_name=None): types = finder.check_tuple_assignments(self, types, seek_name) first_operation = stmt.first_operation() - if first_operation not in ('=', None) and not isinstance(stmt, er.InstanceElement): # TODO don't check for this. + if first_operation not in ('=', None) and not isinstance(stmt, er.InstanceElement) and first_operation.type == 'operator': # TODO don't check for this. # `=` is always the last character in aug assignments -> -1 operator = copy.copy(first_operation) operator.value = operator.value[:-1] @@ -327,6 +327,8 @@ def _eval_element_not_cached(self, element): types = types elif element.type == 'eval_input': types = self._eval_element_not_cached(element.children[0]) + elif element.type == 'annassign': + types = self.eval_element(element.children[1]) else: types = precedence.calculate_children(self, element.children) debug.dbg('eval_element result %s', types) diff --git a/pythonFiles/preview/jedi/parser/grammar3.6.txt b/pythonFiles/preview/jedi/parser/grammar3.6.txt new file mode 100644 index 000000000000..b44a56981564 --- /dev/null +++ b/pythonFiles/preview/jedi/parser/grammar3.6.txt @@ -0,0 +1,161 @@ +# Grammar for Python + +# Note: Changing the grammar specified in this file will most likely +# require corresponding changes in the parser module +# (../Modules/parsermodule.c). If you can't make the changes to +# that module yourself, please co-ordinate the required changes +# with someone who can; ask around on python-dev for help. Fred +# Drake will probably be listening there. + +# NOTE WELL: You should also follow all the steps listed at +# https://docs.python.org/devguide/grammar.html + +# Start symbols for the grammar: +# file_input is a module or sequence of commands read from an input file; +# single_input is a single interactive statement; +# eval_input is the input for the eval() functions. +# NB: compound_stmt in single_input is followed by extra NEWLINE! +file_input: (NEWLINE | stmt)* ENDMARKER +single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE +eval_input: testlist NEWLINE* ENDMARKER + +decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE +decorators: decorator+ +decorated: decorators (classdef | funcdef | async_funcdef) + +# NOTE: Francisco Souza/Reinoud Elhorst, using ASYNC/'await' keywords instead of +# skipping python3.5+ compatibility, in favour of 3.7 solution +async_funcdef: 'async' funcdef +funcdef: 'def' NAME parameters ['->' test] ':' suite + +parameters: '(' [typedargslist] ')' +typedargslist: (tfpdef ['=' test] (',' tfpdef ['=' test])* [',' [ + '*' [tfpdef] (',' tfpdef ['=' test])* [',' ['**' tfpdef [',']]] + | '**' tfpdef [',']]] + | '*' [tfpdef] (',' tfpdef ['=' test])* [',' ['**' tfpdef [',']]] + | '**' tfpdef [',']) +tfpdef: NAME [':' test] +varargslist: (vfpdef ['=' test] (',' vfpdef ['=' test])* [',' [ + '*' [vfpdef] (',' vfpdef ['=' test])* [',' ['**' vfpdef [',']]] + | '**' vfpdef [',']]] + | '*' [vfpdef] (',' vfpdef ['=' test])* [',' ['**' vfpdef [',']]] + | '**' vfpdef [','] +) +vfpdef: NAME + +stmt: simple_stmt | compound_stmt +simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE +small_stmt: (expr_stmt | del_stmt | pass_stmt | flow_stmt | + import_stmt | global_stmt | nonlocal_stmt | assert_stmt) +expr_stmt: testlist_star_expr (annassign | augassign (yield_expr|testlist) | + ('=' (yield_expr|testlist_star_expr))*) +annassign: ':' test ['=' test] +testlist_star_expr: (test|star_expr) (',' (test|star_expr))* [','] +augassign: ('+=' | '-=' | '*=' | '@=' | '/=' | '%=' | '&=' | '|=' | '^=' | + '<<=' | '>>=' | '**=' | '//=') +# For normal and annotated assignments, additional restrictions enforced by the interpreter +del_stmt: 'del' exprlist +pass_stmt: 'pass' +flow_stmt: break_stmt | continue_stmt | return_stmt | raise_stmt | yield_stmt +break_stmt: 'break' +continue_stmt: 'continue' +return_stmt: 'return' [testlist] +yield_stmt: yield_expr +raise_stmt: 'raise' [test ['from' test]] +import_stmt: import_name | import_from +import_name: 'import' dotted_as_names +# note below: the ('.' | '...') is necessary because '...' is tokenized as ELLIPSIS +import_from: ('from' (('.' | '...')* dotted_name | ('.' | '...')+) + 'import' ('*' | '(' import_as_names ')' | import_as_names)) +import_as_name: NAME ['as' NAME] +dotted_as_name: dotted_name ['as' NAME] +import_as_names: import_as_name (',' import_as_name)* [','] +dotted_as_names: dotted_as_name (',' dotted_as_name)* +dotted_name: NAME ('.' NAME)* +global_stmt: 'global' NAME (',' NAME)* +nonlocal_stmt: 'nonlocal' NAME (',' NAME)* +assert_stmt: 'assert' test [',' test] + +compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated | async_stmt +async_stmt: 'async' (funcdef | with_stmt | for_stmt) +if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite] +while_stmt: 'while' test ':' suite ['else' ':' suite] +for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite] +try_stmt: ('try' ':' suite + ((except_clause ':' suite)+ + ['else' ':' suite] + ['finally' ':' suite] | + 'finally' ':' suite)) +with_stmt: 'with' with_item (',' with_item)* ':' suite +with_item: test ['as' expr] +# NB compile.c makes sure that the default except clause is last +except_clause: 'except' [test ['as' NAME]] +# Edit by Francisco Souza/David Halter: The stmt is now optional. This reflects +# how Jedi allows classes and functions to be empty, which is beneficial for +# autocompletion. +suite: simple_stmt | NEWLINE INDENT stmt* DEDENT + +test: or_test ['if' or_test 'else' test] | lambdef +test_nocond: or_test | lambdef_nocond +lambdef: 'lambda' [varargslist] ':' test +lambdef_nocond: 'lambda' [varargslist] ':' test_nocond +or_test: and_test ('or' and_test)* +and_test: not_test ('and' not_test)* +not_test: 'not' not_test | comparison +comparison: expr (comp_op expr)* +# <> isn't actually a valid comparison operator in Python. It's here for the +# sake of a __future__ import described in PEP 401 (which really works :-) +comp_op: '<'|'>'|'=='|'>='|'<='|'<>'|'!='|'in'|'not' 'in'|'is'|'is' 'not' +star_expr: '*' expr +expr: xor_expr ('|' xor_expr)* +xor_expr: and_expr ('^' and_expr)* +and_expr: shift_expr ('&' shift_expr)* +shift_expr: arith_expr (('<<'|'>>') arith_expr)* +arith_expr: term (('+'|'-') term)* +term: factor (('*'|'@'|'/'|'%'|'//') factor)* +factor: ('+'|'-'|'~') factor | power +power: atom_expr ['**' factor] +atom_expr: ['await'] atom trailer* +atom: ('(' [yield_expr|testlist_comp] ')' | + '[' [testlist_comp] ']' | + '{' [dictorsetmaker] '}' | + NAME | NUMBER | STRING+ | '...' | 'None' | 'True' | 'False') +testlist_comp: (test|star_expr) ( comp_for | (',' (test|star_expr))* [','] ) +trailer: '(' [arglist] ')' | '[' subscriptlist ']' | '.' NAME +subscriptlist: subscript (',' subscript)* [','] +subscript: test | [test] ':' [test] [sliceop] +sliceop: ':' [test] +exprlist: (expr|star_expr) (',' (expr|star_expr))* [','] +testlist: test (',' test)* [','] +dictorsetmaker: ( ((test ':' test | '**' expr) + (comp_for | (',' (test ':' test | '**' expr))* [','])) | + ((test | star_expr) + (comp_for | (',' (test | star_expr))* [','])) ) + +classdef: 'class' NAME ['(' [arglist] ')'] ':' suite + +arglist: argument (',' argument)* [','] + +# The reason that keywords are test nodes instead of NAME is that using NAME +# results in an ambiguity. ast.c makes sure it's a NAME. +# "test '=' test" is really "keyword '=' test", but we have no such token. +# These need to be in a single rule to avoid grammar that is ambiguous +# to our LL(1) parser. Even though 'test' includes '*expr' in star_expr, +# we explicitly match '*' here, too, to give it proper precedence. +# Illegal combinations and orderings are blocked in ast.c: +# multiple (test comp_for) arguments are blocked; keyword unpackings +# that precede iterable unpackings are blocked; etc. +argument: ( test [comp_for] | + test '=' test | + '**' test | + '*' test ) + +comp_iter: comp_for | comp_if +comp_for: ['async'] 'for' exprlist 'in' or_test [comp_iter] +comp_if: 'if' test_nocond [comp_iter] + +# not used in grammar, but may appear in "node" passed from Parser to Compiler +encoding_decl: NAME + +yield_expr: 'yield' [yield_arg] +yield_arg: 'from' test | testlist diff --git a/pythonFiles/preview/jedi/parser/tree.py b/pythonFiles/preview/jedi/parser/tree.py index c5a2d554d10e..0033a49cbfde 100644 --- a/pythonFiles/preview/jedi/parser/tree.py +++ b/pythonFiles/preview/jedi/parser/tree.py @@ -1522,9 +1522,14 @@ class ExprStmt(BaseNode, DocstringMixin): __slots__ = () def get_defined_names(self): - return list(chain.from_iterable(_defined_names(self.children[i]) - for i in range(0, len(self.children) - 2, 2) - if '=' in self.children[i + 1].value)) + names = [] + if self.children[1].type == 'annassign': + names = _defined_names(self.children[0]) + return list(chain.from_iterable( + _defined_names(self.children[i]) + for i in range(0, len(self.children) - 2, 2) + if '=' in self.children[i + 1].value) + ) + names def get_rhs(self): """Returns the right-hand-side of the equals.""" diff --git a/pythonFiles/preview/jedi/settings.py b/pythonFiles/preview/jedi/settings.py index 13dcfd45f50b..17cf9a0237b6 100644 --- a/pythonFiles/preview/jedi/settings.py +++ b/pythonFiles/preview/jedi/settings.py @@ -109,7 +109,7 @@ """ if platform.system().lower() == 'windows': - _cache_directory = os.path.join(os.getenv('APPDATA') or '~', 'Jedi', + _cache_directory = os.path.join(os.getenv('LOCALAPPDATA') or '~', 'Jedi', 'Jedi') elif platform.system().lower() == 'darwin': _cache_directory = os.path.join('~', 'Library', 'Caches', 'Jedi') diff --git a/pythonFiles/refactor.py b/pythonFiles/refactor.py index 9235a5b63a33..dd4b23ee7727 100644 --- a/pythonFiles/refactor.py +++ b/pythonFiles/refactor.py @@ -6,13 +6,18 @@ import sys import json import traceback -import rope -from rope.base import libutils -from rope.refactor.rename import Rename -from rope.refactor.extract import ExtractMethod, ExtractVariable -import rope.base.project -import rope.base.taskhandle +try: + import rope + from rope.base import libutils + from rope.refactor.rename import Rename + from rope.refactor.extract import ExtractMethod, ExtractVariable + import rope.base.project + import rope.base.taskhandle +except: + jsonMessage = {'error': True, 'message': 'Rope not installed', 'traceback': '', 'type': 'ModuleNotFoundError'} + sys.stderr.write(json.dumps(jsonMessage)) + sys.stderr.flush() WORKSPACE_ROOT = sys.argv[1] ROPE_PROJECT_FOLDER = '.vscode/.ropeproject' diff --git a/pythonFiles/rope/__init__.py b/pythonFiles/rope/__init__.py deleted file mode 100644 index 624b6279afcf..000000000000 --- a/pythonFiles/rope/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -"""rope, a python refactoring library""" - -INFO = __doc__ -VERSION = '0.10.3' -COPYRIGHT = """\ -Copyright (C) 2014-2015 Matej Cepl -Copyright (C) 2006-2012 Ali Gholami Rudi -Copyright (C) 2009-2012 Anton Gritsay - -This program is free software; you can redistribute it and/or modify it -under the terms of GNU General Public License as published by the -Free Software Foundation; either version 2 of the license, or (at your -opinion) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details.""" diff --git a/pythonFiles/rope/base/__init__.py b/pythonFiles/rope/base/__init__.py deleted file mode 100644 index ff5f8c63ae9d..000000000000 --- a/pythonFiles/rope/base/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Base rope package - -This package contains rope core modules that are used by other modules -and packages. - -""" - -__all__ = ['project', 'libutils', 'exceptions'] diff --git a/pythonFiles/rope/base/arguments.py b/pythonFiles/rope/base/arguments.py deleted file mode 100644 index 7ba43640663f..000000000000 --- a/pythonFiles/rope/base/arguments.py +++ /dev/null @@ -1,111 +0,0 @@ -import rope.base.evaluate -from rope.base import ast - - -class Arguments(object): - """A class for evaluating parameters passed to a function - - You can use the `create_arguments` factory. It handles implicit - first arguments. - - """ - - def __init__(self, args, scope): - self.args = args - self.scope = scope - self.instance = None - - def get_arguments(self, parameters): - result = [] - for pyname in self.get_pynames(parameters): - if pyname is None: - result.append(None) - else: - result.append(pyname.get_object()) - return result - - def get_pynames(self, parameters): - result = [None] * max(len(parameters), len(self.args)) - for index, arg in enumerate(self.args): - if isinstance(arg, ast.keyword) and arg.arg in parameters: - result[parameters.index(arg.arg)] = self._evaluate(arg.value) - else: - result[index] = self._evaluate(arg) - return result - - def get_instance_pyname(self): - if self.args: - return self._evaluate(self.args[0]) - - def _evaluate(self, ast_node): - return rope.base.evaluate.eval_node(self.scope, ast_node) - - -def create_arguments(primary, pyfunction, call_node, scope): - """A factory for creating `Arguments`""" - args = list(call_node.args) - args.extend(call_node.keywords) - called = call_node.func - # XXX: Handle constructors - if _is_method_call(primary, pyfunction) and \ - isinstance(called, ast.Attribute): - args.insert(0, called.value) - return Arguments(args, scope) - - -class ObjectArguments(object): - - def __init__(self, pynames): - self.pynames = pynames - - def get_arguments(self, parameters): - result = [] - for pyname in self.pynames: - if pyname is None: - result.append(None) - else: - result.append(pyname.get_object()) - return result - - def get_pynames(self, parameters): - return self.pynames - - def get_instance_pyname(self): - return self.pynames[0] - - -class MixedArguments(object): - - def __init__(self, pyname, arguments, scope): - """`argumens` is an instance of `Arguments`""" - self.pyname = pyname - self.args = arguments - - def get_pynames(self, parameters): - return [self.pyname] + self.args.get_pynames(parameters[1:]) - - def get_arguments(self, parameters): - result = [] - for pyname in self.get_pynames(parameters): - if pyname is None: - result.append(None) - else: - result.append(pyname.get_object()) - return result - - def get_instance_pyname(self): - return self.pyname - - -def _is_method_call(primary, pyfunction): - if primary is None: - return False - pyobject = primary.get_object() - if isinstance(pyobject.get_type(), rope.base.pyobjects.PyClass) and \ - isinstance(pyfunction, rope.base.pyobjects.PyFunction) and \ - isinstance(pyfunction.parent, rope.base.pyobjects.PyClass): - return True - if isinstance(pyobject.get_type(), rope.base.pyobjects.AbstractClass) and \ - isinstance(pyfunction, rope.base.builtins.BuiltinFunction): - return True - return False diff --git a/pythonFiles/rope/base/ast.py b/pythonFiles/rope/base/ast.py deleted file mode 100644 index d43c83c55bed..000000000000 --- a/pythonFiles/rope/base/ast.py +++ /dev/null @@ -1,76 +0,0 @@ -import _ast -from _ast import * - -from rope.base import fscommands - -try: - unicode -except NameError: - unicode = str - - -def parse(source, filename=''): - # NOTE: the raw string should be given to `compile` function - if isinstance(source, unicode): - source = fscommands.unicode_to_file_data(source) - if b'\r' in source: - source = source.replace(b'\r\n', b'\n').replace(b'\r', b'\n') - if not source.endswith(b'\n'): - source += b'\n' - try: - return compile(source, filename, 'exec', _ast.PyCF_ONLY_AST) - except (TypeError, ValueError) as e: - error = SyntaxError() - error.lineno = 1 - error.filename = filename - error.msg = str(e) - raise error - - -def walk(node, walker): - """Walk the syntax tree""" - method_name = '_' + node.__class__.__name__ - method = getattr(walker, method_name, None) - if method is not None: - if isinstance(node, _ast.ImportFrom) and node.module is None: - # In python < 2.7 ``node.module == ''`` for relative imports - # but for python 2.7 it is None. Generalizing it to ''. - node.module = '' - return method(node) - for child in get_child_nodes(node): - walk(child, walker) - - -def get_child_nodes(node): - if isinstance(node, _ast.Module): - return node.body - result = [] - if node._fields is not None: - for name in node._fields: - child = getattr(node, name) - if isinstance(child, list): - for entry in child: - if isinstance(entry, _ast.AST): - result.append(entry) - if isinstance(child, _ast.AST): - result.append(child) - return result - - -def call_for_nodes(node, callback, recursive=False): - """If callback returns `True` the child nodes are skipped""" - result = callback(node) - if recursive and not result: - for child in get_child_nodes(node): - call_for_nodes(child, callback, recursive) - - -def get_children(node): - result = [] - if node._fields is not None: - for name in node._fields: - if name in ['lineno', 'col_offset']: - continue - child = getattr(node, name) - result.append(child) - return result diff --git a/pythonFiles/rope/base/astutils.py b/pythonFiles/rope/base/astutils.py deleted file mode 100644 index 6c0b3d78e2e5..000000000000 --- a/pythonFiles/rope/base/astutils.py +++ /dev/null @@ -1,64 +0,0 @@ -from rope.base import ast - - -def get_name_levels(node): - """Return a list of ``(name, level)`` tuples for assigned names - - The `level` is `None` for simple assignments and is a list of - numbers for tuple assignments for example in:: - - a, (b, c) = x - - The levels for for `a` is ``[0]``, for `b` is ``[1, 0]`` and for - `c` is ``[1, 1]``. - - """ - visitor = _NodeNameCollector() - ast.walk(node, visitor) - return visitor.names - - -class _NodeNameCollector(object): - - def __init__(self, levels=None): - self.names = [] - self.levels = levels - self.index = 0 - - def _add_node(self, node): - new_levels = [] - if self.levels is not None: - new_levels = list(self.levels) - new_levels.append(self.index) - self.index += 1 - self._added(node, new_levels) - - def _added(self, node, levels): - if hasattr(node, 'id'): - self.names.append((node.id, levels)) - - def _Name(self, node): - self._add_node(node) - - def _ExceptHandler(self, node): - self.names.append((node.name, [])) - - def _Tuple(self, node): - new_levels = [] - if self.levels is not None: - new_levels = list(self.levels) - new_levels.append(self.index) - self.index += 1 - visitor = _NodeNameCollector(new_levels) - for child in ast.get_child_nodes(node): - ast.walk(child, visitor) - self.names.extend(visitor.names) - - def _Subscript(self, node): - self._add_node(node) - - def _Attribute(self, node): - self._add_node(node) - - def _Slice(self, node): - self._add_node(node) diff --git a/pythonFiles/rope/base/builtins.py b/pythonFiles/rope/base/builtins.py deleted file mode 100644 index bc42f7200f6e..000000000000 --- a/pythonFiles/rope/base/builtins.py +++ /dev/null @@ -1,809 +0,0 @@ -"""This module trys to support builtin types and functions.""" -import inspect -try: - raw_input -except NameError: - raw_input = input - -import rope.base.evaluate -from rope.base.utils import pycompat -from rope.base import pynames, pyobjects, arguments, utils - - -class BuiltinModule(pyobjects.AbstractModule): - - def __init__(self, name, pycore=None, initial={}): - super(BuiltinModule, self).__init__() - self.name = name - self.pycore = pycore - self.initial = initial - - parent = None - - def get_attributes(self): - return self.attributes - - def get_doc(self): - if self.module: - return self.module.__doc__ - - def get_name(self): - return self.name.split('.')[-1] - - @property - @utils.saveit - def attributes(self): - result = _object_attributes(self.module, self) - result.update(self.initial) - if self.pycore is not None: - submodules = self.pycore._builtin_submodules(self.name) - for name, module in submodules.items(): - result[name] = rope.base.builtins.BuiltinName(module) - return result - - @property - @utils.saveit - def module(self): - try: - result = __import__(self.name) - for token in self.name.split('.')[1:]: - result = getattr(result, token, None) - return result - except ImportError: - return - - -class _BuiltinElement(object): - - def __init__(self, builtin, parent=None): - self.builtin = builtin - self._parent = parent - - def get_doc(self): - if self.builtin: - return getattr(self.builtin, '__doc__', None) - - def get_name(self): - if self.builtin: - return getattr(self.builtin, '__name__', None) - - @property - def parent(self): - if self._parent is None: - return builtins - return self._parent - - -class BuiltinClass(_BuiltinElement, pyobjects.AbstractClass): - - def __init__(self, builtin, attributes, parent=None): - _BuiltinElement.__init__(self, builtin, parent) - pyobjects.AbstractClass.__init__(self) - self.initial = attributes - - @utils.saveit - def get_attributes(self): - result = _object_attributes(self.builtin, self) - result.update(self.initial) - return result - - -class BuiltinFunction(_BuiltinElement, pyobjects.AbstractFunction): - - def __init__(self, returned=None, function=None, builtin=None, - argnames=[], parent=None): - _BuiltinElement.__init__(self, builtin, parent) - pyobjects.AbstractFunction.__init__(self) - self.argnames = argnames - self.returned = returned - self.function = function - - def get_returned_object(self, args): - if self.function is not None: - return self.function(_CallContext(self.argnames, args)) - else: - return self.returned - - def get_param_names(self, special_args=True): - return self.argnames - - -class BuiltinUnknown(_BuiltinElement, pyobjects.PyObject): - - def __init__(self, builtin): - super(BuiltinUnknown, self).__init__(pyobjects.get_unknown()) - self.builtin = builtin - self.type = pyobjects.get_unknown() - - def get_name(self): - return getattr(type(self.builtin), '__name__', None) - - @utils.saveit - def get_attributes(self): - return _object_attributes(self.builtin, self) - - -def _object_attributes(obj, parent): - attributes = {} - for name in dir(obj): - if name == 'None': - continue - try: - child = getattr(obj, name) - except AttributeError: - # descriptors are allowed to raise AttributeError - # even if they are in dir() - continue - pyobject = None - if inspect.isclass(child): - pyobject = BuiltinClass(child, {}, parent=parent) - elif inspect.isroutine(child): - pyobject = BuiltinFunction(builtin=child, parent=parent) - else: - pyobject = BuiltinUnknown(builtin=child) - attributes[name] = BuiltinName(pyobject) - return attributes - - -def _create_builtin_type_getter(cls): - def _get_builtin(*args): - if not hasattr(cls, '_generated'): - cls._generated = {} - if args not in cls._generated: - cls._generated[args] = cls(*args) - return cls._generated[args] - return _get_builtin - - -def _create_builtin_getter(cls): - type_getter = _create_builtin_type_getter(cls) - - def _get_builtin(*args): - return pyobjects.PyObject(type_getter(*args)) - return _get_builtin - - -class _CallContext(object): - - def __init__(self, argnames, args): - self.argnames = argnames - self.args = args - - def _get_scope_and_pyname(self, pyname): - if pyname is not None and isinstance(pyname, pynames.AssignedName): - pymodule, lineno = pyname.get_definition_location() - if pymodule is None: - return None, None - if lineno is None: - lineno = 1 - scope = pymodule.get_scope().get_inner_scope_for_line(lineno) - name = None - while name is None and scope is not None: - for current in scope.get_names(): - if scope[current] is pyname: - name = current - break - else: - scope = scope.parent - return scope, name - return None, None - - def get_argument(self, name): - if self.args: - args = self.args.get_arguments(self.argnames) - return args[self.argnames.index(name)] - - def get_pyname(self, name): - if self.args: - args = self.args.get_pynames(self.argnames) - if name in self.argnames: - return args[self.argnames.index(name)] - - def get_arguments(self, argnames): - if self.args: - return self.args.get_arguments(argnames) - - def get_pynames(self, argnames): - if self.args: - return self.args.get_pynames(argnames) - - def get_per_name(self): - if self.args is None: - return None - pyname = self.args.get_instance_pyname() - scope, name = self._get_scope_and_pyname(pyname) - if name is not None: - pymodule = pyname.get_definition_location()[0] - return pymodule.pycore.object_info.get_per_name(scope, name) - return None - - def save_per_name(self, value): - if self.args is None: - return None - pyname = self.args.get_instance_pyname() - scope, name = self._get_scope_and_pyname(pyname) - if name is not None: - pymodule = pyname.get_definition_location()[0] - pymodule.pycore.object_info.save_per_name(scope, name, value) - - -class _AttributeCollector(object): - - def __init__(self, type): - self.attributes = {} - self.type = type - - def __call__(self, name, returned=None, function=None, - argnames=['self'], check_existence=True): - try: - builtin = getattr(self.type, name) - except AttributeError: - if check_existence: - raise - builtin = None - self.attributes[name] = BuiltinName( - BuiltinFunction(returned=returned, function=function, - argnames=argnames, builtin=builtin)) - - def __setitem__(self, name, value): - self.attributes[name] = value - - -class List(BuiltinClass): - - def __init__(self, holding=None): - self.holding = holding - collector = _AttributeCollector(list) - - collector('__iter__', function=self._iterator_get) - collector('__new__', function=self._new_list) - - # Adding methods - collector('append', function=self._list_add, - argnames=['self', 'value']) - collector('__setitem__', function=self._list_add, - argnames=['self', 'index', 'value']) - collector('insert', function=self._list_add, - argnames=['self', 'index', 'value']) - collector('extend', function=self._self_set, - argnames=['self', 'iterable']) - - # Getting methods - collector('__getitem__', function=self._list_get) - collector('pop', function=self._list_get) - try: - collector('__getslice__', function=self._list_get) - except AttributeError: - pass - - super(List, self).__init__(list, collector.attributes) - - def _new_list(self, args): - return _create_builtin(args, get_list) - - def _list_add(self, context): - if self.holding is not None: - return - holding = context.get_argument('value') - if holding is not None and holding != pyobjects.get_unknown(): - context.save_per_name(holding) - - def _self_set(self, context): - if self.holding is not None: - return - iterable = context.get_pyname('iterable') - holding = _infer_sequence_for_pyname(iterable) - if holding is not None and holding != pyobjects.get_unknown(): - context.save_per_name(holding) - - def _list_get(self, context): - if self.holding is not None: - args = context.get_arguments(['self', 'key']) - if (len(args) > 1 and args[1] is not None and - args[1].get_type() == builtins['slice'].get_object()): - return get_list(self.holding) - return self.holding - return context.get_per_name() - - def _iterator_get(self, context): - return get_iterator(self._list_get(context)) - - def _self_get(self, context): - return get_list(self._list_get(context)) - - -get_list = _create_builtin_getter(List) -get_list_type = _create_builtin_type_getter(List) - - -class Dict(BuiltinClass): - - def __init__(self, keys=None, values=None): - self.keys = keys - self.values = values - collector = _AttributeCollector(dict) - collector('__new__', function=self._new_dict) - collector('__setitem__', function=self._dict_add) - collector('popitem', function=self._item_get) - collector('pop', function=self._value_get) - collector('get', function=self._key_get) - collector('keys', function=self._key_list) - collector('values', function=self._value_list) - collector('items', function=self._item_list) - collector('copy', function=self._self_get) - collector('__getitem__', function=self._value_get) - collector('__iter__', function=self._key_iter) - collector('update', function=self._self_set) - super(Dict, self).__init__(dict, collector.attributes) - - def _new_dict(self, args): - def do_create(holding=None): - if holding is None: - return get_dict() - type = holding.get_type() - if isinstance(type, Tuple) and \ - len(type.get_holding_objects()) == 2: - return get_dict(*type.get_holding_objects()) - return _create_builtin(args, do_create) - - def _dict_add(self, context): - if self.keys is not None: - return - key, value = context.get_arguments(['self', 'key', 'value'])[1:] - if key is not None and key != pyobjects.get_unknown(): - context.save_per_name(get_tuple(key, value)) - - def _item_get(self, context): - if self.keys is not None: - return get_tuple(self.keys, self.values) - item = context.get_per_name() - if item is None or not isinstance(item.get_type(), Tuple): - return get_tuple(self.keys, self.values) - return item - - def _value_get(self, context): - item = self._item_get(context).get_type() - return item.get_holding_objects()[1] - - def _key_get(self, context): - item = self._item_get(context).get_type() - return item.get_holding_objects()[0] - - def _value_list(self, context): - return get_list(self._value_get(context)) - - def _key_list(self, context): - return get_list(self._key_get(context)) - - def _item_list(self, context): - return get_list(self._item_get(context)) - - def _value_iter(self, context): - return get_iterator(self._value_get(context)) - - def _key_iter(self, context): - return get_iterator(self._key_get(context)) - - def _item_iter(self, context): - return get_iterator(self._item_get(context)) - - def _self_get(self, context): - item = self._item_get(context).get_type() - key, value = item.get_holding_objects()[:2] - return get_dict(key, value) - - def _self_set(self, context): - if self.keys is not None: - return - new_dict = context.get_pynames(['self', 'd'])[1] - if new_dict and isinstance(new_dict.get_object().get_type(), Dict): - args = arguments.ObjectArguments([new_dict]) - items = new_dict.get_object()['popitem'].\ - get_object().get_returned_object(args) - context.save_per_name(items) - else: - holding = _infer_sequence_for_pyname(new_dict) - if holding is not None and isinstance(holding.get_type(), Tuple): - context.save_per_name(holding) - - -get_dict = _create_builtin_getter(Dict) -get_dict_type = _create_builtin_type_getter(Dict) - - -class Tuple(BuiltinClass): - - def __init__(self, *objects): - self.objects = objects - first = None - if objects: - first = objects[0] - attributes = { - '__getitem__': BuiltinName(BuiltinFunction(first)), # TODO: add slice support - '__getslice__': - BuiltinName(BuiltinFunction(pyobjects.PyObject(self))), - '__new__': BuiltinName(BuiltinFunction(function=self._new_tuple)), - '__iter__': BuiltinName(BuiltinFunction(get_iterator(first)))} - super(Tuple, self).__init__(tuple, attributes) - - def get_holding_objects(self): - return self.objects - - def _new_tuple(self, args): - return _create_builtin(args, get_tuple) - - -get_tuple = _create_builtin_getter(Tuple) -get_tuple_type = _create_builtin_type_getter(Tuple) - - -class Set(BuiltinClass): - - def __init__(self, holding=None): - self.holding = holding - collector = _AttributeCollector(set) - collector('__new__', function=self._new_set) - - self_methods = ['copy', 'difference', 'intersection', - 'symmetric_difference', 'union'] - for method in self_methods: - collector(method, function=self._self_get) - collector('add', function=self._set_add) - collector('update', function=self._self_set) - collector('update', function=self._self_set) - collector('symmetric_difference_update', function=self._self_set) - collector('difference_update', function=self._self_set) - - collector('pop', function=self._set_get) - collector('__iter__', function=self._iterator_get) - super(Set, self).__init__(set, collector.attributes) - - def _new_set(self, args): - return _create_builtin(args, get_set) - - def _set_add(self, context): - if self.holding is not None: - return - holding = context.get_arguments(['self', 'value'])[1] - if holding is not None and holding != pyobjects.get_unknown(): - context.save_per_name(holding) - - def _self_set(self, context): - if self.holding is not None: - return - iterable = context.get_pyname('iterable') - holding = _infer_sequence_for_pyname(iterable) - if holding is not None and holding != pyobjects.get_unknown(): - context.save_per_name(holding) - - def _set_get(self, context): - if self.holding is not None: - return self.holding - return context.get_per_name() - - def _iterator_get(self, context): - return get_iterator(self._set_get(context)) - - def _self_get(self, context): - return get_list(self._set_get(context)) - - -get_set = _create_builtin_getter(Set) -get_set_type = _create_builtin_type_getter(Set) - - -class Str(BuiltinClass): - - def __init__(self): - self_object = pyobjects.PyObject(self) - collector = _AttributeCollector(str) - collector('__iter__', get_iterator(self_object), check_existence=False) - - self_methods = ['__getitem__', 'capitalize', 'center', - 'encode', 'expandtabs', 'join', 'ljust', - 'lower', 'lstrip', 'replace', 'rjust', 'rstrip', - 'strip', 'swapcase', 'title', 'translate', 'upper', - 'zfill'] - for method in self_methods: - collector(method, self_object) - - py2_self_methods = ["__getslice__", "decode"] - for method in py2_self_methods: - try: - collector(method, self_object) - except AttributeError: - pass - - for method in ['rsplit', 'split', 'splitlines']: - collector(method, get_list(self_object)) - - super(Str, self).__init__(str, collector.attributes) - - def get_doc(self): - return str.__doc__ - - -get_str = _create_builtin_getter(Str) -get_str_type = _create_builtin_type_getter(Str) - - -class BuiltinName(pynames.PyName): - - def __init__(self, pyobject): - self.pyobject = pyobject - - def get_object(self): - return self.pyobject - - def get_definition_location(self): - return (None, None) - - -class Iterator(pyobjects.AbstractClass): - - def __init__(self, holding=None): - super(Iterator, self).__init__() - self.holding = holding - self.attributes = { - 'next': BuiltinName(BuiltinFunction(self.holding)), - '__iter__': BuiltinName(BuiltinFunction(self))} - - def get_attributes(self): - return self.attributes - - def get_returned_object(self, args): - return self.holding - -get_iterator = _create_builtin_getter(Iterator) - - -class Generator(pyobjects.AbstractClass): - - def __init__(self, holding=None): - super(Generator, self).__init__() - self.holding = holding - self.attributes = { - 'next': BuiltinName(BuiltinFunction(self.holding)), - '__iter__': BuiltinName(BuiltinFunction( - get_iterator(self.holding))), - 'close': BuiltinName(BuiltinFunction()), - 'send': BuiltinName(BuiltinFunction()), - 'throw': BuiltinName(BuiltinFunction())} - - def get_attributes(self): - return self.attributes - - def get_returned_object(self, args): - return self.holding - -get_generator = _create_builtin_getter(Generator) - - -class File(BuiltinClass): - - def __init__(self): - str_object = get_str() - str_list = get_list(get_str()) - attributes = {} - - def add(name, returned=None, function=None): - builtin = getattr(open, name, None) - attributes[name] = BuiltinName( - BuiltinFunction(returned=returned, function=function, - builtin=builtin)) - add('__iter__', get_iterator(str_object)) - for method in ['next', 'read', 'readline', 'readlines']: - add(method, str_list) - for method in ['close', 'flush', 'lineno', 'isatty', 'seek', 'tell', - 'truncate', 'write', 'writelines']: - add(method) - super(File, self).__init__(open, attributes) - - -get_file = _create_builtin_getter(File) -get_file_type = _create_builtin_type_getter(File) - - -class Property(BuiltinClass): - - def __init__(self, fget=None, fset=None, fdel=None, fdoc=None): - self._fget = fget - self._fdoc = fdoc - attributes = { - 'fget': BuiltinName(BuiltinFunction()), - 'fset': BuiltinName(pynames.UnboundName()), - 'fdel': BuiltinName(pynames.UnboundName()), - '__new__': BuiltinName( - BuiltinFunction(function=_property_function))} - super(Property, self).__init__(property, attributes) - - def get_property_object(self, args): - if isinstance(self._fget, pyobjects.AbstractFunction): - return self._fget.get_returned_object(args) - - -def _property_function(args): - parameters = args.get_arguments(['fget', 'fset', 'fdel', 'fdoc']) - return pyobjects.PyObject(Property(parameters[0])) - - -class Lambda(pyobjects.AbstractFunction): - - def __init__(self, node, scope): - super(Lambda, self).__init__() - self.node = node - self.arguments = node.args - self.scope = scope - - def get_returned_object(self, args): - result = rope.base.evaluate.eval_node(self.scope, self.node.body) - if result is not None: - return result.get_object() - else: - return pyobjects.get_unknown() - - def get_module(self): - return self.parent.get_module() - - def get_scope(self): - return self.scope - - def get_kind(self): - return 'lambda' - - def get_ast(self): - return self.node - - def get_attributes(self): - return {} - - def get_name(self): - return 'lambda' - - def get_param_names(self, special_args=True): - result = [pycompat.get_ast_arg_arg(node) for node in self.arguments.args - if isinstance(node, pycompat.ast_arg_type)] - if self.arguments.vararg: - result.append('*' + pycompat.get_ast_arg_arg(self.arguments.vararg)) - if self.arguments.kwarg: - result.append('**' + pycompat.get_ast_arg_arg(self.arguments.kwarg)) - return result - - @property - def parent(self): - return self.scope.pyobject - - -class BuiltinObject(BuiltinClass): - - def __init__(self): - super(BuiltinObject, self).__init__(object, {}) - - -class BuiltinType(BuiltinClass): - - def __init__(self): - super(BuiltinType, self).__init__(type, {}) - - -def _infer_sequence_for_pyname(pyname): - if pyname is None: - return None - seq = pyname.get_object() - args = arguments.ObjectArguments([pyname]) - if '__iter__' in seq: - obj = seq['__iter__'].get_object() - if not isinstance(obj, pyobjects.AbstractFunction): - return None - iter = obj.get_returned_object(args) - if iter is not None and 'next' in iter: - holding = iter['next'].get_object().\ - get_returned_object(args) - return holding - - -def _create_builtin(args, creator): - passed = args.get_pynames(['sequence'])[0] - if passed is None: - holding = None - else: - holding = _infer_sequence_for_pyname(passed) - if holding is not None: - return creator(holding) - else: - return creator() - - -def _range_function(args): - return get_list() - - -def _reversed_function(args): - return _create_builtin(args, get_iterator) - - -def _sorted_function(args): - return _create_builtin(args, get_list) - - -def _super_function(args): - passed_class, passed_self = args.get_arguments(['type', 'self']) - if passed_self is None: - return passed_class - else: - #pyclass = passed_self.get_type() - pyclass = passed_class - if isinstance(pyclass, pyobjects.AbstractClass): - supers = pyclass.get_superclasses() - if supers: - return pyobjects.PyObject(supers[0]) - return passed_self - - -def _zip_function(args): - args = args.get_pynames(['sequence']) - objects = [] - for seq in args: - if seq is None: - holding = None - else: - holding = _infer_sequence_for_pyname(seq) - objects.append(holding) - tuple = get_tuple(*objects) - return get_list(tuple) - - -def _enumerate_function(args): - passed = args.get_pynames(['sequence'])[0] - if passed is None: - holding = None - else: - holding = _infer_sequence_for_pyname(passed) - tuple = get_tuple(None, holding) - return get_iterator(tuple) - - -def _iter_function(args): - passed = args.get_pynames(['sequence'])[0] - if passed is None: - holding = None - else: - holding = _infer_sequence_for_pyname(passed) - return get_iterator(holding) - - -def _input_function(args): - return get_str() - - -_initial_builtins = { - 'list': BuiltinName(get_list_type()), - 'dict': BuiltinName(get_dict_type()), - 'tuple': BuiltinName(get_tuple_type()), - 'set': BuiltinName(get_set_type()), - 'str': BuiltinName(get_str_type()), - 'file': BuiltinName(get_file_type()), - 'open': BuiltinName(get_file_type()), - 'unicode': BuiltinName(get_str_type()), - 'range': BuiltinName(BuiltinFunction(function=_range_function, - builtin=range)), - 'reversed': BuiltinName(BuiltinFunction(function=_reversed_function, - builtin=reversed)), - 'sorted': BuiltinName(BuiltinFunction(function=_sorted_function, - builtin=sorted)), - 'super': BuiltinName(BuiltinFunction(function=_super_function, - builtin=super)), - 'property': BuiltinName(BuiltinFunction(function=_property_function, - builtin=property)), - 'zip': BuiltinName(BuiltinFunction(function=_zip_function, builtin=zip)), - 'enumerate': BuiltinName(BuiltinFunction(function=_enumerate_function, - builtin=enumerate)), - 'object': BuiltinName(BuiltinObject()), - 'type': BuiltinName(BuiltinType()), - 'iter': BuiltinName(BuiltinFunction(function=_iter_function, - builtin=iter)), - 'raw_input': BuiltinName(BuiltinFunction(function=_input_function, - builtin=raw_input)), -} - -builtins = BuiltinModule(pycompat.builtins.__name__, initial=_initial_builtins) diff --git a/pythonFiles/rope/base/change.py b/pythonFiles/rope/base/change.py deleted file mode 100644 index fe2ebf435adb..000000000000 --- a/pythonFiles/rope/base/change.py +++ /dev/null @@ -1,450 +0,0 @@ -import datetime -import difflib -import os -import time - -import rope.base.fscommands -from rope.base import taskhandle, exceptions, utils - - -class Change(object): - """The base class for changes - - Rope refactorings return `Change` objects. They can be previewed, - committed or undone. - """ - - def do(self, job_set=None): - """Perform the change - - .. note:: Do use this directly. Use `Project.do()` instead. - """ - - def undo(self, job_set=None): - """Perform the change - - .. note:: Do use this directly. Use `History.undo()` instead. - """ - - def get_description(self): - """Return the description of this change - - This can be used for previewing the changes. - """ - return str(self) - - def get_changed_resources(self): - """Return the list of resources that will be changed""" - return [] - - @property - @utils.saveit - def _operations(self): - return _ResourceOperations(self.resource.project) - - -class ChangeSet(Change): - """A collection of `Change` objects - - This class holds a collection of changes. This class provides - these fields: - - * `changes`: the list of changes - * `description`: the goal of these changes - """ - - def __init__(self, description, timestamp=None): - self.changes = [] - self.description = description - self.time = timestamp - - def do(self, job_set=taskhandle.NullJobSet()): - try: - done = [] - for change in self.changes: - change.do(job_set) - done.append(change) - self.time = time.time() - except Exception: - for change in done: - change.undo() - raise - - def undo(self, job_set=taskhandle.NullJobSet()): - try: - done = [] - for change in reversed(self.changes): - change.undo(job_set) - done.append(change) - except Exception: - for change in done: - change.do() - raise - - def add_change(self, change): - self.changes.append(change) - - def get_description(self): - result = [str(self) + ':\n\n\n'] - for change in self.changes: - result.append(change.get_description()) - result.append('\n') - return ''.join(result) - - def __str__(self): - if self.time is not None: - date = datetime.datetime.fromtimestamp(self.time) - if date.date() == datetime.date.today(): - string_date = 'today' - elif date.date() == (datetime.date.today() - - datetime.timedelta(1)): - string_date = 'yesterday' - elif date.year == datetime.date.today().year: - string_date = date.strftime('%b %d') - else: - string_date = date.strftime('%d %b, %Y') - string_time = date.strftime('%H:%M:%S') - string_time = '%s %s ' % (string_date, string_time) - return self.description + ' - ' + string_time - return self.description - - def get_changed_resources(self): - result = set() - for change in self.changes: - result.update(change.get_changed_resources()) - return result - - -def _handle_job_set(function): - """A decorator for handling `taskhandle.JobSet`\s - - A decorator for handling `taskhandle.JobSet`\s for `do` and `undo` - methods of `Change`\s. - """ - def call(self, job_set=taskhandle.NullJobSet()): - job_set.started_job(str(self)) - function(self) - job_set.finished_job() - return call - - -class ChangeContents(Change): - """A class to change the contents of a file - - Fields: - - * `resource`: The `rope.base.resources.File` to change - * `new_contents`: What to write in the file - """ - - def __init__(self, resource, new_contents, old_contents=None): - self.resource = resource - # IDEA: Only saving diffs; possible problems when undo/redoing - self.new_contents = new_contents - self.old_contents = old_contents - - @_handle_job_set - def do(self): - if self.old_contents is None: - self.old_contents = self.resource.read() - self._operations.write_file(self.resource, self.new_contents) - - @_handle_job_set - def undo(self): - if self.old_contents is None: - raise exceptions.HistoryError( - 'Undoing a change that is not performed yet!') - self._operations.write_file(self.resource, self.old_contents) - - def __str__(self): - return 'Change <%s>' % self.resource.path - - def get_description(self): - new = self.new_contents - old = self.old_contents - if old is None: - if self.resource.exists(): - old = self.resource.read() - else: - old = '' - result = difflib.unified_diff( - old.splitlines(True), new.splitlines(True), - 'a/' + self.resource.path, 'b/' + self.resource.path) - return ''.join(list(result)) - - def get_changed_resources(self): - return [self.resource] - - -class MoveResource(Change): - """Move a resource to a new location - - Fields: - - * `resource`: The `rope.base.resources.Resource` to move - * `new_resource`: The destination for move; It is the moved - resource not the folder containing that resource. - """ - - def __init__(self, resource, new_location, exact=False): - self.project = resource.project - self.resource = resource - if not exact: - new_location = _get_destination_for_move(resource, new_location) - if resource.is_folder(): - self.new_resource = self.project.get_folder(new_location) - else: - self.new_resource = self.project.get_file(new_location) - - @_handle_job_set - def do(self): - self._operations.move(self.resource, self.new_resource) - - @_handle_job_set - def undo(self): - self._operations.move(self.new_resource, self.resource) - - def __str__(self): - return 'Move <%s>' % self.resource.path - - def get_description(self): - return 'rename from %s\nrename to %s' % (self.resource.path, - self.new_resource.path) - - def get_changed_resources(self): - return [self.resource, self.new_resource] - - -class CreateResource(Change): - """A class to create a resource - - Fields: - - * `resource`: The resource to create - """ - - def __init__(self, resource): - self.resource = resource - - @_handle_job_set - def do(self): - self._operations.create(self.resource) - - @_handle_job_set - def undo(self): - self._operations.remove(self.resource) - - def __str__(self): - return 'Create Resource <%s>' % (self.resource.path) - - def get_description(self): - return 'new file %s' % (self.resource.path) - - def get_changed_resources(self): - return [self.resource] - - def _get_child_path(self, parent, name): - if parent.path == '': - return name - else: - return parent.path + '/' + name - - -class CreateFolder(CreateResource): - """A class to create a folder - - See docs for `CreateResource`. - """ - - def __init__(self, parent, name): - resource = parent.project.get_folder( - self._get_child_path(parent, name)) - super(CreateFolder, self).__init__(resource) - - -class CreateFile(CreateResource): - """A class to create a file - - See docs for `CreateResource`. - """ - - def __init__(self, parent, name): - resource = parent.project.get_file(self._get_child_path(parent, name)) - super(CreateFile, self).__init__(resource) - - -class RemoveResource(Change): - """A class to remove a resource - - Fields: - - * `resource`: The resource to be removed - """ - - def __init__(self, resource): - self.resource = resource - - @_handle_job_set - def do(self): - self._operations.remove(self.resource) - - # TODO: Undoing remove operations - @_handle_job_set - def undo(self): - raise NotImplementedError( - 'Undoing `RemoveResource` is not implemented yet.') - - def __str__(self): - return 'Remove <%s>' % (self.resource.path) - - def get_changed_resources(self): - return [self.resource] - - -def count_changes(change): - """Counts the number of basic changes a `Change` will make""" - if isinstance(change, ChangeSet): - result = 0 - for child in change.changes: - result += count_changes(child) - return result - return 1 - - -def create_job_set(task_handle, change): - return task_handle.create_jobset(str(change), count_changes(change)) - - -class _ResourceOperations(object): - - def __init__(self, project): - self.project = project - self.fscommands = project.fscommands - self.direct_commands = rope.base.fscommands.FileSystemCommands() - - def _get_fscommands(self, resource): - if self.project.is_ignored(resource): - return self.direct_commands - return self.fscommands - - def write_file(self, resource, contents): - data = rope.base.fscommands.unicode_to_file_data(contents) - fscommands = self._get_fscommands(resource) - fscommands.write(resource.real_path, data) - for observer in list(self.project.observers): - observer.resource_changed(resource) - - def move(self, resource, new_resource): - fscommands = self._get_fscommands(resource) - fscommands.move(resource.real_path, new_resource.real_path) - for observer in list(self.project.observers): - observer.resource_moved(resource, new_resource) - - def create(self, resource): - if resource.is_folder(): - self._create_resource(resource.path, kind='folder') - else: - self._create_resource(resource.path) - for observer in list(self.project.observers): - observer.resource_created(resource) - - def remove(self, resource): - fscommands = self._get_fscommands(resource) - fscommands.remove(resource.real_path) - for observer in list(self.project.observers): - observer.resource_removed(resource) - - def _create_resource(self, file_name, kind='file'): - resource_path = self.project._get_resource_path(file_name) - if os.path.exists(resource_path): - raise exceptions.RopeError('Resource <%s> already exists' - % resource_path) - resource = self.project.get_file(file_name) - if not resource.parent.exists(): - raise exceptions.ResourceNotFoundError( - 'Parent folder of <%s> does not exist' % resource.path) - fscommands = self._get_fscommands(resource) - try: - if kind == 'file': - fscommands.create_file(resource_path) - else: - fscommands.create_folder(resource_path) - except IOError as e: - raise exceptions.RopeError(e) - - -def _get_destination_for_move(resource, destination): - dest_path = resource.project._get_resource_path(destination) - if os.path.isdir(dest_path): - if destination != '': - return destination + '/' + resource.name - else: - return resource.name - return destination - - -class ChangeToData(object): - - def convertChangeSet(self, change): - description = change.description - changes = [] - for child in change.changes: - changes.append(self(child)) - return (description, changes, change.time) - - def convertChangeContents(self, change): - return (change.resource.path, change.new_contents, change.old_contents) - - def convertMoveResource(self, change): - return (change.resource.path, change.new_resource.path) - - def convertCreateResource(self, change): - return (change.resource.path, change.resource.is_folder()) - - def convertRemoveResource(self, change): - return (change.resource.path, change.resource.is_folder()) - - def __call__(self, change): - change_type = type(change) - if change_type in (CreateFolder, CreateFile): - change_type = CreateResource - method = getattr(self, 'convert' + change_type.__name__) - return (change_type.__name__, method(change)) - - -class DataToChange(object): - - def __init__(self, project): - self.project = project - - def makeChangeSet(self, description, changes, time=None): - result = ChangeSet(description, time) - for child in changes: - result.add_change(self(child)) - return result - - def makeChangeContents(self, path, new_contents, old_contents): - resource = self.project.get_file(path) - return ChangeContents(resource, new_contents, old_contents) - - def makeMoveResource(self, old_path, new_path): - resource = self.project.get_file(old_path) - return MoveResource(resource, new_path, exact=True) - - def makeCreateResource(self, path, is_folder): - if is_folder: - resource = self.project.get_folder(path) - else: - resource = self.project.get_file(path) - return CreateResource(resource) - - def makeRemoveResource(self, path, is_folder): - if is_folder: - resource = self.project.get_folder(path) - else: - resource = self.project.get_file(path) - return RemoveResource(resource) - - def __call__(self, data): - method = getattr(self, 'make' + data[0]) - return method(*data[1]) diff --git a/pythonFiles/rope/base/codeanalyze.py b/pythonFiles/rope/base/codeanalyze.py deleted file mode 100644 index 1704e9ade971..000000000000 --- a/pythonFiles/rope/base/codeanalyze.py +++ /dev/null @@ -1,362 +0,0 @@ -import bisect -import re -import token -import tokenize - - -class ChangeCollector(object): - - def __init__(self, text): - self.text = text - self.changes = [] - - def add_change(self, start, end, new_text=None): - if new_text is None: - new_text = self.text[start:end] - self.changes.append((start, end, new_text)) - - def get_changed(self): - if not self.changes: - return None - - self.changes.sort(key=lambda x: x[:2]) - pieces = [] - last_changed = 0 - for change in self.changes: - start, end, text = change - pieces.append(self.text[last_changed:start] + text) - last_changed = end - if last_changed < len(self.text): - pieces.append(self.text[last_changed:]) - result = ''.join(pieces) - if result != self.text: - return result - - -class SourceLinesAdapter(object): - """Adapts source to Lines interface - - Note: The creation of this class is expensive. - """ - - def __init__(self, source_code): - self.code = source_code - self.starts = None - self._initialize_line_starts() - - def _initialize_line_starts(self): - self.starts = [] - self.starts.append(0) - try: - i = 0 - while True: - i = self.code.index('\n', i) + 1 - self.starts.append(i) - except ValueError: - pass - self.starts.append(len(self.code) + 1) - - def get_line(self, lineno): - return self.code[self.starts[lineno - 1]: - self.starts[lineno] - 1] - - def length(self): - return len(self.starts) - 1 - - def get_line_number(self, offset): - return bisect.bisect(self.starts, offset) - - def get_line_start(self, lineno): - return self.starts[lineno - 1] - - def get_line_end(self, lineno): - return self.starts[lineno] - 1 - - -class ArrayLinesAdapter(object): - - def __init__(self, lines): - self.lines = lines - - def get_line(self, line_number): - return self.lines[line_number - 1] - - def length(self): - return len(self.lines) - - -class LinesToReadline(object): - - def __init__(self, lines, start): - self.lines = lines - self.current = start - - def readline(self): - if self.current <= self.lines.length(): - self.current += 1 - return self.lines.get_line(self.current - 1) + '\n' - return '' - - def __call__(self): - return self.readline() - - -class _CustomGenerator(object): - - def __init__(self, lines): - self.lines = lines - self.in_string = '' - self.open_count = 0 - self.continuation = False - - def __call__(self): - size = self.lines.length() - result = [] - i = 1 - while i <= size: - while i <= size and not self.lines.get_line(i).strip(): - i += 1 - if i <= size: - start = i - while True: - line = self.lines.get_line(i) - self._analyze_line(line) - if not (self.continuation or self.open_count or - self.in_string) or i == size: - break - i += 1 - result.append((start, i)) - i += 1 - return result - - # Matches all backslashes before the token, to detect escaped quotes - _main_tokens = re.compile(r'(\\*)((\'\'\'|"""|\'|")|#|\[|\]|\{|\}|\(|\))') - - def _analyze_line(self, line): - token = None - for match in self._main_tokens.finditer(line): - prefix = match.group(1) - token = match.group(2) - # Skip any tokens which are escaped - if len(prefix) % 2 == 1: - continue - if token in ["'''", '"""', "'", '"']: - if not self.in_string: - self.in_string = token - elif self.in_string == token: - self.in_string = '' - if self.in_string: - continue - if token == '#': - break - if token in '([{': - self.open_count += 1 - elif token in ')]}': - self.open_count -= 1 - if line and token != '#' and line.endswith('\\'): - self.continuation = True - else: - self.continuation = False - - -def custom_generator(lines): - return _CustomGenerator(lines)() - - -class LogicalLineFinder(object): - - def __init__(self, lines): - self.lines = lines - - def logical_line_in(self, line_number): - indents = count_line_indents(self.lines.get_line(line_number)) - tries = 0 - while True: - block_start = get_block_start(self.lines, line_number, indents) - try: - return self._block_logical_line(block_start, line_number) - except IndentationError as e: - tries += 1 - if tries == 5: - raise e - lineno = e.lineno + block_start - 1 - indents = count_line_indents(self.lines.get_line(lineno)) - - def generate_starts(self, start_line=1, end_line=None): - for start, end in self.generate_regions(start_line, end_line): - yield start - - def generate_regions(self, start_line=1, end_line=None): - # XXX: `block_start` should be at a better position! - block_start = 1 - readline = LinesToReadline(self.lines, block_start) - try: - for start, end in self._logical_lines(readline): - real_start = start + block_start - 1 - real_start = self._first_non_blank(real_start) - if end_line is not None and real_start >= end_line: - break - real_end = end + block_start - 1 - if real_start >= start_line: - yield (real_start, real_end) - except tokenize.TokenError: - pass - - def _block_logical_line(self, block_start, line_number): - readline = LinesToReadline(self.lines, block_start) - shifted = line_number - block_start + 1 - region = self._calculate_logical(readline, shifted) - start = self._first_non_blank(region[0] + block_start - 1) - if region[1] is None: - end = self.lines.length() - else: - end = region[1] + block_start - 1 - return start, end - - def _calculate_logical(self, readline, line_number): - last_end = 1 - try: - for start, end in self._logical_lines(readline): - if line_number <= end: - return (start, end) - last_end = end + 1 - except tokenize.TokenError as e: - current = e.args[1][0] - return (last_end, max(last_end, current - 1)) - return (last_end, None) - - def _logical_lines(self, readline): - last_end = 1 - for current_token in tokenize.generate_tokens(readline): - current = current_token[2][0] - if current_token[0] == token.NEWLINE: - yield (last_end, current) - last_end = current + 1 - - def _first_non_blank(self, line_number): - current = line_number - while current < self.lines.length(): - line = self.lines.get_line(current).strip() - if line and not line.startswith('#'): - return current - current += 1 - return current - - -def tokenizer_generator(lines): - return LogicalLineFinder(lines).generate_regions() - - -class CachingLogicalLineFinder(object): - - def __init__(self, lines, generate=custom_generator): - self.lines = lines - self._generate = generate - - _starts = None - - @property - def starts(self): - if self._starts is None: - self._init_logicals() - return self._starts - - _ends = None - - @property - def ends(self): - if self._ends is None: - self._init_logicals() - return self._ends - - def _init_logicals(self): - """Should initialize _starts and _ends attributes""" - size = self.lines.length() + 1 - self._starts = [None] * size - self._ends = [None] * size - for start, end in self._generate(self.lines): - self._starts[start] = True - self._ends[end] = True - - def logical_line_in(self, line_number): - start = line_number - while start > 0 and not self.starts[start]: - start -= 1 - if start == 0: - try: - start = self.starts.index(True, line_number) - except ValueError: - return (line_number, line_number) - return (start, self.ends.index(True, start)) - - def generate_starts(self, start_line=1, end_line=None): - if end_line is None: - end_line = self.lines.length() - for index in range(start_line, end_line): - if self.starts[index]: - yield index - - -def get_block_start(lines, lineno, maximum_indents=80): - """Approximate block start""" - pattern = get_block_start_patterns() - for i in range(lineno, 0, -1): - match = pattern.search(lines.get_line(i)) - if match is not None and \ - count_line_indents(lines.get_line(i)) <= maximum_indents: - striped = match.string.lstrip() - # Maybe we're in a list comprehension or generator expression - if i > 1 and striped.startswith('if') or striped.startswith('for'): - bracs = 0 - for j in range(i, min(i + 5, lines.length() + 1)): - for c in lines.get_line(j): - if c == '#': - break - if c in '[(': - bracs += 1 - if c in ')]': - bracs -= 1 - if bracs < 0: - break - if bracs < 0: - break - if bracs < 0: - continue - return i - return 1 - - -_block_start_pattern = None - - -def get_block_start_patterns(): - global _block_start_pattern - if not _block_start_pattern: - pattern = '^\\s*(((def|class|if|elif|except|for|while|with)\\s)|'\ - '((try|else|finally|except)\\s*:))' - _block_start_pattern = re.compile(pattern, re.M) - return _block_start_pattern - - -def count_line_indents(line): - indents = 0 - for char in line: - if char == ' ': - indents += 1 - elif char == '\t': - indents += 8 - else: - return indents - return 0 - - -def get_string_pattern(): - start = r'(\b[uU]?[rR]?)?' - longstr = r'%s"""(\\.|"(?!"")|\\\n|[^"\\])*"""' % start - shortstr = r'%s"(\\.|\\\n|[^"\\])*"' % start - return '|'.join([longstr, longstr.replace('"', "'"), - shortstr, shortstr.replace('"', "'")]) - - -def get_comment_pattern(): - return r'#[^\n]*' diff --git a/pythonFiles/rope/base/default_config.py b/pythonFiles/rope/base/default_config.py deleted file mode 100644 index d1e8f3df1992..000000000000 --- a/pythonFiles/rope/base/default_config.py +++ /dev/null @@ -1,100 +0,0 @@ -# The default ``config.py`` -# flake8: noqa - - -def set_prefs(prefs): - """This function is called before opening the project""" - - # Specify which files and folders to ignore in the project. - # Changes to ignored resources are not added to the history and - # VCSs. Also they are not returned in `Project.get_files()`. - # Note that ``?`` and ``*`` match all characters but slashes. - # '*.pyc': matches 'test.pyc' and 'pkg/test.pyc' - # 'mod*.pyc': matches 'test/mod1.pyc' but not 'mod/1.pyc' - # '.svn': matches 'pkg/.svn' and all of its children - # 'build/*.o': matches 'build/lib.o' but not 'build/sub/lib.o' - # 'build//*.o': matches 'build/lib.o' and 'build/sub/lib.o' - prefs['ignored_resources'] = ['*.pyc', '*~', '.ropeproject', - '.hg', '.svn', '_svn', '.git', '.tox'] - - # Specifies which files should be considered python files. It is - # useful when you have scripts inside your project. Only files - # ending with ``.py`` are considered to be python files by - # default. - #prefs['python_files'] = ['*.py'] - - # Custom source folders: By default rope searches the project - # for finding source folders (folders that should be searched - # for finding modules). You can add paths to that list. Note - # that rope guesses project source folders correctly most of the - # time; use this if you have any problems. - # The folders should be relative to project root and use '/' for - # separating folders regardless of the platform rope is running on. - # 'src/my_source_folder' for instance. - #prefs.add('source_folders', 'src') - - # You can extend python path for looking up modules - #prefs.add('python_path', '~/python/') - - # Should rope save object information or not. - prefs['save_objectdb'] = True - prefs['compress_objectdb'] = False - - # If `True`, rope analyzes each module when it is being saved. - prefs['automatic_soa'] = True - # The depth of calls to follow in static object analysis - prefs['soa_followed_calls'] = 0 - - # If `False` when running modules or unit tests "dynamic object - # analysis" is turned off. This makes them much faster. - prefs['perform_doa'] = True - - # Rope can check the validity of its object DB when running. - prefs['validate_objectdb'] = True - - # How many undos to hold? - prefs['max_history_items'] = 32 - - # Shows whether to save history across sessions. - prefs['save_history'] = True - prefs['compress_history'] = False - - # Set the number spaces used for indenting. According to - # :PEP:`8`, it is best to use 4 spaces. Since most of rope's - # unit-tests use 4 spaces it is more reliable, too. - prefs['indent_size'] = 4 - - # Builtin and c-extension modules that are allowed to be imported - # and inspected by rope. - prefs['extension_modules'] = [] - - # Add all standard c-extensions to extension_modules list. - prefs['import_dynload_stdmods'] = True - - # If `True` modules with syntax errors are considered to be empty. - # The default value is `False`; When `False` syntax errors raise - # `rope.base.exceptions.ModuleSyntaxError` exception. - prefs['ignore_syntax_errors'] = False - - # If `True`, rope ignores unresolvable imports. Otherwise, they - # appear in the importing namespace. - prefs['ignore_bad_imports'] = False - - # If `True`, rope will insert new module imports as - # `from import ` by default. - prefs['prefer_module_from_imports'] = False - - # If `True`, rope will transform a comma list of imports into - # multiple separate import statements when organizing - # imports. - prefs['split_imports'] = False - - # If `True`, rope will sort imports alphabetically by module name - # instead of alphabetically by import statement, with from imports - # after normal imports. - prefs['sort_imports_alphabetically'] = False - - -def project_opened(project): - """This function is called after opening the project""" - # Do whatever you like here! diff --git a/pythonFiles/rope/base/evaluate.py b/pythonFiles/rope/base/evaluate.py deleted file mode 100644 index f43239237111..000000000000 --- a/pythonFiles/rope/base/evaluate.py +++ /dev/null @@ -1,332 +0,0 @@ -import rope.base.builtins -import rope.base.pynames -import rope.base.pyobjects -from rope.base import ast, astutils, exceptions, pyobjects, arguments, worder -from rope.base.utils import pycompat - - -BadIdentifierError = exceptions.BadIdentifierError - - -def eval_location(pymodule, offset): - """Find the pyname at the offset""" - return eval_location2(pymodule, offset)[1] - - -def eval_location2(pymodule, offset): - """Find the primary and pyname at offset""" - pyname_finder = ScopeNameFinder(pymodule) - return pyname_finder.get_primary_and_pyname_at(offset) - - -def eval_node(scope, node): - """Evaluate a `ast.AST` node and return a PyName - - Return `None` if the expression cannot be evaluated. - """ - return eval_node2(scope, node)[1] - - -def eval_node2(scope, node): - evaluator = StatementEvaluator(scope) - ast.walk(node, evaluator) - return evaluator.old_result, evaluator.result - - -def eval_str(holding_scope, name): - return eval_str2(holding_scope, name)[1] - - -def eval_str2(holding_scope, name): - try: - # parenthesizing for handling cases like 'a_var.\nattr' - node = ast.parse('(%s)' % name) - except SyntaxError: - raise BadIdentifierError( - 'Not a resolvable python identifier selected.') - return eval_node2(holding_scope, node) - - -class ScopeNameFinder(object): - - def __init__(self, pymodule): - self.module_scope = pymodule.get_scope() - self.lines = pymodule.lines - self.worder = worder.Worder(pymodule.source_code, True) - - def _is_defined_in_class_body(self, holding_scope, offset, lineno): - if lineno == holding_scope.get_start() and \ - holding_scope.parent is not None and \ - holding_scope.parent.get_kind() == 'Class' and \ - self.worder.is_a_class_or_function_name_in_header(offset): - return True - if lineno != holding_scope.get_start() and \ - holding_scope.get_kind() == 'Class' and \ - self.worder.is_name_assigned_in_class_body(offset): - return True - return False - - def _is_function_name_in_function_header(self, scope, offset, lineno): - if scope.get_start() <= lineno <= scope.get_body_start() and \ - scope.get_kind() == 'Function' and \ - self.worder.is_a_class_or_function_name_in_header(offset): - return True - return False - - def get_pyname_at(self, offset): - return self.get_primary_and_pyname_at(offset)[1] - - def get_primary_and_pyname_at(self, offset): - lineno = self.lines.get_line_number(offset) - holding_scope = self.module_scope.get_inner_scope_for_line(lineno) - # function keyword parameter - if self.worder.is_function_keyword_parameter(offset): - keyword_name = self.worder.get_word_at(offset) - pyobject = self.get_enclosing_function(offset) - if isinstance(pyobject, pyobjects.PyFunction): - return (None, - pyobject.get_parameters().get(keyword_name, None)) - # class body - if self._is_defined_in_class_body(holding_scope, offset, lineno): - class_scope = holding_scope - if lineno == holding_scope.get_start(): - class_scope = holding_scope.parent - name = self.worder.get_primary_at(offset).strip() - try: - return (None, class_scope.pyobject[name]) - except rope.base.exceptions.AttributeNotFoundError: - return (None, None) - # function header - if self._is_function_name_in_function_header(holding_scope, - offset, lineno): - name = self.worder.get_primary_at(offset).strip() - return (None, holding_scope.parent[name]) - # from statement module - if self.worder.is_from_statement_module(offset): - module = self.worder.get_primary_at(offset) - module_pyname = self._find_module(module) - return (None, module_pyname) - if self.worder.is_from_aliased(offset): - name = self.worder.get_from_aliased(offset) - else: - name = self.worder.get_primary_at(offset) - return eval_str2(holding_scope, name) - - def get_enclosing_function(self, offset): - function_parens = self.worder.find_parens_start_from_inside(offset) - try: - function_pyname = self.get_pyname_at(function_parens - 1) - except BadIdentifierError: - function_pyname = None - if function_pyname is not None: - pyobject = function_pyname.get_object() - if isinstance(pyobject, pyobjects.AbstractFunction): - return pyobject - elif isinstance(pyobject, pyobjects.AbstractClass) and \ - '__init__' in pyobject: - return pyobject['__init__'].get_object() - elif '__call__' in pyobject: - return pyobject['__call__'].get_object() - return None - - def _find_module(self, module_name): - dots = 0 - while module_name[dots] == '.': - dots += 1 - return rope.base.pynames.ImportedModule( - self.module_scope.pyobject, module_name[dots:], dots) - - -class StatementEvaluator(object): - - def __init__(self, scope): - self.scope = scope - self.result = None - self.old_result = None - - def _Name(self, node): - self.result = self.scope.lookup(node.id) - - def _Attribute(self, node): - pyname = eval_node(self.scope, node.value) - if pyname is None: - pyname = rope.base.pynames.UnboundName() - self.old_result = pyname - if pyname.get_object() != rope.base.pyobjects.get_unknown(): - try: - self.result = pyname.get_object()[node.attr] - except exceptions.AttributeNotFoundError: - self.result = None - - def _Call(self, node): - primary, pyobject = self._get_primary_and_object_for_node(node.func) - if pyobject is None: - return - - def _get_returned(pyobject): - args = arguments.create_arguments(primary, pyobject, - node, self.scope) - return pyobject.get_returned_object(args) - if isinstance(pyobject, rope.base.pyobjects.AbstractClass): - result = None - if '__new__' in pyobject: - new_function = pyobject['__new__'].get_object() - result = _get_returned(new_function) - if result is None or \ - result == rope.base.pyobjects.get_unknown(): - result = rope.base.pyobjects.PyObject(pyobject) - self.result = rope.base.pynames.UnboundName(pyobject=result) - return - - pyfunction = None - if isinstance(pyobject, rope.base.pyobjects.AbstractFunction): - pyfunction = pyobject - elif '__call__' in pyobject: - pyfunction = pyobject['__call__'].get_object() - if pyfunction is not None: - self.result = rope.base.pynames.UnboundName( - pyobject=_get_returned(pyfunction)) - - def _Str(self, node): - self.result = rope.base.pynames.UnboundName( - pyobject=rope.base.builtins.get_str()) - - def _Num(self, node): - type_name = type(node.n).__name__ - self.result = self._get_builtin_name(type_name) - - def _get_builtin_name(self, type_name): - pytype = rope.base.builtins.builtins[type_name].get_object() - return rope.base.pynames.UnboundName( - rope.base.pyobjects.PyObject(pytype)) - - def _BinOp(self, node): - self.result = rope.base.pynames.UnboundName( - self._get_object_for_node(node.left)) - - def _BoolOp(self, node): - pyobject = self._get_object_for_node(node.values[0]) - if pyobject is None: - pyobject = self._get_object_for_node(node.values[1]) - self.result = rope.base.pynames.UnboundName(pyobject) - - def _Repr(self, node): - self.result = self._get_builtin_name('str') - - def _UnaryOp(self, node): - self.result = rope.base.pynames.UnboundName( - self._get_object_for_node(node.operand)) - - def _Compare(self, node): - self.result = self._get_builtin_name('bool') - - def _Dict(self, node): - keys = None - values = None - if node.keys: - keys = self._get_object_for_node(node.keys[0]) - values = self._get_object_for_node(node.values[0]) - self.result = rope.base.pynames.UnboundName( - pyobject=rope.base.builtins.get_dict(keys, values)) - - def _List(self, node): - holding = None - if node.elts: - holding = self._get_object_for_node(node.elts[0]) - self.result = rope.base.pynames.UnboundName( - pyobject=rope.base.builtins.get_list(holding)) - - def _ListComp(self, node): - pyobject = self._what_does_comprehension_hold(node) - self.result = rope.base.pynames.UnboundName( - pyobject=rope.base.builtins.get_list(pyobject)) - - def _GeneratorExp(self, node): - pyobject = self._what_does_comprehension_hold(node) - self.result = rope.base.pynames.UnboundName( - pyobject=rope.base.builtins.get_iterator(pyobject)) - - def _what_does_comprehension_hold(self, node): - scope = self._make_comprehension_scope(node) - pyname = eval_node(scope, node.elt) - return pyname.get_object() if pyname is not None else None - - def _make_comprehension_scope(self, node): - scope = self.scope - module = scope.pyobject.get_module() - names = {} - for comp in node.generators: - new_names = _get_evaluated_names(comp.target, comp.iter, module, - '.__iter__().next()', node.lineno) - names.update(new_names) - return rope.base.pyscopes.TemporaryScope(scope.pycore, scope, names) - - def _Tuple(self, node): - objects = [] - if len(node.elts) < 4: - for stmt in node.elts: - pyobject = self._get_object_for_node(stmt) - objects.append(pyobject) - else: - objects.append(self._get_object_for_node(node.elts[0])) - self.result = rope.base.pynames.UnboundName( - pyobject=rope.base.builtins.get_tuple(*objects)) - - def _get_object_for_node(self, stmt): - pyname = eval_node(self.scope, stmt) - pyobject = None - if pyname is not None: - pyobject = pyname.get_object() - return pyobject - - def _get_primary_and_object_for_node(self, stmt): - primary, pyname = eval_node2(self.scope, stmt) - pyobject = None - if pyname is not None: - pyobject = pyname.get_object() - return primary, pyobject - - def _Subscript(self, node): - if isinstance(node.slice, ast.Index): - self._call_function(node.value, '__getitem__', - [node.slice.value]) - elif isinstance(node.slice, ast.Slice): - self._call_function(node.value, '__getitem__', - [node.slice]) - - def _Slice(self, node): - self.result = self._get_builtin_name('slice') - - def _call_function(self, node, function_name, other_args=None): - pyname = eval_node(self.scope, node) - if pyname is not None: - pyobject = pyname.get_object() - else: - return - if function_name in pyobject: - called = pyobject[function_name].get_object() - if not called or \ - not isinstance(called, pyobjects.AbstractFunction): - return - args = [node] - if other_args: - args += other_args - arguments_ = arguments.Arguments(args, self.scope) - self.result = rope.base.pynames.UnboundName( - pyobject=called.get_returned_object(arguments_)) - - def _Lambda(self, node): - self.result = rope.base.pynames.UnboundName( - pyobject=rope.base.builtins.Lambda(node, self.scope)) - - -def _get_evaluated_names(targets, assigned, module, evaluation, lineno): - result = {} - for name, levels in astutils.get_name_levels(targets): - assignment = rope.base.pynames.AssignmentValue(assigned, levels, - evaluation) - # XXX: this module should not access `rope.base.pynamesdef`! - pyname = rope.base.pynamesdef.AssignedName(lineno, module) - pyname.assignments.append(assignment) - result[name] = pyname - return result diff --git a/pythonFiles/rope/base/exceptions.py b/pythonFiles/rope/base/exceptions.py deleted file mode 100644 index d161c89ed688..000000000000 --- a/pythonFiles/rope/base/exceptions.py +++ /dev/null @@ -1,61 +0,0 @@ -class RopeError(Exception): - """Base exception for rope""" - - -class ResourceNotFoundError(RopeError): - """Resource not found exception""" - - -class RefactoringError(RopeError): - """Errors for performing a refactoring""" - - -class InterruptedTaskError(RopeError): - """The task has been interrupted""" - - -class HistoryError(RopeError): - """Errors for history undo/redo operations""" - - -class ModuleNotFoundError(RopeError): - """Module not found exception""" - - -class AttributeNotFoundError(RopeError): - """Attribute not found exception""" - - -class NameNotFoundError(RopeError): - """Name not found exception""" - - -class BadIdentifierError(RopeError): - """The name cannot be resolved""" - - -class ModuleSyntaxError(RopeError): - """Module has syntax errors - - The `filename` and `lineno` fields indicate where the error has - occurred. - - """ - - def __init__(self, filename, lineno, message): - self.filename = filename - self.lineno = lineno - self.message_ = message - super(ModuleSyntaxError, self).__init__( - 'Syntax error in file <%s> line <%s>: %s' % - (filename, lineno, message)) - - -class ModuleDecodeError(RopeError): - """Cannot decode module""" - - def __init__(self, filename, message): - self.filename = filename - self.message_ = message - super(ModuleDecodeError, self).__init__( - 'Cannot decode file <%s>: %s' % (filename, message)) diff --git a/pythonFiles/rope/base/fscommands.py b/pythonFiles/rope/base/fscommands.py deleted file mode 100644 index 3564ed919c9c..000000000000 --- a/pythonFiles/rope/base/fscommands.py +++ /dev/null @@ -1,288 +0,0 @@ -"""Project file system commands. - -This modules implements file system operations used by rope. Different -version control systems can be supported by implementing the interface -provided by `FileSystemCommands` class. See `SubversionCommands` and -`MercurialCommands` for example. - -""" -import os -import shutil -import subprocess - -import rope.base.utils.pycompat as pycompat - -try: - unicode -except NameError: - unicode = str - -def create_fscommands(root): - dirlist = os.listdir(root) - commands = {'.hg': MercurialCommands, - '.svn': SubversionCommands, - '.git': GITCommands, - '_svn': SubversionCommands, - '_darcs': DarcsCommands} - for key in commands: - if key in dirlist: - try: - return commands[key](root) - except (ImportError, OSError): - pass - return FileSystemCommands() - - -class FileSystemCommands(object): - - def create_file(self, path): - open(path, 'w').close() - - def create_folder(self, path): - os.mkdir(path) - - def move(self, path, new_location): - shutil.move(path, new_location) - - def remove(self, path): - if os.path.isfile(path): - os.remove(path) - else: - shutil.rmtree(path) - - def write(self, path, data): - file_ = open(path, 'wb') - try: - file_.write(data) - finally: - file_.close() - - -class SubversionCommands(object): - - def __init__(self, *args): - self.normal_actions = FileSystemCommands() - import pysvn - self.client = pysvn.Client() - - def create_file(self, path): - self.normal_actions.create_file(path) - self.client.add(path, force=True) - - def create_folder(self, path): - self.normal_actions.create_folder(path) - self.client.add(path, force=True) - - def move(self, path, new_location): - self.client.move(path, new_location, force=True) - - def remove(self, path): - self.client.remove(path, force=True) - - def write(self, path, data): - self.normal_actions.write(path, data) - - -class MercurialCommands(object): - - def __init__(self, root): - self.hg = self._import_mercurial() - self.normal_actions = FileSystemCommands() - try: - self.ui = self.hg.ui.ui( - verbose=False, debug=False, quiet=True, - interactive=False, traceback=False, report_untrusted=False) - except: - self.ui = self.hg.ui.ui() - self.ui.setconfig('ui', 'interactive', 'no') - self.ui.setconfig('ui', 'debug', 'no') - self.ui.setconfig('ui', 'traceback', 'no') - self.ui.setconfig('ui', 'verbose', 'no') - self.ui.setconfig('ui', 'report_untrusted', 'no') - self.ui.setconfig('ui', 'quiet', 'yes') - - self.repo = self.hg.hg.repository(self.ui, root) - - def _import_mercurial(self): - import mercurial.commands - import mercurial.hg - import mercurial.ui - return mercurial - - def create_file(self, path): - self.normal_actions.create_file(path) - self.hg.commands.add(self.ui, self.repo, path) - - def create_folder(self, path): - self.normal_actions.create_folder(path) - - def move(self, path, new_location): - self.hg.commands.rename(self.ui, self.repo, path, - new_location, after=False) - - def remove(self, path): - self.hg.commands.remove(self.ui, self.repo, path) - - def write(self, path, data): - self.normal_actions.write(path, data) - - -class GITCommands(object): - - def __init__(self, root): - self.root = root - self._do(['version']) - self.normal_actions = FileSystemCommands() - - def create_file(self, path): - self.normal_actions.create_file(path) - self._do(['add', self._in_dir(path)]) - - def create_folder(self, path): - self.normal_actions.create_folder(path) - - def move(self, path, new_location): - self._do(['mv', self._in_dir(path), self._in_dir(new_location)]) - - def remove(self, path): - self._do(['rm', self._in_dir(path)]) - - def write(self, path, data): - # XXX: should we use ``git add``? - self.normal_actions.write(path, data) - - def _do(self, args): - _execute(['git'] + args, cwd=self.root) - - def _in_dir(self, path): - if path.startswith(self.root): - return path[len(self.root) + 1:] - return self.root - - -class DarcsCommands(object): - - def __init__(self, root): - self.root = root - self.normal_actions = FileSystemCommands() - - def create_file(self, path): - self.normal_actions.create_file(path) - self._do(['add', path]) - - def create_folder(self, path): - self.normal_actions.create_folder(path) - self._do(['add', path]) - - def move(self, path, new_location): - self._do(['mv', path, new_location]) - - def remove(self, path): - self.normal_actions.remove(path) - - def write(self, path, data): - self.normal_actions.write(path, data) - - def _do(self, args): - _execute(['darcs'] + args, cwd=self.root) - - -def _execute(args, cwd=None): - process = subprocess.Popen(args, cwd=cwd, stdout=subprocess.PIPE) - process.wait() - return process.returncode - - -def unicode_to_file_data(contents, encoding=None): - if not isinstance(contents, unicode): - return contents - if encoding is None: - encoding = read_str_coding(contents) - if encoding is not None: - return contents.encode(encoding) - try: - return contents.encode() - except UnicodeEncodeError: - return contents.encode('utf-8') - - -def file_data_to_unicode(data, encoding=None): - result = _decode_data(data, encoding) - if '\r' in result: - result = result.replace('\r\n', '\n').replace('\r', '\n') - return result - - -def _decode_data(data, encoding): - if isinstance(data, unicode): - return data - if encoding is None: - encoding = read_str_coding(data) - if encoding is None: - # there is no encoding tip, we need to guess. - # PEP263 says that "encoding not explicitly defined" means it is ascii, - # but we will use utf8 instead since utf8 fully covers ascii and btw is - # the only non-latin sane encoding. - encoding = 'utf-8' - try: - return data.decode(encoding) - except (UnicodeError, LookupError): - # fallback to latin1: it should never fail - return data.decode('latin1') - - -def read_file_coding(path): - file = open(path, 'b') - count = 0 - result = [] - while True: - current = file.read(10) - if not current: - break - count += current.count('\n') - result.append(current) - file.close() - return _find_coding(''.join(result)) - - -def read_str_coding(source): - if type(source) == bytes: - newline = b'\n' - else: - newline = '\n' - #try: - # source = source.decode("utf-8") - #except AttributeError: - # pass - try: - first = source.index(newline) + 1 - second = source.index(newline, first) + 1 - except ValueError: - second = len(source) - return _find_coding(source[:second]) - - -def _find_coding(text): - if isinstance(text, pycompat.str): - text = text.encode('utf-8') - coding = b'coding' - to_chr = chr if pycompat.PY3 else lambda x: x - try: - start = text.index(coding) + len(coding) - if text[start] not in b'=:': - return - start += 1 - while start < len(text) and to_chr(text[start]).isspace(): - start += 1 - end = start - while end < len(text): - c = text[end] - if not to_chr(c).isalnum() and c not in b'-_': - break - end += 1 - result = text[start:end] - if isinstance(result, bytes): - result = result.decode('utf-8') - return result - except ValueError: - pass diff --git a/pythonFiles/rope/base/history.py b/pythonFiles/rope/base/history.py deleted file mode 100644 index d3c523d310c4..000000000000 --- a/pythonFiles/rope/base/history.py +++ /dev/null @@ -1,235 +0,0 @@ -from rope.base import exceptions, change, taskhandle - - -class History(object): - """A class that holds project history""" - - def __init__(self, project, maxundos=None): - self.project = project - self._undo_list = [] - self._redo_list = [] - self._maxundos = maxundos - self._load_history() - self.project.data_files.add_write_hook(self.write) - self.current_change = None - - def _load_history(self): - if self.save: - result = self.project.data_files.read_data( - 'history', compress=self.compress, import_=True) - if result is not None: - to_change = change.DataToChange(self.project) - for data in result[0]: - self._undo_list.append(to_change(data)) - for data in result[1]: - self._redo_list.append(to_change(data)) - - def do(self, changes, task_handle=taskhandle.NullTaskHandle()): - """Perform the change and add it to the `self.undo_list` - - Note that uninteresting changes (changes to ignored files) - will not be appended to `self.undo_list`. - - """ - try: - self.current_change = changes - changes.do(change.create_job_set(task_handle, changes)) - finally: - self.current_change = None - if self._is_change_interesting(changes): - self.undo_list.append(changes) - self._remove_extra_items() - del self.redo_list[:] - - def _remove_extra_items(self): - if len(self.undo_list) > self.max_undos: - del self.undo_list[0:len(self.undo_list) - self.max_undos] - - def _is_change_interesting(self, changes): - for resource in changes.get_changed_resources(): - if not self.project.is_ignored(resource): - return True - return False - - def undo(self, change=None, drop=False, - task_handle=taskhandle.NullTaskHandle()): - """Redo done changes from the history - - When `change` is `None`, the last done change will be undone. - If change is not `None` it should be an item from - `self.undo_list`; this change and all changes that depend on - it will be undone. In both cases the list of undone changes - will be returned. - - If `drop` is `True`, the undone change will not be appended to - the redo list. - - """ - if not self._undo_list: - raise exceptions.HistoryError('Undo list is empty') - if change is None: - change = self.undo_list[-1] - dependencies = self._find_dependencies(self.undo_list, change) - self._move_front(self.undo_list, dependencies) - self._perform_undos(len(dependencies), task_handle) - result = self.redo_list[-len(dependencies):] - if drop: - del self.redo_list[-len(dependencies):] - return result - - def redo(self, change=None, task_handle=taskhandle.NullTaskHandle()): - """Redo undone changes from the history - - When `change` is `None`, the last undone change will be - redone. If change is not `None` it should be an item from - `self.redo_list`; this change and all changes that depend on - it will be redone. In both cases the list of redone changes - will be returned. - - """ - if not self.redo_list: - raise exceptions.HistoryError('Redo list is empty') - if change is None: - change = self.redo_list[-1] - dependencies = self._find_dependencies(self.redo_list, change) - self._move_front(self.redo_list, dependencies) - self._perform_redos(len(dependencies), task_handle) - return self.undo_list[-len(dependencies):] - - def _move_front(self, change_list, changes): - for change in changes: - change_list.remove(change) - change_list.append(change) - - def _find_dependencies(self, change_list, change): - index = change_list.index(change) - return _FindChangeDependencies(change_list[index:])() - - def _perform_undos(self, count, task_handle): - for i in range(count): - self.current_change = self.undo_list[-1] - try: - job_set = change.create_job_set(task_handle, - self.current_change) - self.current_change.undo(job_set) - finally: - self.current_change = None - self.redo_list.append(self.undo_list.pop()) - - def _perform_redos(self, count, task_handle): - for i in range(count): - self.current_change = self.redo_list[-1] - try: - job_set = change.create_job_set(task_handle, - self.current_change) - self.current_change.do(job_set) - finally: - self.current_change = None - self.undo_list.append(self.redo_list.pop()) - - def contents_before_current_change(self, file): - if self.current_change is None: - return None - result = self._search_for_change_contents([self.current_change], file) - if result is not None: - return result - if file.exists() and not file.is_folder(): - return file.read() - else: - return None - - def _search_for_change_contents(self, change_list, file): - for change_ in reversed(change_list): - if isinstance(change_, change.ChangeSet): - result = self._search_for_change_contents(change_.changes, - file) - if result is not None: - return result - if isinstance(change_, change.ChangeContents) and \ - change_.resource == file: - return change_.old_contents - - def write(self): - if self.save: - data = [] - to_data = change.ChangeToData() - self._remove_extra_items() - data.append([to_data(change_) for change_ in self.undo_list]) - data.append([to_data(change_) for change_ in self.redo_list]) - self.project.data_files.write_data('history', data, - compress=self.compress) - - def get_file_undo_list(self, resource): - result = [] - for change in self.undo_list: - if resource in change.get_changed_resources(): - result.append(change) - return result - - def __str__(self): - return 'History holds %s changes in memory' % \ - (len(self.undo_list) + len(self.redo_list)) - - undo_list = property(lambda self: self._undo_list) - redo_list = property(lambda self: self._redo_list) - - @property - def tobe_undone(self): - """The last done change if available, `None` otherwise""" - if self.undo_list: - return self.undo_list[-1] - - @property - def tobe_redone(self): - """The last undone change if available, `None` otherwise""" - if self.redo_list: - return self.redo_list[-1] - - @property - def max_undos(self): - if self._maxundos is None: - return self.project.prefs.get('max_history_items', 100) - else: - return self._maxundos - - @property - def save(self): - return self.project.prefs.get('save_history', False) - - @property - def compress(self): - return self.project.prefs.get('compress_history', False) - - def clear(self): - """Forget all undo and redo information""" - del self.undo_list[:] - del self.redo_list[:] - - -class _FindChangeDependencies(object): - - def __init__(self, change_list): - self.change = change_list[0] - self.change_list = change_list - self.changed_resources = set(self.change.get_changed_resources()) - - def __call__(self): - result = [self.change] - for change in self.change_list[1:]: - if self._depends_on(change, result): - result.append(change) - self.changed_resources.update(change.get_changed_resources()) - return result - - def _depends_on(self, changes, result): - for resource in changes.get_changed_resources(): - if resource is None: - continue - if resource in self.changed_resources: - return True - for changed in self.changed_resources: - if resource.is_folder() and resource.contains(changed): - return True - if changed.is_folder() and changed.contains(resource): - return True - return False diff --git a/pythonFiles/rope/base/libutils.py b/pythonFiles/rope/base/libutils.py deleted file mode 100644 index 4037f183f576..000000000000 --- a/pythonFiles/rope/base/libutils.py +++ /dev/null @@ -1,122 +0,0 @@ -"""A few useful functions for using rope as a library""" -import os.path - -import rope.base.project -import rope.base.pycore -from rope.base import pyobjectsdef -from rope.base import utils -from rope.base import taskhandle - - -def path_to_resource(project, path, type=None): - """Get the resource at path - - You only need to specify `type` if `path` does not exist. It can - be either 'file' or 'folder'. If the type is `None` it is assumed - that the resource already exists. - - Note that this function uses `Project.get_resource()`, - `Project.get_file()`, and `Project.get_folder()` methods. - - """ - project_path = path_relative_to_project_root(project, path) - if project_path is None: - project_path = rope.base.project._realpath(path) - project = rope.base.project.get_no_project() - if type is None: - return project.get_resource(project_path) - if type == 'file': - return project.get_file(project_path) - if type == 'folder': - return project.get_folder(project_path) - return None - - -def path_relative_to_project_root(project, path): - return relative(project.address, path) - -@utils.deprecated() -def relative(root, path): - root = rope.base.project._realpath(root).replace(os.path.sep, '/') - path = rope.base.project._realpath(path).replace(os.path.sep, '/') - if path == root: - return '' - if path.startswith(root + '/'): - return path[len(root) + 1:] - - -def report_change(project, path, old_content): - """Report that the contents of file at `path` was changed - - The new contents of file is retrieved by reading the file. - - """ - resource = path_to_resource(project, path) - if resource is None: - return - for observer in list(project.observers): - observer.resource_changed(resource) - if project.pycore.automatic_soa: - rope.base.pycore.perform_soa_on_changed_scopes(project, resource, - old_content) - - -def analyze_module(project, resource): - """Perform static object analysis on a python file in the project - - Note that this might be really time consuming. - """ - project.pycore.analyze_module(resource) - - -def analyze_modules(project, task_handle=taskhandle.NullTaskHandle()): - """Perform static object analysis on all python files in the project - - Note that this might be really time consuming. - """ - resources = project.get_python_files() - job_set = task_handle.create_jobset('Analyzing Modules', len(resources)) - for resource in resources: - job_set.started_job(resource.path) - analyze_module(project, resource) - job_set.finished_job() - - -def get_string_module(project, code, resource=None, force_errors=False): - """Returns a `PyObject` object for the given code - - If `force_errors` is `True`, `exceptions.ModuleSyntaxError` is - raised if module has syntax errors. This overrides - ``ignore_syntax_errors`` project config. - - """ - return pyobjectsdef.PyModule(project.pycore, code, resource, - force_errors=force_errors) - - -def get_string_scope(project, code, resource=None): - """Returns a `Scope` object for the given code""" - return get_string_module(project, code, resource).get_scope() - - -def is_python_file(project, resource): - return project.pycore.is_python_file(resource) - - -def modname(resource): - if resource.is_folder(): - module_name = resource.name - source_folder = resource.parent - elif resource.name == '__init__.py': - module_name = resource.parent.name - source_folder = resource.parent.parent - else: - module_name = resource.name[:-3] - source_folder = resource.parent - - while source_folder != source_folder.parent and \ - source_folder.has_child('__init__.py'): - module_name = source_folder.name + '.' + module_name - source_folder = source_folder.parent - - return module_name diff --git a/pythonFiles/rope/base/oi/__init__.py b/pythonFiles/rope/base/oi/__init__.py deleted file mode 100644 index 0b1a1525c088..000000000000 --- a/pythonFiles/rope/base/oi/__init__.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Rope object analysis and inference package - -Rope makes some simplifying assumptions about a python program. It -assumes that a program only performs assignments and function calls. -Tracking assignments is simple and `PyName` objects handle that. The -main problem is function calls. Rope uses these two approaches for -obtaining call information: - -* Static object analysis: `rope.base.pycore.PyCore.analyze_module()` - - It can analyze modules to obtain information about functions. This - is done by analyzing function calls in a module or scope. Currently - SOA analyzes the scopes that are changed while saving or when the - user asks to analyze a module. That is mainly because static - analysis is time-consuming. - -* Dynamic object analysis: `rope.base.pycore.PyCore.run_module()` - - When you run a module or your testsuite, when DOA is enabled, it - collects information about parameters passed to and objects returned - from functions. The main problem with this approach is that it is - quite slow; Not when looking up the information but when collecting - them. - -An instance of `rope.base.oi.objectinfo.ObjectInfoManager` can be used -for accessing these information. It saves the data in a -`rope.base.oi.objectdb.ObjectDB` internally. - -Now if our objectdb does not know anything about a function and we -need the value returned by it, static object inference, SOI, comes -into play. It analyzes function body and tries to infer the object -that is returned from it (we usually need the returned value for the -given parameter objects). - -Rope might collect and store information for other `PyName`\s, too. -For instance rope stores the object builtin containers hold. - -""" diff --git a/pythonFiles/rope/base/oi/doa.py b/pythonFiles/rope/base/oi/doa.py deleted file mode 100644 index 3f314c66028c..000000000000 --- a/pythonFiles/rope/base/oi/doa.py +++ /dev/null @@ -1,166 +0,0 @@ -try: - import pickle -except ImportError: - import cPickle as pickle -import marshal -import os -import socket -import subprocess -import sys -import tempfile -import threading - - -class PythonFileRunner(object): - """A class for running python project files""" - - def __init__(self, pycore, file_, args=None, stdin=None, - stdout=None, analyze_data=None): - self.pycore = pycore - self.file = file_ - self.analyze_data = analyze_data - self.observers = [] - self.args = args - self.stdin = stdin - self.stdout = stdout - - def run(self): - """Execute the process""" - env = dict(os.environ) - file_path = self.file.real_path - path_folders = self.pycore.project.get_source_folders() + \ - self.pycore.project.get_python_path_folders() - env['PYTHONPATH'] = os.pathsep.join(folder.real_path - for folder in path_folders) - runmod_path = self.pycore.project.find_module('rope.base.oi.runmod').real_path - self.receiver = None - self._init_data_receiving() - send_info = '-' - if self.receiver: - send_info = self.receiver.get_send_info() - args = [sys.executable, runmod_path, send_info, - self.pycore.project.address, self.file.real_path] - if self.analyze_data is None: - del args[1:4] - if self.args is not None: - args.extend(self.args) - self.process = subprocess.Popen( - executable=sys.executable, args=args, env=env, - cwd=os.path.split(file_path)[0], stdin=self.stdin, - stdout=self.stdout, stderr=self.stdout, close_fds=os.name != 'nt') - - def _init_data_receiving(self): - if self.analyze_data is None: - return - # Disabling FIFO data transfer due to blocking when running - # unittests in the GUI. - # XXX: Handle FIFO data transfer for `rope.ui.testview` - if True or os.name == 'nt': - self.receiver = _SocketReceiver() - else: - self.receiver = _FIFOReceiver() - self.receiving_thread = threading.Thread( - target=self._receive_information) - self.receiving_thread.setDaemon(True) - self.receiving_thread.start() - - def _receive_information(self): - #temp = open('/dev/shm/info', 'wb') - for data in self.receiver.receive_data(): - self.analyze_data(data) - #temp.write(str(data) + '\n') - #temp.close() - for observer in self.observers: - observer() - - def wait_process(self): - """Wait for the process to finish""" - self.process.wait() - if self.analyze_data: - self.receiving_thread.join() - - def kill_process(self): - """Stop the process""" - if self.process.poll() is not None: - return - try: - if hasattr(self.process, 'terminate'): - self.process.terminate() - elif os.name != 'nt': - os.kill(self.process.pid, 9) - else: - import ctypes - handle = int(self.process._handle) - ctypes.windll.kernel32.TerminateProcess(handle, -1) - except OSError: - pass - - def add_finishing_observer(self, observer): - """Notify this observer when execution finishes""" - self.observers.append(observer) - - -class _MessageReceiver(object): - - def receive_data(self): - pass - - def get_send_info(self): - pass - - -class _SocketReceiver(_MessageReceiver): - - def __init__(self): - self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.data_port = 3037 - while self.data_port < 4000: - try: - self.server_socket.bind(('', self.data_port)) - break - except socket.error: - self.data_port += 1 - self.server_socket.listen(1) - - def get_send_info(self): - return str(self.data_port) - - def receive_data(self): - conn, addr = self.server_socket.accept() - self.server_socket.close() - my_file = conn.makefile('rb') - while True: - try: - yield pickle.load(my_file) - except EOFError: - break - my_file.close() - conn.close() - - -class _FIFOReceiver(_MessageReceiver): - - def __init__(self): - # XXX: this is insecure and might cause race conditions - self.file_name = self._get_file_name() - os.mkfifo(self.file_name) - - def _get_file_name(self): - prefix = tempfile.gettempdir() + '/__rope_' - i = 0 - while os.path.exists(prefix + str(i).rjust(4, '0')): - i += 1 - return prefix + str(i).rjust(4, '0') - - def get_send_info(self): - return self.file_name - - def receive_data(self): - my_file = open(self.file_name, 'rb') - while True: - try: - yield marshal.load(my_file) - except EOFError: - break - my_file.close() - os.remove(self.file_name) diff --git a/pythonFiles/rope/base/oi/docstrings.py b/pythonFiles/rope/base/oi/docstrings.py deleted file mode 100644 index 4519e12630ef..000000000000 --- a/pythonFiles/rope/base/oi/docstrings.py +++ /dev/null @@ -1,226 +0,0 @@ -""" -Hinting the type using docstring of class/function. - -It's an irreplaceable thing if you are using Dependency Injection with passive class: -http://www.martinfowler.com/articles/injection.html - -Some code extracted (or based on code) from: -https://github.com/davidhalter/jedi/blob/b489019f5bd5750051122b94cc767df47751ecb7/jedi/evaluate/docstrings.py -Thanks to @davidhalter for this utils under MIT License. - -Similar solutions: - - - https://www.jetbrains.com/pycharm/help/type-hinting-in-pycharm.html - - https://www.python.org/dev/peps/pep-0484/#type-comments - - http://www.pydev.org/manual_adv_type_hints.html - - https://jedi.readthedocs.org/en/latest/docs/features.html#type-hinting - -Discussions: - - - https://groups.google.com/d/topic/rope-dev/JlAzmZ83K1M/discussion - - https://groups.google.com/d/topic/rope-dev/LCFNN98vckI/discussion - -""" -import re -from ast import literal_eval - -from rope.base.exceptions import AttributeNotFoundError -from rope.base.evaluate import ScopeNameFinder -from rope.base.pyobjects import PyClass, PyFunction - -PEP0484_PATTERNS = [ - re.compile(r'type:\s*([^\n, ]+)'), -] - -DOCSTRING_PARAM_PATTERNS = [ - r'\s*:type\s+%s:\s*([^\n, ]+)', # Sphinx - r'\s*:param\s+(\w+)\s+%s:[^\n]+', # Sphinx param with type - r'\s*@type\s+%s:\s*([^\n, ]+)', # Epydoc -] - -DOCSTRING_RETURN_PATTERNS = [ - re.compile(r'\s*:rtype:\s*([^\n, ]+)', re.M), # Sphinx - re.compile(r'\s*@rtype:\s*([^\n, ]+)', re.M), # Epydoc -] - -REST_ROLE_PATTERN = re.compile(r':[^`]+:`([^`]+)`') - -try: - from numpydoc.docscrape import NumpyDocString -except ImportError: - def _search_param_in_numpydocstr(docstr, param_str): - return [] -else: - def _search_param_in_numpydocstr(docstr, param_str): - """Search `docstr` (in numpydoc format) for type(-s) of `param_str`.""" - params = NumpyDocString(docstr)._parsed_data['Parameters'] - for p_name, p_type, p_descr in params: - if p_name == param_str: - m = re.match('([^,]+(,[^,]+)*?)(,[ ]*optional)?$', p_type) - if m: - p_type = m.group(1) - - if p_type.startswith('{'): - types = set(type(x).__name__ for x in literal_eval(p_type)) - return list(types) - else: - return [p_type] - return [] - - -def hint_pep0484(pyname): - from rope.base.oi.soi import _get_lineno_for_node - lineno = _get_lineno_for_node(pyname.assignments[0].ast_node) - holding_scope = pyname.module.get_scope().get_inner_scope_for_line(lineno) - line = holding_scope._get_global_scope()._scope_finder.lines.get_line(lineno) - if '#' in line: - type_strs = _search_type_in_pep0484(line.split('#', 1)[1]) - if type_strs: - return _resolve_type(type_strs[0], holding_scope.pyobject) - - -def _search_type_in_pep0484(code): - """ For more info see: - https://www.python.org/dev/peps/pep-0484/#type-comments - - >>> _search_type_in_pep0484('type: int') - ['int'] - """ - for p in PEP0484_PATTERNS: - match = p.search(code) - if match: - return [match.group(1)] - - -def hint_param(pyfunc, param_name): - type_strs = None - func = pyfunc - while not type_strs and func: - if func.get_doc(): - type_strs = _search_param_in_docstr(func.get_doc(), param_name) - func = _get_superfunc(func) - - if type_strs: - return _resolve_type(type_strs[0], pyfunc) - - -def _get_superfunc(pyfunc): - - if not isinstance(pyfunc.parent, PyClass): - return - - for cls in _get_mro(pyfunc.parent)[1:]: - try: - superfunc = cls.get_attribute(pyfunc.get_name()).get_object() - except AttributeNotFoundError: - pass - else: - if isinstance(superfunc, PyFunction): - return superfunc - - -def _get_mro(pyclass): - # FIXME: to use real mro() result - l = [pyclass] - for cls in l: - for super_cls in cls.get_superclasses(): - if isinstance(super_cls, PyClass) and super_cls not in l: - l.append(super_cls) - return l - - -def _resolve_type(type_name, pyobj): - type_ = None - if '.' not in type_name: - try: - type_ = pyobj.get_module().get_scope().get_name(type_name).get_object() - except Exception: - pass - else: - mod_name, attr_name = type_name.rsplit('.', 1) - try: - mod_finder = ScopeNameFinder(pyobj.get_module()) - mod = mod_finder._find_module(mod_name).get_object() - type_ = mod.get_attribute(attr_name).get_object() - except Exception: - pass - return type_ - - -def _search_param_in_docstr(docstr, param_str): - """ - Search `docstr` for type(-s) of `param_str`. - - >>> _search_param_in_docstr(':type param: int', 'param') - ['int'] - >>> _search_param_in_docstr('@type param: int', 'param') - ['int'] - >>> _search_param_in_docstr( - ... ':type param: :class:`threading.Thread`', 'param') - ['threading.Thread'] - >>> bool(_search_param_in_docstr('no document', 'param')) - False - >>> _search_param_in_docstr(':param int param: some description', 'param') - ['int'] - - """ - patterns = [re.compile(p % re.escape(param_str)) - for p in DOCSTRING_PARAM_PATTERNS] - for pattern in patterns: - match = pattern.search(docstr) - if match: - return [_strip_rst_role(match.group(1))] - - return (_search_param_in_numpydocstr(docstr, param_str) or - []) - - -def _strip_rst_role(type_str): - """ - Strip off the part looks like a ReST role in `type_str`. - - >>> _strip_rst_role(':class:`ClassName`') # strip off :class: - 'ClassName' - >>> _strip_rst_role(':py:obj:`module.Object`') # works with domain - 'module.Object' - >>> _strip_rst_role('ClassName') # do nothing when not ReST role - 'ClassName' - - See also: - http://sphinx-doc.org/domains.html#cross-referencing-python-objects - - """ - match = REST_ROLE_PATTERN.match(type_str) - if match: - return match.group(1) - else: - return type_str - - -def hint_return(pyfunc): - type_str = None - func = pyfunc - while not type_str and func: - if func.get_doc(): - type_str = _search_return_in_docstr(func.get_doc()) - func = _get_superfunc(func) - if type_str: - return _resolve_type(type_str, pyfunc) - - -def _search_return_in_docstr(code): - for p in DOCSTRING_RETURN_PATTERNS: - match = p.search(code) - if match: - return _strip_rst_role(match.group(1)) - - -def hint_attr(pyclass, attr_name): - type_strs = None - for cls in _get_mro(pyclass): - if cls.get_doc(): - type_strs = _search_param_in_docstr(cls.get_doc(), attr_name) - if type_strs: - break - if type_strs: - return _resolve_type(type_strs[0], pyclass) diff --git a/pythonFiles/rope/base/oi/memorydb.py b/pythonFiles/rope/base/oi/memorydb.py deleted file mode 100644 index 01c814ce460a..000000000000 --- a/pythonFiles/rope/base/oi/memorydb.py +++ /dev/null @@ -1,127 +0,0 @@ -from rope.base.oi import objectdb - - -class MemoryDB(objectdb.FileDict): - - def __init__(self, project, persist=None): - self.project = project - self._persist = persist - self.files = self - self._load_files() - self.project.data_files.add_write_hook(self.write) - - def _load_files(self): - self._files = {} - if self.persist: - result = self.project.data_files.read_data( - 'objectdb', compress=self.compress, import_=True) - if result is not None: - self._files = result - - def keys(self): - return self._files.keys() - - def __iter__(self): - for f in self._files: - yield f - - def __len__(self): - return len(self._files) - - def __setitem__(self): - raise NotImplementedError() - - def __contains__(self, key): - return key in self._files - - def __getitem__(self, key): - return FileInfo(self._files[key]) - - def create(self, path): - self._files[path] = {} - - def rename(self, file, newfile): - if file not in self._files: - return - self._files[newfile] = self._files[file] - del self[file] - - def __delitem__(self, file): - del self._files[file] - - def write(self): - if self.persist: - self.project.data_files.write_data('objectdb', self._files, - self.compress) - - @property - def compress(self): - return self.project.prefs.get('compress_objectdb', False) - - @property - def persist(self): - if self._persist is not None: - return self._persist - else: - return self.project.prefs.get('save_objectdb', False) - - -class FileInfo(objectdb.FileInfo): - - def __init__(self, scopes): - self.scopes = scopes - - def create_scope(self, key): - self.scopes[key] = ScopeInfo() - - def keys(self): - return self.scopes.keys() - - def __contains__(self, key): - return key in self.scopes - - def __getitem__(self, key): - return self.scopes[key] - - def __delitem__(self, key): - del self.scopes[key] - - def __iter__(self): - for s in self.scopes: - yield s - - def __len__(self): - return len(self.scopes) - - def __setitem__(self): - raise NotImplementedError() - - - -class ScopeInfo(objectdb.ScopeInfo): - - def __init__(self): - self.call_info = {} - self.per_name = {} - - def get_per_name(self, name): - return self.per_name.get(name, None) - - def save_per_name(self, name, value): - self.per_name[name] = value - - def get_returned(self, parameters): - return self.call_info.get(parameters, None) - - def get_call_infos(self): - for args, returned in self.call_info.items(): - yield objectdb.CallInfo(args, returned) - - def add_call(self, parameters, returned): - self.call_info[parameters] = returned - - def __getstate__(self): - return (self.call_info, self.per_name) - - def __setstate__(self, data): - self.call_info, self.per_name = data diff --git a/pythonFiles/rope/base/oi/objectdb.py b/pythonFiles/rope/base/oi/objectdb.py deleted file mode 100644 index 61f2711dd4e0..000000000000 --- a/pythonFiles/rope/base/oi/objectdb.py +++ /dev/null @@ -1,179 +0,0 @@ -from __future__ import print_function -try: - from collections import MutableMapping -except ImportError: - from UserDict import DictMixin as MutableMapping - - -class ObjectDB(object): - - def __init__(self, db, validation): - self.db = db - self.validation = validation - self.observers = [] - self.files = db.files - - def validate_files(self): - for file in list(self.files): - if not self.validation.is_file_valid(file): - del self.files[file] - self._file_removed(file) - - def validate_file(self, file): - if file not in self.files: - return - for key in list(self.files[file]): - if not self.validation.is_scope_valid(file, key): - del self.files[file][key] - - def file_moved(self, file, newfile): - if file not in self.files: - return - self.files.rename(file, newfile) - self._file_removed(file) - self._file_added(newfile) - - def get_files(self): - return self.files.keys() - - def get_returned(self, path, key, args): - scope_info = self._get_scope_info(path, key, readonly=True) - result = scope_info.get_returned(args) - if self.validation.is_value_valid(result): - return result - - def get_pername(self, path, key, name): - scope_info = self._get_scope_info(path, key, readonly=True) - result = scope_info.get_per_name(name) - if self.validation.is_value_valid(result): - return result - - def get_callinfos(self, path, key): - scope_info = self._get_scope_info(path, key, readonly=True) - return scope_info.get_call_infos() - - def add_callinfo(self, path, key, args, returned): - scope_info = self._get_scope_info(path, key, readonly=False) - old_returned = scope_info.get_returned(args) - if self.validation.is_more_valid(returned, old_returned): - scope_info.add_call(args, returned) - - def add_pername(self, path, key, name, value): - scope_info = self._get_scope_info(path, key, readonly=False) - old_value = scope_info.get_per_name(name) - if self.validation.is_more_valid(value, old_value): - scope_info.save_per_name(name, value) - - def add_file_list_observer(self, observer): - self.observers.append(observer) - - def write(self): - self.db.write() - - def _get_scope_info(self, path, key, readonly=True): - if path not in self.files: - if readonly: - return _NullScopeInfo() - self.files.create(path) - self._file_added(path) - if key not in self.files[path]: - if readonly: - return _NullScopeInfo() - self.files[path].create_scope(key) - result = self.files[path][key] - if isinstance(result, dict): - print(self.files, self.files[path], self.files[path][key]) - return result - - def _file_removed(self, path): - for observer in self.observers: - observer.removed(path) - - def _file_added(self, path): - for observer in self.observers: - observer.added(path) - - def __str__(self): - scope_count = 0 - for file_dict in self.files.values(): - scope_count += len(file_dict) - return 'ObjectDB holds %s file and %s scope infos' % \ - (len(self.files), scope_count) - - -class _NullScopeInfo(object): - - def __init__(self, error_on_write=True): - self.error_on_write = error_on_write - - def get_per_name(self, name): - pass - - def save_per_name(self, name, value): - if self.error_on_write: - raise NotImplementedError() - - def get_returned(self, parameters): - pass - - def get_call_infos(self): - return [] - - def add_call(self, parameters, returned): - if self.error_on_write: - raise NotImplementedError() - - -class FileInfo(MutableMapping): - - def create_scope(self, key): - pass - - -class FileDict(MutableMapping): - - def create(self, key): - pass - - def rename(self, key, new_key): - pass - - -class ScopeInfo(object): - - def get_per_name(self, name): - pass - - def save_per_name(self, name, value): - pass - - def get_returned(self, parameters): - pass - - def get_call_infos(self): - pass - - def add_call(self, parameters, returned): - pass - - -class CallInfo(object): - - def __init__(self, args, returned): - self.args = args - self.returned = returned - - def get_parameters(self): - return self.args - - def get_returned(self): - return self.returned - - -class FileListObserver(object): - - def added(self, path): - pass - - def removed(self, path): - pass diff --git a/pythonFiles/rope/base/oi/objectinfo.py b/pythonFiles/rope/base/oi/objectinfo.py deleted file mode 100644 index f86d72e0b5cc..000000000000 --- a/pythonFiles/rope/base/oi/objectinfo.py +++ /dev/null @@ -1,232 +0,0 @@ -import warnings - -from rope.base import exceptions, resourceobserver -from rope.base.oi import objectdb, memorydb, transform - - -class ObjectInfoManager(object): - """Stores object information - - It uses an instance of `objectdb.ObjectDB` for storing - information. - - """ - - def __init__(self, project): - self.project = project - self.to_textual = transform.PyObjectToTextual(project) - self.to_pyobject = transform.TextualToPyObject(project) - self.doi_to_pyobject = transform.DOITextualToPyObject(project) - self._init_objectdb() - if project.prefs.get('validate_objectdb', False): - self._init_validation() - - def _init_objectdb(self): - dbtype = self.project.get_prefs().get('objectdb_type', None) - persist = None - if dbtype is not None: - warnings.warn( - '"objectdb_type" project config is deprecated;\n' - 'Use "save_objectdb" instead in your project ' - 'config file.\n(".ropeproject/config.py" by default)\n', - DeprecationWarning) - if dbtype != 'memory' and self.project.ropefolder is not None: - persist = True - self.validation = TextualValidation(self.to_pyobject) - db = memorydb.MemoryDB(self.project, persist=persist) - self.objectdb = objectdb.ObjectDB(db, self.validation) - - def _init_validation(self): - self.objectdb.validate_files() - observer = resourceobserver.ResourceObserver( - changed=self._resource_changed, moved=self._resource_moved, - removed=self._resource_moved) - files = [] - for path in self.objectdb.get_files(): - resource = self.to_pyobject.path_to_resource(path) - if resource is not None and resource.project == self.project: - files.append(resource) - self.observer = resourceobserver.FilteredResourceObserver(observer, - files) - self.objectdb.add_file_list_observer(_FileListObserver(self)) - self.project.add_observer(self.observer) - - def _resource_changed(self, resource): - try: - self.objectdb.validate_file( - self.to_textual.resource_to_path(resource)) - except exceptions.ModuleSyntaxError: - pass - - def _resource_moved(self, resource, new_resource=None): - self.observer.remove_resource(resource) - if new_resource is not None: - old = self.to_textual.resource_to_path(resource) - new = self.to_textual.resource_to_path(new_resource) - self.objectdb.file_moved(old, new) - self.observer.add_resource(new_resource) - - def get_returned(self, pyobject, args): - result = self.get_exact_returned(pyobject, args) - if result is not None: - return result - path, key = self._get_scope(pyobject) - if path is None: - return None - for call_info in self.objectdb.get_callinfos(path, key): - returned = call_info.get_returned() - if returned and returned[0] not in ('unknown', 'none'): - result = returned - break - if result is None: - result = returned - if result is not None: - return self.to_pyobject(result) - - def get_exact_returned(self, pyobject, args): - path, key = self._get_scope(pyobject) - if path is not None: - returned = self.objectdb.get_returned( - path, key, self._args_to_textual(pyobject, args)) - if returned is not None: - return self.to_pyobject(returned) - - def _args_to_textual(self, pyfunction, args): - parameters = list(pyfunction.get_param_names(special_args=False)) - arguments = args.get_arguments(parameters)[:len(parameters)] - textual_args = tuple([self.to_textual(arg) - for arg in arguments]) - return textual_args - - def get_parameter_objects(self, pyobject): - path, key = self._get_scope(pyobject) - if path is None: - return None - arg_count = len(pyobject.get_param_names(special_args=False)) - unknowns = arg_count - parameters = [None] * arg_count - for call_info in self.objectdb.get_callinfos(path, key): - args = call_info.get_parameters() - for index, arg in enumerate(args[:arg_count]): - old = parameters[index] - if self.validation.is_more_valid(arg, old): - parameters[index] = arg - if self.validation.is_value_valid(arg): - unknowns -= 1 - if unknowns == 0: - break - if unknowns < arg_count: - return [self.to_pyobject(parameter) - for parameter in parameters] - - def get_passed_objects(self, pyfunction, parameter_index): - path, key = self._get_scope(pyfunction) - if path is None: - return [] - result = [] - for call_info in self.objectdb.get_callinfos(path, key): - args = call_info.get_parameters() - if len(args) > parameter_index: - parameter = self.to_pyobject(args[parameter_index]) - if parameter is not None: - result.append(parameter) - return result - - def doa_data_received(self, data): - def doi_to_normal(textual): - pyobject = self.doi_to_pyobject(textual) - return self.to_textual(pyobject) - function = doi_to_normal(data[0]) - args = tuple([doi_to_normal(textual) for textual in data[1]]) - returned = doi_to_normal(data[2]) - if function[0] == 'defined' and len(function) == 3: - self._save_data(function, args, returned) - - def function_called(self, pyfunction, params, returned=None): - function_text = self.to_textual(pyfunction) - params_text = tuple([self.to_textual(param) - for param in params]) - returned_text = ('unknown',) - if returned is not None: - returned_text = self.to_textual(returned) - self._save_data(function_text, params_text, returned_text) - - def save_per_name(self, scope, name, data): - path, key = self._get_scope(scope.pyobject) - if path is not None: - self.objectdb.add_pername(path, key, name, self.to_textual(data)) - - def get_per_name(self, scope, name): - path, key = self._get_scope(scope.pyobject) - if path is not None: - result = self.objectdb.get_pername(path, key, name) - if result is not None: - return self.to_pyobject(result) - - def _save_data(self, function, args, returned=('unknown',)): - self.objectdb.add_callinfo(function[1], function[2], args, returned) - - def _get_scope(self, pyobject): - resource = pyobject.get_module().get_resource() - if resource is None: - return None, None - textual = self.to_textual(pyobject) - if textual[0] == 'defined': - path = textual[1] - if len(textual) == 3: - key = textual[2] - else: - key = '' - return path, key - return None, None - - def sync(self): - self.objectdb.sync() - - def __str__(self): - return str(self.objectdb) - - -class TextualValidation(object): - - def __init__(self, to_pyobject): - self.to_pyobject = to_pyobject - - def is_value_valid(self, value): - # ???: Should none and unknown be considered valid? - if value is None or value[0] in ('none', 'unknown'): - return False - return self.to_pyobject(value) is not None - - def is_more_valid(self, new, old): - if old is None: - return True - return new[0] not in ('unknown', 'none') - - def is_file_valid(self, path): - return self.to_pyobject.path_to_resource(path) is not None - - def is_scope_valid(self, path, key): - if key == '': - textual = ('defined', path) - else: - textual = ('defined', path, key) - return self.to_pyobject(textual) is not None - - -class _FileListObserver(object): - - def __init__(self, object_info): - self.object_info = object_info - self.observer = self.object_info.observer - self.to_pyobject = self.object_info.to_pyobject - - def removed(self, path): - resource = self.to_pyobject.path_to_resource(path) - if resource is not None: - self.observer.remove_resource(resource) - - def added(self, path): - resource = self.to_pyobject.path_to_resource(path) - if resource is not None: - self.observer.add_resource(resource) diff --git a/pythonFiles/rope/base/oi/runmod.py b/pythonFiles/rope/base/oi/runmod.py deleted file mode 100644 index ba0184c17830..000000000000 --- a/pythonFiles/rope/base/oi/runmod.py +++ /dev/null @@ -1,222 +0,0 @@ -def __rope_start_everything(): - import os - import sys - import socket - try: - import pickle - except ImportError: - import cPickle as pickle - import marshal - import inspect - import types - import threading - import rope.base.utils.pycompat as pycompat - - class _MessageSender(object): - - def send_data(self, data): - pass - - class _SocketSender(_MessageSender): - - def __init__(self, port): - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.connect(('127.0.0.1', port)) - self.my_file = s.makefile('wb') - - def send_data(self, data): - if not self.my_file.closed: - pickle.dump(data, self.my_file) - - def close(self): - self.my_file.close() - - class _FileSender(_MessageSender): - - def __init__(self, file_name): - self.my_file = open(file_name, 'wb') - - def send_data(self, data): - if not self.my_file.closed: - marshal.dump(data, self.my_file) - - def close(self): - self.my_file.close() - - def _cached(func): - cache = {} - - def newfunc(self, arg): - if arg in cache: - return cache[arg] - result = func(self, arg) - cache[arg] = result - return result - return newfunc - - class _FunctionCallDataSender(object): - - def __init__(self, send_info, project_root): - self.project_root = project_root - if send_info.isdigit(): - self.sender = _SocketSender(int(send_info)) - else: - self.sender = _FileSender(send_info) - - def global_trace(frame, event, arg): - # HACK: Ignoring out->in calls - # This might lose some information - if self._is_an_interesting_call(frame): - return self.on_function_call - sys.settrace(global_trace) - threading.settrace(global_trace) - - def on_function_call(self, frame, event, arg): - if event != 'return': - return - args = [] - returned = ('unknown',) - code = frame.f_code - for argname in code.co_varnames[:code.co_argcount]: - try: - argvalue = self._object_to_persisted_form( - frame.f_locals[argname]) - args.append(argvalue) - except (TypeError, AttributeError): - args.append(('unknown',)) - try: - returned = self._object_to_persisted_form(arg) - except (TypeError, AttributeError): - pass - try: - data = (self._object_to_persisted_form(frame.f_code), - tuple(args), returned) - self.sender.send_data(data) - except (TypeError): - pass - return self.on_function_call - - def _is_an_interesting_call(self, frame): - #if frame.f_code.co_name in ['?', '']: - # return False - #return not frame.f_back or - # not self._is_code_inside_project(frame.f_back.f_code) - if not self._is_code_inside_project(frame.f_code) and \ - (not frame.f_back or - not self._is_code_inside_project(frame.f_back.f_code)): - return False - return True - - def _is_code_inside_project(self, code): - source = self._path(code.co_filename) - return source is not None and os.path.exists(source) and \ - _realpath(source).startswith(self.project_root) - - @_cached - def _get_persisted_code(self, object_): - source = self._path(object_.co_filename) - if not os.path.exists(source): - raise TypeError('no source') - return ('defined', _realpath(source), str(object_.co_firstlineno)) - - @_cached - def _get_persisted_class(self, object_): - try: - return ('defined', _realpath(inspect.getsourcefile(object_)), - object_.__name__) - except (TypeError, AttributeError): - return ('unknown',) - - def _get_persisted_builtin(self, object_): - if isinstance(object_, pycompat.string_types): - return ('builtin', 'str') - if isinstance(object_, list): - holding = None - if len(object_) > 0: - holding = object_[0] - return ('builtin', 'list', - self._object_to_persisted_form(holding)) - if isinstance(object_, dict): - keys = None - values = None - if len(object_) > 0: - # @todo - fix it properly, why is __locals__ being - # duplicated ? - keys = [key for key in object_.keys() if key != '__locals__'][0] - values = object_[keys] - return ('builtin', 'dict', - self._object_to_persisted_form(keys), - self._object_to_persisted_form(values)) - if isinstance(object_, tuple): - objects = [] - if len(object_) < 3: - for holding in object_: - objects.append(self._object_to_persisted_form(holding)) - else: - objects.append(self._object_to_persisted_form(object_[0])) - return tuple(['builtin', 'tuple'] + objects) - if isinstance(object_, set): - holding = None - if len(object_) > 0: - for o in object_: - holding = o - break - return ('builtin', 'set', - self._object_to_persisted_form(holding)) - return ('unknown',) - - def _object_to_persisted_form(self, object_): - if object_ is None: - return ('none',) - if isinstance(object_, types.CodeType): - return self._get_persisted_code(object_) - if isinstance(object_, types.FunctionType): - return self._get_persisted_code(object_.__code__) - if isinstance(object_, types.MethodType): - return self._get_persisted_code(object_.__func__.__code__) - if isinstance(object_, types.ModuleType): - return self._get_persisted_module(object_) - if isinstance(object_, pycompat.string_types + (list, dict, tuple, set)): - return self._get_persisted_builtin(object_) - if isinstance(object_, type): - return self._get_persisted_class(object_) - return ('instance', self._get_persisted_class(type(object_))) - - @_cached - def _get_persisted_module(self, object_): - path = self._path(object_.__file__) - if path and os.path.exists(path): - return ('defined', _realpath(path)) - return ('unknown',) - - def _path(self, path): - if path.endswith('.pyc'): - path = path[:-1] - if path.endswith('.py'): - return path - - def close(self): - self.sender.close() - sys.settrace(None) - - def _realpath(path): - return os.path.realpath(os.path.abspath(os.path.expanduser(path))) - - send_info = sys.argv[1] - project_root = sys.argv[2] - file_to_run = sys.argv[3] - run_globals = globals() - run_globals.update({'__name__': '__main__', - '__builtins__': __builtins__, - '__file__': file_to_run}) - - if send_info != '-': - data_sender = _FunctionCallDataSender(send_info, project_root) - del sys.argv[1:4] - pycompat.execfile(file_to_run, run_globals) - if send_info != '-': - data_sender.close() - - -if __name__ == '__main__': - __rope_start_everything() diff --git a/pythonFiles/rope/base/oi/soa.py b/pythonFiles/rope/base/oi/soa.py deleted file mode 100644 index a34b970ea2f5..000000000000 --- a/pythonFiles/rope/base/oi/soa.py +++ /dev/null @@ -1,139 +0,0 @@ -import rope.base.ast -import rope.base.oi.soi -import rope.base.pynames -from rope.base import pyobjects, evaluate, astutils, arguments - - -def analyze_module(pycore, pymodule, should_analyze, - search_subscopes, followed_calls): - """Analyze `pymodule` for static object inference - - Analyzes scopes for collecting object information. The analysis - starts from inner scopes. - - """ - _analyze_node(pycore, pymodule, should_analyze, - search_subscopes, followed_calls) - - -def _analyze_node(pycore, pydefined, should_analyze, - search_subscopes, followed_calls): - if search_subscopes(pydefined): - for scope in pydefined.get_scope().get_scopes(): - _analyze_node(pycore, scope.pyobject, should_analyze, - search_subscopes, followed_calls) - if should_analyze(pydefined): - new_followed_calls = max(0, followed_calls - 1) - return_true = lambda pydefined: True - return_false = lambda pydefined: False - - def _follow(pyfunction): - _analyze_node(pycore, pyfunction, return_true, - return_false, new_followed_calls) - - if not followed_calls: - _follow = None - visitor = SOAVisitor(pycore, pydefined, _follow) - for child in rope.base.ast.get_child_nodes(pydefined.get_ast()): - rope.base.ast.walk(child, visitor) - - -class SOAVisitor(object): - - def __init__(self, pycore, pydefined, follow_callback=None): - self.pycore = pycore - self.pymodule = pydefined.get_module() - self.scope = pydefined.get_scope() - self.follow = follow_callback - - def _FunctionDef(self, node): - pass - - def _ClassDef(self, node): - pass - - def _Call(self, node): - for child in rope.base.ast.get_child_nodes(node): - rope.base.ast.walk(child, self) - primary, pyname = evaluate.eval_node2(self.scope, node.func) - if pyname is None: - return - pyfunction = pyname.get_object() - if isinstance(pyfunction, pyobjects.AbstractFunction): - args = arguments.create_arguments(primary, pyfunction, - node, self.scope) - elif isinstance(pyfunction, pyobjects.PyClass): - pyclass = pyfunction - if '__init__' in pyfunction: - pyfunction = pyfunction['__init__'].get_object() - pyname = rope.base.pynames.UnboundName(pyobjects.PyObject(pyclass)) - args = self._args_with_self(primary, pyname, pyfunction, node) - elif '__call__' in pyfunction: - pyfunction = pyfunction['__call__'].get_object() - args = self._args_with_self(primary, pyname, pyfunction, node) - else: - return - self._call(pyfunction, args) - - def _args_with_self(self, primary, self_pyname, pyfunction, node): - base_args = arguments.create_arguments(primary, pyfunction, - node, self.scope) - return arguments.MixedArguments(self_pyname, base_args, self.scope) - - def _call(self, pyfunction, args): - if isinstance(pyfunction, pyobjects.PyFunction): - if self.follow is not None: - before = self._parameter_objects(pyfunction) - self.pycore.object_info.function_called( - pyfunction, args.get_arguments(pyfunction.get_param_names())) - pyfunction._set_parameter_pyobjects(None) - if self.follow is not None: - after = self._parameter_objects(pyfunction) - if after != before: - self.follow(pyfunction) - # XXX: Maybe we should not call every builtin function - if isinstance(pyfunction, rope.base.builtins.BuiltinFunction): - pyfunction.get_returned_object(args) - - def _parameter_objects(self, pyfunction): - result = [] - for i in range(len(pyfunction.get_param_names(False))): - result.append(pyfunction.get_parameter(i)) - return result - - def _Assign(self, node): - for child in rope.base.ast.get_child_nodes(node): - rope.base.ast.walk(child, self) - visitor = _SOAAssignVisitor() - nodes = [] - for child in node.targets: - rope.base.ast.walk(child, visitor) - nodes.extend(visitor.nodes) - for subscript, levels in nodes: - instance = evaluate.eval_node(self.scope, subscript.value) - args_pynames = [] - args_pynames.append(evaluate.eval_node(self.scope, - subscript.slice.value)) - value = rope.base.oi.soi._infer_assignment( - rope.base.pynames.AssignmentValue(node.value, levels), - self.pymodule) - args_pynames.append(rope.base.pynames.UnboundName(value)) - if instance is not None and value is not None: - pyobject = instance.get_object() - if '__setitem__' in pyobject: - pyfunction = pyobject['__setitem__'].get_object() - args = arguments.ObjectArguments([instance] + args_pynames) - self._call(pyfunction, args) - # IDEA: handle `__setslice__`, too - - -class _SOAAssignVisitor(astutils._NodeNameCollector): - - def __init__(self): - super(_SOAAssignVisitor, self).__init__() - self.nodes = [] - - def _added(self, node, levels): - if isinstance(node, rope.base.ast.Subscript) and \ - isinstance(node.slice, rope.base.ast.Index): - self.nodes.append((node, levels)) diff --git a/pythonFiles/rope/base/oi/soi.py b/pythonFiles/rope/base/oi/soi.py deleted file mode 100644 index f39e6a8a38fa..000000000000 --- a/pythonFiles/rope/base/oi/soi.py +++ /dev/null @@ -1,243 +0,0 @@ -"""A module for inferring objects - -For more information see the documentation in `rope.base.oi` -package. - -""" -import rope.base.builtins -import rope.base.pynames -import rope.base.pyobjects -from rope.base import evaluate, utils, arguments -from rope.base.oi.docstrings import hint_return, hint_param, hint_attr, hint_pep0484 - - -_ignore_inferred = utils.ignore_exception( - rope.base.pyobjects.IsBeingInferredError) - - -@_ignore_inferred -def infer_returned_object(pyfunction, args): - """Infer the `PyObject` this `PyFunction` returns after calling""" - object_info = pyfunction.pycore.object_info - result = object_info.get_exact_returned(pyfunction, args) - if result is not None: - return result - result = _infer_returned(pyfunction, args) - if result is not None: - if args and pyfunction.get_module().get_resource() is not None: - params = args.get_arguments( - pyfunction.get_param_names(special_args=False)) - object_info.function_called(pyfunction, params, result) - return result - result = object_info.get_returned(pyfunction, args) - if result is not None: - return result - type_ = hint_return(pyfunction) - if type_ is not None: - return rope.base.pyobjects.PyObject(type_) - - -@_ignore_inferred -def infer_parameter_objects(pyfunction): - """Infer the `PyObject`\s of parameters of this `PyFunction`""" - object_info = pyfunction.pycore.object_info - result = object_info.get_parameter_objects(pyfunction) - if result is None: - result = _parameter_objects(pyfunction) - _handle_first_parameter(pyfunction, result) - return result - - -def _handle_first_parameter(pyobject, parameters): - kind = pyobject.get_kind() - if parameters is None or kind not in ['method', 'classmethod']: - pass - if not parameters: - if not pyobject.get_param_names(special_args=False): - return - parameters.append(rope.base.pyobjects.get_unknown()) - if kind == 'method': - parameters[0] = rope.base.pyobjects.PyObject(pyobject.parent) - if kind == 'classmethod': - parameters[0] = pyobject.parent - - -@_ignore_inferred -def infer_assigned_object(pyname): - if not pyname.assignments: - return - for assignment in reversed(pyname.assignments): - result = _infer_assignment(assignment, pyname.module) - if isinstance(result, rope.base.builtins.BuiltinUnknown) and result.get_name() == 'NotImplementedType': - break - elif result == rope.base.pyobjects.get_unknown(): - break - elif result is not None: - return result - - hinting_result = hint_pep0484(pyname) - if hinting_result is not None: - return hinting_result - - hinting_result = _infer_assigned_object_by_hint(pyname) - if hinting_result is not None: - return hinting_result - - return result - - -def _infer_assigned_object_by_hint(pyname): - lineno = _get_lineno_for_node(pyname.assignments[0].ast_node) - holding_scope = pyname.module.get_scope().get_inner_scope_for_line(lineno) - pyobject = holding_scope.pyobject - if isinstance(pyobject, rope.base.pyobjects.PyClass): - pyclass = pyobject - elif (isinstance(pyobject, rope.base.pyobjectsdef.PyFunction) and - isinstance(pyobject.parent, rope.base.pyobjects.PyClass)): - pyclass = pyobject.parent - else: - return - for name, attr in pyclass.get_attributes().items(): - if attr is pyname: - type_ = hint_attr(pyclass, name) - if type_ is not None: - return rope.base.pyobjects.PyObject(type_) - break - - -def get_passed_objects(pyfunction, parameter_index): - object_info = pyfunction.pycore.object_info - result = object_info.get_passed_objects(pyfunction, - parameter_index) - if not result: - statically_inferred = _parameter_objects(pyfunction) - if len(statically_inferred) > parameter_index: - result.append(statically_inferred[parameter_index]) - return result - - -def _infer_returned(pyobject, args): - if args: - # HACK: Setting parameter objects manually - # This is not thread safe and might cause problems if `args` - # does not come from a good call site - pyobject.get_scope().invalidate_data() - pyobject._set_parameter_pyobjects( - args.get_arguments(pyobject.get_param_names(special_args=False))) - scope = pyobject.get_scope() - if not scope._get_returned_asts(): - return - maxtries = 3 - for returned_node in reversed(scope._get_returned_asts()[-maxtries:]): - try: - resulting_pyname = evaluate.eval_node(scope, returned_node) - if resulting_pyname is None: - continue - pyobject = resulting_pyname.get_object() - if pyobject == rope.base.pyobjects.get_unknown(): - continue - if not scope._is_generator(): - return pyobject - else: - return rope.base.builtins.get_generator(pyobject) - except rope.base.pyobjects.IsBeingInferredError: - pass - - -def _parameter_objects(pyobject): - result = [] - params = pyobject.get_param_names(special_args=False) - for name in params: - type_ = hint_param(pyobject, name) - if type_ is not None: - result.append(rope.base.pyobjects.PyObject(type_)) - else: - result.append(rope.base.pyobjects.get_unknown()) - return result - -# handling `rope.base.pynames.AssignmentValue` - - -@_ignore_inferred -def _infer_assignment(assignment, pymodule): - result = _follow_pyname(assignment, pymodule) - if result is None: - return None - pyname, pyobject = result - pyobject = _follow_evaluations(assignment, pyname, pyobject) - if pyobject is None: - return None - return _follow_levels(assignment, pyobject) - - -def _follow_levels(assignment, pyobject): - for index in assignment.levels: - if isinstance(pyobject.get_type(), rope.base.builtins.Tuple): - holdings = pyobject.get_type().get_holding_objects() - if holdings: - pyobject = holdings[min(len(holdings) - 1, index)] - else: - pyobject = None - elif isinstance(pyobject.get_type(), rope.base.builtins.List): - pyobject = pyobject.get_type().holding - else: - pyobject = None - if pyobject is None: - break - return pyobject - - -@_ignore_inferred -def _follow_pyname(assignment, pymodule, lineno=None): - assign_node = assignment.ast_node - if lineno is None: - lineno = _get_lineno_for_node(assign_node) - holding_scope = pymodule.get_scope().get_inner_scope_for_line(lineno) - pyname = evaluate.eval_node(holding_scope, assign_node) - if pyname is not None: - result = pyname.get_object() - if isinstance(result.get_type(), rope.base.builtins.Property) and \ - holding_scope.get_kind() == 'Class': - arg = rope.base.pynames.UnboundName( - rope.base.pyobjects.PyObject(holding_scope.pyobject)) - return pyname, result.get_type().get_property_object( - arguments.ObjectArguments([arg])) - return pyname, result - - -@_ignore_inferred -def _follow_evaluations(assignment, pyname, pyobject): - new_pyname = pyname - tokens = assignment.evaluation.split('.') - for token in tokens: - call = token.endswith('()') - if call: - token = token[:-2] - if token: - pyname = new_pyname - new_pyname = _get_attribute(pyobject, token) - if new_pyname is not None: - pyobject = new_pyname.get_object() - if pyobject is not None and call: - if isinstance(pyobject, rope.base.pyobjects.AbstractFunction): - args = arguments.ObjectArguments([pyname]) - pyobject = pyobject.get_returned_object(args) - else: - pyobject = None - if pyobject is None: - break - if pyobject is not None and assignment.assign_type: - return rope.base.pyobjects.PyObject(pyobject) - return pyobject - - -def _get_lineno_for_node(assign_node): - if hasattr(assign_node, 'lineno') and \ - assign_node.lineno is not None: - return assign_node.lineno - return 1 - - -def _get_attribute(pyobject, name): - if pyobject is not None and name in pyobject: - return pyobject[name] diff --git a/pythonFiles/rope/base/oi/transform.py b/pythonFiles/rope/base/oi/transform.py deleted file mode 100644 index aa29c373d5cb..000000000000 --- a/pythonFiles/rope/base/oi/transform.py +++ /dev/null @@ -1,285 +0,0 @@ -"""Provides classes for persisting `PyObject`\s""" -import os -import re - -import rope.base.builtins -from rope.base import exceptions - - -class PyObjectToTextual(object): - """For transforming `PyObject` to textual form - - This can be used for storing `PyObjects` in files. Use - `TextualToPyObject` for converting back. - - """ - - def __init__(self, project): - self.project = project - - def transform(self, pyobject): - """Transform a `PyObject` to textual form""" - if pyobject is None: - return ('none',) - object_type = type(pyobject) - try: - method = getattr(self, object_type.__name__ + '_to_textual') - return method(pyobject) - except AttributeError: - return ('unknown',) - - def __call__(self, pyobject): - return self.transform(pyobject) - - def PyObject_to_textual(self, pyobject): - if isinstance(pyobject.get_type(), rope.base.pyobjects.AbstractClass): - result = self.transform(pyobject.get_type()) - if result[0] == 'defined': - return ('instance', result) - return result - return ('unknown',) - - def PyFunction_to_textual(self, pyobject): - return self._defined_to_textual(pyobject) - - def PyClass_to_textual(self, pyobject): - return self._defined_to_textual(pyobject) - - def _defined_to_textual(self, pyobject): - address = [] - while pyobject.parent is not None: - address.insert(0, pyobject.get_name()) - pyobject = pyobject.parent - return ('defined', self._get_pymodule_path(pyobject.get_module()), - '.'.join(address)) - - def PyModule_to_textual(self, pyobject): - return ('defined', self._get_pymodule_path(pyobject)) - - def PyPackage_to_textual(self, pyobject): - return ('defined', self._get_pymodule_path(pyobject)) - - def List_to_textual(self, pyobject): - return ('builtin', 'list', self.transform(pyobject.holding)) - - def Dict_to_textual(self, pyobject): - return ('builtin', 'dict', self.transform(pyobject.keys), - self.transform(pyobject.values)) - - def Tuple_to_textual(self, pyobject): - objects = [self.transform(holding) - for holding in pyobject.get_holding_objects()] - return tuple(['builtin', 'tuple'] + objects) - - def Set_to_textual(self, pyobject): - return ('builtin', 'set', self.transform(pyobject.holding)) - - def Iterator_to_textual(self, pyobject): - return ('builtin', 'iter', self.transform(pyobject.holding)) - - def Generator_to_textual(self, pyobject): - return ('builtin', 'generator', self.transform(pyobject.holding)) - - def Str_to_textual(self, pyobject): - return ('builtin', 'str') - - def File_to_textual(self, pyobject): - return ('builtin', 'file') - - def BuiltinFunction_to_textual(self, pyobject): - return ('builtin', 'function', pyobject.get_name()) - - def _get_pymodule_path(self, pymodule): - return self.resource_to_path(pymodule.get_resource()) - - def resource_to_path(self, resource): - if resource.project == self.project: - return resource.path - else: - return resource.real_path - - -class TextualToPyObject(object): - """For transforming textual form to `PyObject`""" - - def __init__(self, project, allow_in_project_absolutes=False): - self.project = project - - def __call__(self, textual): - return self.transform(textual) - - def transform(self, textual): - """Transform an object from textual form to `PyObject`""" - if textual is None: - return None - type = textual[0] - try: - method = getattr(self, type + '_to_pyobject') - return method(textual) - except AttributeError: - return None - - def builtin_to_pyobject(self, textual): - method = getattr(self, 'builtin_%s_to_pyobject' % textual[1], None) - if method is not None: - return method(textual) - - def builtin_str_to_pyobject(self, textual): - return rope.base.builtins.get_str() - - def builtin_list_to_pyobject(self, textual): - holding = self.transform(textual[2]) - return rope.base.builtins.get_list(holding) - - def builtin_dict_to_pyobject(self, textual): - keys = self.transform(textual[2]) - values = self.transform(textual[3]) - return rope.base.builtins.get_dict(keys, values) - - def builtin_tuple_to_pyobject(self, textual): - objects = [] - for holding in textual[2:]: - objects.append(self.transform(holding)) - return rope.base.builtins.get_tuple(*objects) - - def builtin_set_to_pyobject(self, textual): - holding = self.transform(textual[2]) - return rope.base.builtins.get_set(holding) - - def builtin_iter_to_pyobject(self, textual): - holding = self.transform(textual[2]) - return rope.base.builtins.get_iterator(holding) - - def builtin_generator_to_pyobject(self, textual): - holding = self.transform(textual[2]) - return rope.base.builtins.get_generator(holding) - - def builtin_file_to_pyobject(self, textual): - return rope.base.builtins.get_file() - - def builtin_function_to_pyobject(self, textual): - if textual[2] in rope.base.builtins.builtins: - return rope.base.builtins.builtins[textual[2]].get_object() - - def unknown_to_pyobject(self, textual): - return None - - def none_to_pyobject(self, textual): - return None - - def _module_to_pyobject(self, textual): - path = textual[1] - return self._get_pymodule(path) - - def _hierarchical_defined_to_pyobject(self, textual): - path = textual[1] - names = textual[2].split('.') - pymodule = self._get_pymodule(path) - pyobject = pymodule - for name in names: - if pyobject is None: - return None - if isinstance(pyobject, rope.base.pyobjects.PyDefinedObject): - try: - pyobject = pyobject.get_scope()[name].get_object() - except exceptions.NameNotFoundError: - return None - else: - return None - return pyobject - - def defined_to_pyobject(self, textual): - if len(textual) == 2 or textual[2] == '': - return self._module_to_pyobject(textual) - else: - return self._hierarchical_defined_to_pyobject(textual) - - def instance_to_pyobject(self, textual): - type = self.transform(textual[1]) - if type is not None: - return rope.base.pyobjects.PyObject(type) - - def _get_pymodule(self, path): - resource = self.path_to_resource(path) - if resource is not None: - return self.project.get_pymodule(resource) - - def path_to_resource(self, path): - try: - root = self.project.address - if not os.path.isabs(path): - return self.project.get_resource(path) - if path == root or path.startswith(root + os.sep): - # INFO: This is a project file; should not be absolute - return None - import rope.base.project - return rope.base.project.get_no_project().get_resource(path) - except exceptions.ResourceNotFoundError: - return None - - -class DOITextualToPyObject(TextualToPyObject): - """For transforming textual form to `PyObject` - - The textual form DOI uses is different from rope's standard - textual form. The reason is that we cannot find the needed - information by analyzing live objects. This class can be - used to transform DOI textual form to `PyObject` and later - we can convert it to standard textual form using - `TextualToPyObject` class. - - """ - - def _function_to_pyobject(self, textual): - path = textual[1] - lineno = int(textual[2]) - pymodule = self._get_pymodule(path) - if pymodule is not None: - scope = pymodule.get_scope() - inner_scope = scope.get_inner_scope_for_line(lineno) - return inner_scope.pyobject - - def _class_to_pyobject(self, textual): - path, name = textual[1:] - pymodule = self._get_pymodule(path) - if pymodule is None: - return None - module_scope = pymodule.get_scope() - suspected = None - if name in module_scope.get_names(): - suspected = module_scope[name].get_object() - if suspected is not None and \ - isinstance(suspected, rope.base.pyobjects.PyClass): - return suspected - else: - lineno = self._find_occurrence(name, - pymodule.get_resource().read()) - if lineno is not None: - inner_scope = module_scope.get_inner_scope_for_line(lineno) - return inner_scope.pyobject - - def defined_to_pyobject(self, textual): - if len(textual) == 2: - return self._module_to_pyobject(textual) - else: - if textual[2].isdigit(): - result = self._function_to_pyobject(textual) - else: - result = self._class_to_pyobject(textual) - if not isinstance(result, rope.base.pyobjects.PyModule): - return result - - def _find_occurrence(self, name, source): - pattern = re.compile(r'^\s*class\s*' + name + r'\b') - lines = source.split('\n') - for i in range(len(lines)): - if pattern.match(lines[i]): - return i + 1 - - def path_to_resource(self, path): - import rope.base.libutils - relpath = rope.base.libutils.path_relative_to_project_root( - self.project, path) - if relpath is not None: - path = relpath - return super(DOITextualToPyObject, self).path_to_resource(path) diff --git a/pythonFiles/rope/base/prefs.py b/pythonFiles/rope/base/prefs.py deleted file mode 100644 index 2ab45dac541d..000000000000 --- a/pythonFiles/rope/base/prefs.py +++ /dev/null @@ -1,41 +0,0 @@ -class Prefs(object): - - def __init__(self): - self.prefs = {} - self.callbacks = {} - - def set(self, key, value): - """Set the value of `key` preference to `value`.""" - if key in self.callbacks: - self.callbacks[key](value) - else: - self.prefs[key] = value - - def add(self, key, value): - """Add an entry to a list preference - - Add `value` to the list of entries for the `key` preference. - - """ - if not key in self.prefs: - self.prefs[key] = [] - self.prefs[key].append(value) - - def get(self, key, default=None): - """Get the value of the key preference""" - return self.prefs.get(key, default) - - def add_callback(self, key, callback): - """Add `key` preference with `callback` function - - Whenever `key` is set the callback is called with the - given `value` as parameter. - - """ - self.callbacks[key] = callback - - def __setitem__(self, key, value): - self.set(key, value) - - def __getitem__(self, key): - return self.get(key) diff --git a/pythonFiles/rope/base/project.py b/pythonFiles/rope/base/project.py deleted file mode 100644 index 2feef36c6825..000000000000 --- a/pythonFiles/rope/base/project.py +++ /dev/null @@ -1,491 +0,0 @@ -import os -import shutil -import sys -import warnings - -import rope.base.fscommands -import rope.base.resourceobserver as resourceobserver -import rope.base.utils.pycompat as pycompat -from rope.base import exceptions, taskhandle, prefs, history, pycore, utils -from rope.base.exceptions import ModuleNotFoundError -from rope.base.resources import File, Folder, _ResourceMatcher - -try: - import pickle -except ImportError: - import cPickle as pickle - - -class _Project(object): - - def __init__(self, fscommands): - self.observers = [] - self.fscommands = fscommands - self.prefs = prefs.Prefs() - self.data_files = _DataFiles(self) - self._custom_source_folders = [] - - def get_resource(self, resource_name): - """Get a resource in a project. - - `resource_name` is the path of a resource in a project. It is - the path of a resource relative to project root. Project root - folder address is an empty string. If the resource does not - exist a `exceptions.ResourceNotFound` exception would be - raised. Use `get_file()` and `get_folder()` when you need to - get nonexistent `Resource`\s. - - """ - path = self._get_resource_path(resource_name) - if not os.path.exists(path): - raise exceptions.ResourceNotFoundError( - 'Resource <%s> does not exist' % resource_name) - elif os.path.isfile(path): - return File(self, resource_name) - elif os.path.isdir(path): - return Folder(self, resource_name) - else: - raise exceptions.ResourceNotFoundError('Unknown resource ' - + resource_name) - - def get_module(self, name, folder=None): - """Returns a `PyObject` if the module was found.""" - # check if this is a builtin module - pymod = self.pycore.builtin_module(name) - if pymod is not None: - return pymod - module = self.find_module(name, folder) - if module is None: - raise ModuleNotFoundError('Module %s not found' % name) - return self.pycore.resource_to_pyobject(module) - - def get_python_path_folders(self): - result = [] - for src in self.prefs.get('python_path', []) + sys.path: - try: - src_folder = get_no_project().get_resource(src) - result.append(src_folder) - except exceptions.ResourceNotFoundError: - pass - return result - - # INFO: It was decided not to cache source folders, since: - # - Does not take much time when the root folder contains - # packages, that is most of the time - # - We need a separate resource observer; `self.observer` - # does not get notified about module and folder creations - def get_source_folders(self): - """Returns project source folders""" - if self.root is None: - return [] - result = list(self._custom_source_folders) - result.extend(self.pycore._find_source_folders(self.root)) - return result - - def validate(self, folder): - """Validate files and folders contained in this folder - - It validates all of the files and folders contained in this - folder if some observers are interested in them. - - """ - for observer in list(self.observers): - observer.validate(folder) - - def add_observer(self, observer): - """Register a `ResourceObserver` - - See `FilteredResourceObserver`. - """ - self.observers.append(observer) - - def remove_observer(self, observer): - """Remove a registered `ResourceObserver`""" - if observer in self.observers: - self.observers.remove(observer) - - def do(self, changes, task_handle=taskhandle.NullTaskHandle()): - """Apply the changes in a `ChangeSet` - - Most of the time you call this function for committing the - changes for a refactoring. - """ - self.history.do(changes, task_handle=task_handle) - - def get_pymodule(self, resource, force_errors=False): - return self.pycore.resource_to_pyobject(resource, force_errors) - - def get_pycore(self): - return self.pycore - - def get_file(self, path): - """Get the file with `path` (it may not exist)""" - return File(self, path) - - def get_folder(self, path): - """Get the folder with `path` (it may not exist)""" - return Folder(self, path) - - def get_prefs(self): - return self.prefs - - def get_relative_module(self, name, folder, level): - module = self.find_relative_module(name, folder, level) - if module is None: - raise ModuleNotFoundError('Module %s not found' % name) - return self.pycore.resource_to_pyobject(module) - - def find_module(self, modname, folder=None): - """Returns a resource corresponding to the given module - - returns None if it can not be found - """ - for src in self.get_source_folders(): - module = _find_module_in_folder(src, modname) - if module is not None: - return module - for src in self.get_python_path_folders(): - module = _find_module_in_folder(src, modname) - if module is not None: - return module - if folder is not None: - module = _find_module_in_folder(folder, modname) - if module is not None: - return module - return None - - def find_relative_module(self, modname, folder, level): - for i in range(level - 1): - folder = folder.parent - if modname == '': - return folder - else: - return _find_module_in_folder(folder, modname) - - def is_ignored(self, resource): - return False - - def _get_resource_path(self, name): - pass - - @property - @utils.saveit - def history(self): - return history.History(self) - - @property - @utils.saveit - def pycore(self): - return pycore.PyCore(self) - - def close(self): - warnings.warn('Cannot close a NoProject', - DeprecationWarning, stacklevel=2) - - ropefolder = None - - -class Project(_Project): - """A Project containing files and folders""" - - def __init__(self, projectroot, fscommands=None, - ropefolder='.ropeproject', **prefs): - """A rope project - - :parameters: - - `projectroot`: The address of the root folder of the project - - `fscommands`: Implements the file system operations used - by rope; have a look at `rope.base.fscommands` - - `ropefolder`: The name of the folder in which rope stores - project configurations and data. Pass `None` for not using - such a folder at all. - - `prefs`: Specify project preferences. These values - overwrite config file preferences. - - """ - if projectroot != '/': - projectroot = _realpath(projectroot).rstrip('/\\') - self._address = projectroot - self._ropefolder_name = ropefolder - if not os.path.exists(self._address): - os.mkdir(self._address) - elif not os.path.isdir(self._address): - raise exceptions.RopeError('Project root exists and' - ' is not a directory') - if fscommands is None: - fscommands = rope.base.fscommands.create_fscommands(self._address) - super(Project, self).__init__(fscommands) - self.ignored = _ResourceMatcher() - self.file_list = _FileListCacher(self) - self.prefs.add_callback('ignored_resources', self.ignored.set_patterns) - if ropefolder is not None: - self.prefs['ignored_resources'] = [ropefolder] - self._init_prefs(prefs) - self._init_source_folders() - - @utils.deprecated('Delete once deprecated functions are gone') - def _init_source_folders(self): - for path in self.prefs.get('source_folders', []): - folder = self.get_resource(path) - self._custom_source_folders.append(folder) - - def get_files(self): - return self.file_list.get_files() - - def get_python_files(self): - """Returns all python files available in the project""" - return [resource for resource in self.get_files() - if self.pycore.is_python_file(resource)] - - def _get_resource_path(self, name): - return os.path.join(self._address, *name.split('/')) - - def _init_ropefolder(self): - if self.ropefolder is not None: - if not self.ropefolder.exists(): - self._create_recursively(self.ropefolder) - if not self.ropefolder.has_child('config.py'): - config = self.ropefolder.create_file('config.py') - config.write(self._default_config()) - - def _create_recursively(self, folder): - if folder.parent != self.root and not folder.parent.exists(): - self._create_recursively(folder.parent) - folder.create() - - def _init_prefs(self, prefs): - run_globals = {} - if self.ropefolder is not None: - config = self.get_file(self.ropefolder.path + '/config.py') - run_globals.update({'__name__': '__main__', - '__builtins__': __builtins__, - '__file__': config.real_path}) - if config.exists(): - config = self.ropefolder.get_child('config.py') - pycompat.execfile(config.real_path, run_globals) - else: - exec(self._default_config(), run_globals) - if 'set_prefs' in run_globals: - run_globals['set_prefs'](self.prefs) - for key, value in prefs.items(): - self.prefs[key] = value - self._init_other_parts() - self._init_ropefolder() - if 'project_opened' in run_globals: - run_globals['project_opened'](self) - - def _default_config(self): - import rope.base.default_config - import inspect - return inspect.getsource(rope.base.default_config) - - def _init_other_parts(self): - # Forcing the creation of `self.pycore` to register observers - self.pycore - - def is_ignored(self, resource): - return self.ignored.does_match(resource) - - def sync(self): - """Closes project open resources""" - self.close() - - def close(self): - """Closes project open resources""" - self.data_files.write() - - def set(self, key, value): - """Set the `key` preference to `value`""" - self.prefs.set(key, value) - - @property - def ropefolder(self): - if self._ropefolder_name is not None: - return self.get_folder(self._ropefolder_name) - - def validate(self, folder=None): - if folder is None: - folder = self.root - super(Project, self).validate(folder) - - root = property(lambda self: self.get_resource('')) - address = property(lambda self: self._address) - - -class NoProject(_Project): - """A null object for holding out of project files. - - This class is singleton use `get_no_project` global function - """ - - def __init__(self): - fscommands = rope.base.fscommands.FileSystemCommands() - super(NoProject, self).__init__(fscommands) - - def _get_resource_path(self, name): - real_name = name.replace('/', os.path.sep) - return _realpath(real_name) - - def get_resource(self, name): - universal_name = _realpath(name).replace(os.path.sep, '/') - return super(NoProject, self).get_resource(universal_name) - - def get_files(self): - return [] - - def get_python_files(self): - return [] - - _no_project = None - - -def get_no_project(): - if NoProject._no_project is None: - NoProject._no_project = NoProject() - return NoProject._no_project - - -class _FileListCacher(object): - - def __init__(self, project): - self.project = project - self.files = None - rawobserver = resourceobserver.ResourceObserver( - self._changed, self._invalid, self._invalid, - self._invalid, self._invalid) - self.project.add_observer(rawobserver) - - def get_files(self): - if self.files is None: - self.files = set() - self._add_files(self.project.root) - return self.files - - def _add_files(self, folder): - for child in folder.get_children(): - if child.is_folder(): - self._add_files(child) - elif not self.project.is_ignored(child): - self.files.add(child) - - def _changed(self, resource): - if resource.is_folder(): - self.files = None - - def _invalid(self, resource, new_resource=None): - self.files = None - - -class _DataFiles(object): - - def __init__(self, project): - self.project = project - self.hooks = [] - - def read_data(self, name, compress=False, import_=False): - if self.project.ropefolder is None: - return None - compress = compress and self._can_compress() - opener = self._get_opener(compress) - file = self._get_file(name, compress) - if not compress and import_: - self._import_old_files(name) - if file.exists(): - input = opener(file.real_path, 'rb') - try: - result = [] - try: - while True: - result.append(pickle.load(input)) - except EOFError: - pass - if len(result) == 1: - return result[0] - if len(result) > 1: - return result - finally: - input.close() - - def write_data(self, name, data, compress=False): - if self.project.ropefolder is not None: - compress = compress and self._can_compress() - file = self._get_file(name, compress) - opener = self._get_opener(compress) - output = opener(file.real_path, 'wb') - try: - pickle.dump(data, output, 2) - finally: - output.close() - - def add_write_hook(self, hook): - self.hooks.append(hook) - - def write(self): - for hook in self.hooks: - hook() - - def _can_compress(self): - try: - import gzip # noqa - return True - except ImportError: - return False - - def _import_old_files(self, name): - old = self._get_file(name + '.pickle', False) - new = self._get_file(name, False) - if old.exists() and not new.exists(): - shutil.move(old.real_path, new.real_path) - - def _get_opener(self, compress): - if compress: - try: - import gzip - return gzip.open - except ImportError: - pass - return open - - def _get_file(self, name, compress): - path = self.project.ropefolder.path + '/' + name - if compress: - path += '.gz' - return self.project.get_file(path) - - -def _realpath(path): - """Return the real path of `path` - - Is equivalent to ``realpath(abspath(expanduser(path)))``. - - Of the particular notice is the hack dealing with the unfortunate - sitaution of running native-Windows python (os.name == 'nt') inside - of Cygwin (abspath starts with '/'), which apparently normal - os.path.realpath completely messes up. - - """ - # there is a bug in cygwin for os.path.abspath() for abs paths - if sys.platform == 'cygwin': - if path[1:3] == ':\\': - return path - elif path[1:3] == ':/': - path = "/cygdrive/" + path[0] + path[2:] - return os.path.abspath(os.path.expanduser(path)) - return os.path.realpath(os.path.abspath(os.path.expanduser(path))) - - -def _find_module_in_folder(folder, modname): - module = folder - packages = modname.split('.') - for pkg in packages[:-1]: - if module.is_folder() and module.has_child(pkg): - module = module.get_child(pkg) - else: - return None - if module.is_folder(): - if module.has_child(packages[-1]) and \ - module.get_child(packages[-1]).is_folder(): - return module.get_child(packages[-1]) - elif module.has_child(packages[-1] + '.py') and \ - not module.get_child(packages[-1] + '.py').is_folder(): - return module.get_child(packages[-1] + '.py') diff --git a/pythonFiles/rope/base/pycore.py b/pythonFiles/rope/base/pycore.py deleted file mode 100644 index c4c1195a4d7e..000000000000 --- a/pythonFiles/rope/base/pycore.py +++ /dev/null @@ -1,346 +0,0 @@ -import bisect -import difflib -import sys -import warnings - -import rope.base.libutils -import rope.base.resourceobserver -import rope.base.resources -import rope.base.oi.doa -import rope.base.oi.objectinfo -import rope.base.oi.soa -from rope.base import builtins -from rope.base import exceptions -from rope.base import stdmods -from rope.base import taskhandle -from rope.base import utils -from rope.base.exceptions import ModuleNotFoundError -from rope.base.pyobjectsdef import PyModule, PyPackage - - -class PyCore(object): - - def __init__(self, project): - self.project = project - self._init_resource_observer() - self.cache_observers = [] - self.module_cache = _ModuleCache(self) - self.extension_cache = _ExtensionCache(self) - self.object_info = rope.base.oi.objectinfo.ObjectInfoManager(project) - self._init_python_files() - self._init_automatic_soa() - - def _init_python_files(self): - self.python_matcher = None - patterns = self.project.prefs.get('python_files', None) - if patterns is not None: - self.python_matcher = rope.base.resources._ResourceMatcher() - self.python_matcher.set_patterns(patterns) - - def _init_resource_observer(self): - callback = self._invalidate_resource_cache - observer = rope.base.resourceobserver.ResourceObserver( - changed=callback, moved=callback, removed=callback) - self.observer = \ - rope.base.resourceobserver.FilteredResourceObserver(observer) - self.project.add_observer(self.observer) - - def _init_automatic_soa(self): - if not self.automatic_soa: - return - callback = self._file_changed_for_soa - observer = rope.base.resourceobserver.ResourceObserver( - changed=callback, moved=callback, removed=callback) - self.project.add_observer(observer) - - @property - def automatic_soa(self): - auto_soa = self.project.prefs.get('automatic_soi', None) - return self.project.prefs.get('automatic_soa', auto_soa) - - def _file_changed_for_soa(self, resource, new_resource=None): - old_contents = self.project.history.\ - contents_before_current_change(resource) - if old_contents is not None: - perform_soa_on_changed_scopes(self.project, resource, old_contents) - - def is_python_file(self, resource): - if resource.is_folder(): - return False - if self.python_matcher is None: - return resource.name.endswith('.py') - return self.python_matcher.does_match(resource) - - @utils.deprecated('Use `project.get_module` instead') - def get_module(self, name, folder=None): - """Returns a `PyObject` if the module was found.""" - return self.project.get_module(name, folder) - - def _builtin_submodules(self, modname): - result = {} - for extension in self.extension_modules: - if extension.startswith(modname + '.'): - name = extension[len(modname) + 1:] - if '.' not in name: - result[name] = self.builtin_module(extension) - return result - - def builtin_module(self, name): - return self.extension_cache.get_pymodule(name) - - @utils.deprecated('Use `project.get_relative_module` instead') - def get_relative_module(self, name, folder, level): - return self.project.get_relative_module(name, folder, level) - - @utils.deprecated('Use `libutils.get_string_module` instead') - def get_string_module(self, code, resource=None, force_errors=False): - """Returns a `PyObject` object for the given code - - If `force_errors` is `True`, `exceptions.ModuleSyntaxError` is - raised if module has syntax errors. This overrides - ``ignore_syntax_errors`` project config. - - """ - return PyModule(self, code, resource, force_errors=force_errors) - - @utils.deprecated('Use `libutils.get_string_scope` instead') - def get_string_scope(self, code, resource=None): - """Returns a `Scope` object for the given code""" - return rope.base.libutils.get_string_scope(code, resource) - - def _invalidate_resource_cache(self, resource, new_resource=None): - for observer in self.cache_observers: - observer(resource) - - @utils.deprecated('Use `project.get_python_path_folders` instead') - def get_python_path_folders(self): - return self.project.get_python_path_folders() - - @utils.deprecated('Use `project.find_module` instead') - def find_module(self, modname, folder=None): - """Returns a resource corresponding to the given module - - returns None if it can not be found - """ - return self.project.find_module(modname, folder) - - @utils.deprecated('Use `project.find_relative_module` instead') - def find_relative_module(self, modname, folder, level): - return self.project.find_relative_module(modname, folder, level) - - # INFO: It was decided not to cache source folders, since: - # - Does not take much time when the root folder contains - # packages, that is most of the time - # - We need a separate resource observer; `self.observer` - # does not get notified about module and folder creations - @utils.deprecated('Use `project.get_source_folders` instead') - def get_source_folders(self): - """Returns project source folders""" - return self.project.get_source_folders() - - def resource_to_pyobject(self, resource, force_errors=False): - return self.module_cache.get_pymodule(resource, force_errors) - - @utils.deprecated('Use `project.get_python_files` instead') - def get_python_files(self): - """Returns all python files available in the project""" - return self.project.get_python_files() - - def _is_package(self, folder): - if folder.has_child('__init__.py') and \ - not folder.get_child('__init__.py').is_folder(): - return True - else: - return False - - def _find_source_folders(self, folder): - for resource in folder.get_folders(): - if self._is_package(resource): - return [folder] - result = [] - for resource in folder.get_files(): - if resource.name.endswith('.py'): - result.append(folder) - break - for resource in folder.get_folders(): - result.extend(self._find_source_folders(resource)) - return result - - def run_module(self, resource, args=None, stdin=None, stdout=None): - """Run `resource` module - - Returns a `rope.base.oi.doa.PythonFileRunner` object for - controlling the process. - - """ - perform_doa = self.project.prefs.get('perform_doi', True) - perform_doa = self.project.prefs.get('perform_doa', perform_doa) - receiver = self.object_info.doa_data_received - if not perform_doa: - receiver = None - runner = rope.base.oi.doa.PythonFileRunner( - self, resource, args, stdin, stdout, receiver) - runner.add_finishing_observer(self.module_cache.forget_all_data) - runner.run() - return runner - - def analyze_module(self, resource, should_analyze=lambda py: True, - search_subscopes=lambda py: True, followed_calls=None): - """Analyze `resource` module for static object inference - - This function forces rope to analyze this module to collect - information about function calls. `should_analyze` is a - function that is called with a `PyDefinedObject` argument. If - it returns `True` the element is analyzed. If it is `None` or - returns `False` the element is not analyzed. - - `search_subscopes` is like `should_analyze`; The difference is - that if it returns `False` the sub-scopes are all ignored. - That is it is assumed that `should_analyze` returns `False` - for all of its subscopes. - - `followed_calls` override the value of ``soa_followed_calls`` - project config. - """ - if followed_calls is None: - followed_calls = self.project.prefs.get('soa_followed_calls', 0) - pymodule = self.resource_to_pyobject(resource) - self.module_cache.forget_all_data() - rope.base.oi.soa.analyze_module( - self, pymodule, should_analyze, search_subscopes, followed_calls) - - def get_classes(self, task_handle=taskhandle.NullTaskHandle()): - warnings.warn('`PyCore.get_classes()` is deprecated', - DeprecationWarning, stacklevel=2) - return [] - - def __str__(self): - return str(self.module_cache) + str(self.object_info) - - @utils.deprecated('Use `libutils.modname` instead') - def modname(self, resource): - return rope.base.libutils.modname(resource) - - @property - @utils.cacheit - def extension_modules(self): - result = set(self.project.prefs.get('extension_modules', [])) - if self.project.prefs.get('import_dynload_stdmods', False): - result.update(stdmods.dynload_modules()) - return result - - -class _ModuleCache(object): - - def __init__(self, pycore): - self.pycore = pycore - self.module_map = {} - self.pycore.cache_observers.append(self._invalidate_resource) - self.observer = self.pycore.observer - - def _invalidate_resource(self, resource): - if resource in self.module_map: - self.forget_all_data() - self.observer.remove_resource(resource) - del self.module_map[resource] - - def get_pymodule(self, resource, force_errors=False): - if resource in self.module_map: - return self.module_map[resource] - if resource.is_folder(): - result = PyPackage(self.pycore, resource, - force_errors=force_errors) - else: - result = PyModule(self.pycore, resource=resource, - force_errors=force_errors) - if result.has_errors: - return result - self.module_map[resource] = result - self.observer.add_resource(resource) - return result - - def forget_all_data(self): - for pymodule in self.module_map.values(): - pymodule._forget_concluded_data() - - def __str__(self): - return 'PyCore caches %d PyModules\n' % len(self.module_map) - - -class _ExtensionCache(object): - - def __init__(self, pycore): - self.pycore = pycore - self.extensions = {} - - def get_pymodule(self, name): - if name == '__builtin__': - return builtins.builtins - allowed = self.pycore.extension_modules - if name not in self.extensions and name in allowed: - self.extensions[name] = builtins.BuiltinModule(name, self.pycore) - return self.extensions.get(name) - - -def perform_soa_on_changed_scopes(project, resource, old_contents): - pycore = project.pycore - if resource.exists() and pycore.is_python_file(resource): - try: - new_contents = resource.read() - # detecting changes in new_contents relative to old_contents - detector = _TextChangeDetector(new_contents, old_contents) - - def search_subscopes(pydefined): - scope = pydefined.get_scope() - return detector.is_changed(scope.get_start(), scope.get_end()) - - def should_analyze(pydefined): - scope = pydefined.get_scope() - start = scope.get_start() - end = scope.get_end() - return detector.consume_changes(start, end) - pycore.analyze_module(resource, should_analyze, search_subscopes) - except exceptions.ModuleSyntaxError: - pass - - -class _TextChangeDetector(object): - - def __init__(self, old, new): - self.old = old - self.new = new - self._set_diffs() - - def _set_diffs(self): - differ = difflib.Differ() - self.lines = [] - lineno = 0 - for line in differ.compare(self.old.splitlines(True), - self.new.splitlines(True)): - if line.startswith(' '): - lineno += 1 - elif line.startswith('-'): - lineno += 1 - self.lines.append(lineno) - - def is_changed(self, start, end): - """Tell whether any of start till end lines have changed - - The end points are inclusive and indices start from 1. - """ - left, right = self._get_changed(start, end) - if left < right: - return True - return False - - def consume_changes(self, start, end): - """Clear the changed status of lines from start till end""" - left, right = self._get_changed(start, end) - if left < right: - del self.lines[left:right] - return left < right - - def _get_changed(self, start, end): - left = bisect.bisect_left(self.lines, start) - right = bisect.bisect_right(self.lines, end) - return left, right diff --git a/pythonFiles/rope/base/pynames.py b/pythonFiles/rope/base/pynames.py deleted file mode 100644 index 5d489814a35c..000000000000 --- a/pythonFiles/rope/base/pynames.py +++ /dev/null @@ -1,201 +0,0 @@ -import rope.base.pyobjects -from rope.base import exceptions, utils - - -class PyName(object): - """References to `PyObject`\s inside python programs""" - - def get_object(self): - """Return the `PyObject` object referenced by this `PyName`""" - - def get_definition_location(self): - """Return a (module, lineno) tuple""" - - -class DefinedName(PyName): - - def __init__(self, pyobject): - self.pyobject = pyobject - - def get_object(self): - return self.pyobject - - def get_definition_location(self): - return (self.pyobject.get_module(), self.pyobject.get_ast().lineno) - - -class AssignedName(PyName): - """Only a placeholder""" - - -class UnboundName(PyName): - - def __init__(self, pyobject=None): - self.pyobject = pyobject - if self.pyobject is None: - self.pyobject = rope.base.pyobjects.get_unknown() - - def get_object(self): - return self.pyobject - - def get_definition_location(self): - return (None, None) - - -class AssignmentValue(object): - """An assigned expression""" - - def __init__(self, ast_node, levels=None, evaluation='', - assign_type=False): - """The `level` is `None` for simple assignments and is - a list of numbers for tuple assignments for example in:: - - a, (b, c) = x - - The levels for for `a` is ``[0]``, for `b` is ``[1, 0]`` and for - `c` is ``[1, 1]``. - - """ - self.ast_node = ast_node - if levels is None: - self.levels = [] - else: - self.levels = levels - self.evaluation = evaluation - self.assign_type = assign_type - - def get_lineno(self): - return self.ast_node.lineno - - -class EvaluatedName(PyName): - """A name whose object will be evaluated later""" - - def __init__(self, callback, module=None, lineno=None): - self.module = module - self.lineno = lineno - self.callback = callback - self.pyobject = _Inferred(callback, _get_concluded_data(module)) - - def get_object(self): - return self.pyobject.get() - - def get_definition_location(self): - return (self.module, self.lineno) - - def invalidate(self): - """Forget the `PyObject` this `PyName` holds""" - self.pyobject.set(None) - - -class ParameterName(PyName): - """Only a placeholder""" - - -class ImportedModule(PyName): - - def __init__(self, importing_module, module_name=None, - level=0, resource=None): - self.importing_module = importing_module - self.module_name = module_name - self.level = level - self.resource = resource - self.pymodule = _get_concluded_data(self.importing_module) - - def _current_folder(self): - resource = self.importing_module.get_module().get_resource() - if resource is None: - return None - return resource.parent - - def _get_pymodule(self): - if self.pymodule.get() is None: - pycore = self.importing_module.pycore - if self.resource is not None: - self.pymodule.set(pycore.project.get_pymodule(self.resource)) - elif self.module_name is not None: - try: - if self.level == 0: - pymodule = pycore.project.get_module( - self.module_name, self._current_folder()) - else: - pymodule = pycore.project.get_relative_module( - self.module_name, self._current_folder(), - self.level) - self.pymodule.set(pymodule) - except exceptions.ModuleNotFoundError: - pass - return self.pymodule.get() - - def get_object(self): - if self._get_pymodule() is None: - return rope.base.pyobjects.get_unknown() - return self._get_pymodule() - - def get_definition_location(self): - pymodule = self._get_pymodule() - if not isinstance(pymodule, rope.base.pyobjects.PyDefinedObject): - return (None, None) - return (pymodule.get_module(), 1) - - -class ImportedName(PyName): - - def __init__(self, imported_module, imported_name): - self.imported_module = imported_module - self.imported_name = imported_name - - def _get_imported_pyname(self): - try: - result = self.imported_module.get_object()[self.imported_name] - if result != self: - return result - except exceptions.AttributeNotFoundError: - pass - return UnboundName() - - @utils.prevent_recursion(rope.base.pyobjects.get_unknown) - def get_object(self): - return self._get_imported_pyname().get_object() - - @utils.prevent_recursion(lambda: (None, None)) - def get_definition_location(self): - return self._get_imported_pyname().get_definition_location() - - -def _get_concluded_data(module): - if module is None: - return rope.base.pyobjects._ConcludedData() - return module._get_concluded_data() - - -def _circular_inference(): - raise rope.base.pyobjects.IsBeingInferredError( - 'Circular Object Inference') - - -class _Inferred(object): - - def __init__(self, get_inferred, concluded=None): - self.get_inferred = get_inferred - self.concluded = concluded - if self.concluded is None: - self.temp = None - - @utils.prevent_recursion(_circular_inference) - def get(self, *args, **kwds): - if self.concluded is None or self.concluded.get() is None: - self.set(self.get_inferred(*args, **kwds)) - if self._get() is None: - self.set(rope.base.pyobjects.get_unknown()) - return self._get() - - def set(self, pyobject): - if self.concluded is not None: - self.concluded.set(pyobject) - self.temp = pyobject - - def _get(self): - if self.concluded is not None: - return self.concluded.get() - return self.temp diff --git a/pythonFiles/rope/base/pynamesdef.py b/pythonFiles/rope/base/pynamesdef.py deleted file mode 100644 index 6dba0a803762..000000000000 --- a/pythonFiles/rope/base/pynamesdef.py +++ /dev/null @@ -1,55 +0,0 @@ -import rope.base.oi.soi -from rope.base import pynames -from rope.base.pynames import * - - -class AssignedName(pynames.AssignedName): - - def __init__(self, lineno=None, module=None, pyobject=None): - self.lineno = lineno - self.module = module - self.assignments = [] - self.pyobject = _Inferred(self._get_inferred, - pynames._get_concluded_data(module)) - self.pyobject.set(pyobject) - - @utils.prevent_recursion(lambda: None) - def _get_inferred(self): - if self.module is not None: - return rope.base.oi.soi.infer_assigned_object(self) - - def get_object(self): - return self.pyobject.get() - - def get_definition_location(self): - """Returns a (module, lineno) tuple""" - if self.lineno is None and self.assignments: - self.lineno = self.assignments[0].get_lineno() - return (self.module, self.lineno) - - def invalidate(self): - """Forget the `PyObject` this `PyName` holds""" - self.pyobject.set(None) - - -class ParameterName(pynames.ParameterName): - - def __init__(self, pyfunction, index): - self.pyfunction = pyfunction - self.index = index - - def get_object(self): - result = self.pyfunction.get_parameter(self.index) - if result is None: - result = rope.base.pyobjects.get_unknown() - return result - - def get_objects(self): - """Returns the list of objects passed as this parameter""" - return rope.base.oi.soi.get_passed_objects( - self.pyfunction, self.index) - - def get_definition_location(self): - return (self.pyfunction.get_module(), self.pyfunction.get_ast().lineno) - -_Inferred = pynames._Inferred diff --git a/pythonFiles/rope/base/pyobjects.py b/pythonFiles/rope/base/pyobjects.py deleted file mode 100644 index fd4d1c822245..000000000000 --- a/pythonFiles/rope/base/pyobjects.py +++ /dev/null @@ -1,311 +0,0 @@ -from rope.base.fscommands import _decode_data -from rope.base import ast, exceptions, utils - - -class PyObject(object): - - def __init__(self, type_): - if type_ is None: - type_ = self - self.type = type_ - - def get_attributes(self): - if self.type is self: - return {} - return self.type.get_attributes() - - def get_attribute(self, name): - if name not in self.get_attributes(): - raise exceptions.AttributeNotFoundError( - 'Attribute %s not found' % name) - return self.get_attributes()[name] - - def get_type(self): - return self.type - - def __getitem__(self, key): - """The same as ``get_attribute(key)``""" - return self.get_attribute(key) - - def __contains__(self, key): - """The same as ``key in self.get_attributes()``""" - return key in self.get_attributes() - - def __eq__(self, obj): - """Check the equality of two `PyObject`\s - - Currently it is assumed that instances (the direct instances - of `PyObject`, not the instances of its subclasses) are equal - if their types are equal. For every other object like - defineds or builtins rope assumes objects are reference - objects and their identities should match. - - """ - if self.__class__ != obj.__class__: - return False - if type(self) == PyObject: - if self is not self.type: - return self.type == obj.type - else: - return self.type is obj.type - return self is obj - - def __ne__(self, obj): - return not self.__eq__(obj) - - def __hash__(self): - """See docs for `__eq__()` method""" - if type(self) == PyObject and self != self.type: - return hash(self.type) + 1 - else: - return super(PyObject, self).__hash__() - - def __iter__(self): - """The same as ``iter(self.get_attributes())``""" - return iter(self.get_attributes()) - - _types = None - _unknown = None - - @staticmethod - def _get_base_type(name): - if PyObject._types is None: - PyObject._types = {} - base_type = PyObject(None) - PyObject._types['Type'] = base_type - PyObject._types['Module'] = PyObject(base_type) - PyObject._types['Function'] = PyObject(base_type) - PyObject._types['Unknown'] = PyObject(base_type) - return PyObject._types[name] - - -def get_base_type(name): - """Return the base type with name `name`. - - The base types are 'Type', 'Function', 'Module' and 'Unknown'. It - was used to check the type of a `PyObject` but currently its use - is discouraged. Use classes defined in this module instead. - For example instead of - ``pyobject.get_type() == get_base_type('Function')`` use - ``isinstance(pyobject, AbstractFunction)``. - - You can use `AbstractClass` for classes, `AbstractFunction` for - functions, and `AbstractModule` for modules. You can also use - `PyFunction` and `PyClass` for testing if an object is - defined somewhere and rope can access its source. These classes - provide more methods. - - """ - return PyObject._get_base_type(name) - - -def get_unknown(): - """Return a pyobject whose type is unknown - - Note that two unknown objects are equal. So for example you can - write:: - - if pyname.get_object() == get_unknown(): - print('cannot determine what this pyname holds') - - Rope could have used `None` for indicating unknown objects but - we had to check that in many places. So actually this method - returns a null object. - - """ - if PyObject._unknown is None: - PyObject._unknown = PyObject(get_base_type('Unknown')) - return PyObject._unknown - - -class AbstractClass(PyObject): - - def __init__(self): - super(AbstractClass, self).__init__(get_base_type('Type')) - - def get_name(self): - pass - - def get_doc(self): - pass - - def get_superclasses(self): - return [] - - -class AbstractFunction(PyObject): - - def __init__(self): - super(AbstractFunction, self).__init__(get_base_type('Function')) - - def get_name(self): - pass - - def get_doc(self): - pass - - def get_param_names(self, special_args=True): - return [] - - def get_returned_object(self, args): - return get_unknown() - - -class AbstractModule(PyObject): - - def __init__(self, doc=None): - super(AbstractModule, self).__init__(get_base_type('Module')) - - def get_doc(self): - pass - - def get_resource(self): - pass - - -class PyDefinedObject(object): - """Python defined names that rope can access their sources""" - - def __init__(self, pycore, ast_node, parent): - self.pycore = pycore - self.ast_node = ast_node - self.scope = None - self.parent = parent - self.structural_attributes = None - self.concluded_attributes = self.get_module()._get_concluded_data() - self.attributes = self.get_module()._get_concluded_data() - self.defineds = None - - visitor_class = None - - @utils.prevent_recursion(lambda: {}) - def _get_structural_attributes(self): - if self.structural_attributes is None: - self.structural_attributes = self._create_structural_attributes() - return self.structural_attributes - - @utils.prevent_recursion(lambda: {}) - def _get_concluded_attributes(self): - if self.concluded_attributes.get() is None: - self._get_structural_attributes() - self.concluded_attributes.set(self._create_concluded_attributes()) - return self.concluded_attributes.get() - - def get_attributes(self): - if self.attributes.get() is None: - result = dict(self._get_concluded_attributes()) - result.update(self._get_structural_attributes()) - self.attributes.set(result) - return self.attributes.get() - - def get_attribute(self, name): - if name in self._get_structural_attributes(): - return self._get_structural_attributes()[name] - if name in self._get_concluded_attributes(): - return self._get_concluded_attributes()[name] - raise exceptions.AttributeNotFoundError('Attribute %s not found' % - name) - - def get_scope(self): - if self.scope is None: - self.scope = self._create_scope() - return self.scope - - def get_module(self): - current_object = self - while current_object.parent is not None: - current_object = current_object.parent - return current_object - - def get_doc(self): - if len(self.get_ast().body) > 0: - expr = self.get_ast().body[0] - if isinstance(expr, ast.Expr) and \ - isinstance(expr.value, ast.Str): - docstring = expr.value.s - coding = self.get_module().coding - return _decode_data(docstring, coding) - - def _get_defined_objects(self): - if self.defineds is None: - self._get_structural_attributes() - return self.defineds - - def _create_structural_attributes(self): - if self.visitor_class is None: - return {} - new_visitor = self.visitor_class(self.pycore, self) - for child in ast.get_child_nodes(self.ast_node): - ast.walk(child, new_visitor) - self.defineds = new_visitor.defineds - return new_visitor.names - - def _create_concluded_attributes(self): - return {} - - def get_ast(self): - return self.ast_node - - def _create_scope(self): - pass - - -class PyFunction(PyDefinedObject, AbstractFunction): - """Only a placeholder""" - - -class PyClass(PyDefinedObject, AbstractClass): - """Only a placeholder""" - - -class _ConcludedData(object): - - def __init__(self): - self.data_ = None - - def set(self, data): - self.data_ = data - - def get(self): - return self.data_ - - data = property(get, set) - - def _invalidate(self): - self.data = None - - def __str__(self): - return '<' + str(self.data) + '>' - - -class _PyModule(PyDefinedObject, AbstractModule): - - def __init__(self, pycore, ast_node, resource): - self.resource = resource - self.concluded_data = [] - AbstractModule.__init__(self) - PyDefinedObject.__init__(self, pycore, ast_node, None) - - def _get_concluded_data(self): - new_data = _ConcludedData() - self.concluded_data.append(new_data) - return new_data - - def _forget_concluded_data(self): - for data in self.concluded_data: - data._invalidate() - - def get_resource(self): - return self.resource - - -class PyModule(_PyModule): - """Only a placeholder""" - - -class PyPackage(_PyModule): - """Only a placeholder""" - - -class IsBeingInferredError(exceptions.RopeError): - pass diff --git a/pythonFiles/rope/base/pyobjectsdef.py b/pythonFiles/rope/base/pyobjectsdef.py deleted file mode 100644 index b7aef6273d92..000000000000 --- a/pythonFiles/rope/base/pyobjectsdef.py +++ /dev/null @@ -1,560 +0,0 @@ -import rope.base.builtins -import rope.base.codeanalyze -import rope.base.evaluate -import rope.base.libutils -import rope.base.oi.soi -import rope.base.pyscopes -from rope.base import (pynamesdef as pynames, exceptions, ast, - astutils, pyobjects, fscommands, arguments, utils) -from rope.base.utils import pycompat - -try: - unicode -except NameError: - unicode = str - - -class PyFunction(pyobjects.PyFunction): - - def __init__(self, pycore, ast_node, parent): - rope.base.pyobjects.AbstractFunction.__init__(self) - rope.base.pyobjects.PyDefinedObject.__init__( - self, pycore, ast_node, parent) - self.arguments = self.ast_node.args - self.parameter_pyobjects = pynames._Inferred( - self._infer_parameters, self.get_module()._get_concluded_data()) - self.returned = pynames._Inferred(self._infer_returned) - self.parameter_pynames = None - - def _create_structural_attributes(self): - return {} - - def _create_concluded_attributes(self): - return {} - - def _create_scope(self): - return rope.base.pyscopes.FunctionScope(self.pycore, self, - _FunctionVisitor) - - def _infer_parameters(self): - pyobjects = rope.base.oi.soi.infer_parameter_objects(self) - self._handle_special_args(pyobjects) - return pyobjects - - def _infer_returned(self, args=None): - return rope.base.oi.soi.infer_returned_object(self, args) - - def _handle_special_args(self, pyobjects): - if len(pyobjects) == len(self.arguments.args): - if self.arguments.vararg: - pyobjects.append(rope.base.builtins.get_list()) - if self.arguments.kwarg: - pyobjects.append(rope.base.builtins.get_dict()) - - def _set_parameter_pyobjects(self, pyobjects): - if pyobjects is not None: - self._handle_special_args(pyobjects) - self.parameter_pyobjects.set(pyobjects) - - def get_parameters(self): - if self.parameter_pynames is None: - result = {} - for index, name in enumerate(self.get_param_names()): - # TODO: handle tuple parameters - result[name] = pynames.ParameterName(self, index) - self.parameter_pynames = result - return self.parameter_pynames - - def get_parameter(self, index): - if index < len(self.parameter_pyobjects.get()): - return self.parameter_pyobjects.get()[index] - - def get_returned_object(self, args): - return self.returned.get(args) - - def get_name(self): - return self.get_ast().name - - def get_param_names(self, special_args=True): - # TODO: handle tuple parameters - result = [pycompat.get_ast_arg_arg(node) for node in self.arguments.args - if isinstance(node, pycompat.ast_arg_type)] - if special_args: - if self.arguments.vararg: - result.append(pycompat.get_ast_arg_arg(self.arguments.vararg)) - if self.arguments.kwarg: - result.append(pycompat.get_ast_arg_arg(self.arguments.kwarg)) - return result - - def get_kind(self): - """Get function type - - It returns one of 'function', 'method', 'staticmethod' or - 'classmethod' strs. - - """ - scope = self.parent.get_scope() - if isinstance(self.parent, PyClass): - for decorator in self.decorators: - pyname = rope.base.evaluate.eval_node(scope, decorator) - if pyname == rope.base.builtins.builtins['staticmethod']: - return 'staticmethod' - if pyname == rope.base.builtins.builtins['classmethod']: - return 'classmethod' - return 'method' - return 'function' - - @property - def decorators(self): - try: - return getattr(self.ast_node, 'decorator_list') - except AttributeError: - return getattr(self.ast_node, 'decorators', None) - - -class PyClass(pyobjects.PyClass): - - def __init__(self, pycore, ast_node, parent): - self.visitor_class = _ClassVisitor - rope.base.pyobjects.AbstractClass.__init__(self) - rope.base.pyobjects.PyDefinedObject.__init__( - self, pycore, ast_node, parent) - self.parent = parent - self._superclasses = self.get_module()._get_concluded_data() - - def get_superclasses(self): - if self._superclasses.get() is None: - self._superclasses.set(self._get_bases()) - return self._superclasses.get() - - def get_name(self): - return self.get_ast().name - - def _create_concluded_attributes(self): - result = {} - for base in reversed(self.get_superclasses()): - result.update(base.get_attributes()) - return result - - def _get_bases(self): - result = [] - for base_name in self.ast_node.bases: - base = rope.base.evaluate.eval_node(self.parent.get_scope(), - base_name) - if base is not None and \ - base.get_object().get_type() == \ - rope.base.pyobjects.get_base_type('Type'): - result.append(base.get_object()) - return result - - def _create_scope(self): - return rope.base.pyscopes.ClassScope(self.pycore, self) - - -class PyModule(pyobjects.PyModule): - - def __init__(self, pycore, source=None, - resource=None, force_errors=False): - ignore = pycore.project.prefs.get('ignore_syntax_errors', False) - syntax_errors = force_errors or not ignore - self.has_errors = False - try: - source, node = self._init_source(pycore, source, resource) - except exceptions.ModuleSyntaxError: - self.has_errors = True - if syntax_errors: - raise - else: - source = '\n' - node = ast.parse('\n') - self.source_code = source - self.star_imports = [] - self.visitor_class = _GlobalVisitor - self.coding = fscommands.read_str_coding(self.source_code) - super(PyModule, self).__init__(pycore, node, resource) - - def _init_source(self, pycore, source_code, resource): - filename = 'string' - if resource: - filename = resource.path - try: - if source_code is None: - source_bytes = resource.read_bytes() - source_code = fscommands.file_data_to_unicode(source_bytes) - else: - if isinstance(source_code, unicode): - source_bytes = fscommands.unicode_to_file_data(source_code) - else: - source_bytes = source_code - ast_node = ast.parse(source_bytes, filename=filename) - except SyntaxError as e: - raise exceptions.ModuleSyntaxError(filename, e.lineno, e.msg) - except UnicodeDecodeError as e: - raise exceptions.ModuleSyntaxError(filename, 1, '%s' % (e.reason)) - return source_code, ast_node - - @utils.prevent_recursion(lambda: {}) - def _create_concluded_attributes(self): - result = {} - for star_import in self.star_imports: - result.update(star_import.get_names()) - return result - - def _create_scope(self): - return rope.base.pyscopes.GlobalScope(self.pycore, self) - - @property - @utils.saveit - def lines(self): - """A `SourceLinesAdapter`""" - return rope.base.codeanalyze.SourceLinesAdapter(self.source_code) - - @property - @utils.saveit - def logical_lines(self): - """A `LogicalLinesFinder`""" - return rope.base.codeanalyze.CachingLogicalLineFinder(self.lines) - - -class PyPackage(pyobjects.PyPackage): - - def __init__(self, pycore, resource=None, force_errors=False): - self.resource = resource - init_dot_py = self._get_init_dot_py() - if init_dot_py is not None: - ast_node = pycore.project.get_pymodule( - init_dot_py, force_errors=force_errors).get_ast() - else: - ast_node = ast.parse('\n') - super(PyPackage, self).__init__(pycore, ast_node, resource) - - def _create_structural_attributes(self): - result = {} - modname = rope.base.libutils.modname(self.resource) - extension_submodules = self.pycore._builtin_submodules(modname) - for name, module in extension_submodules.items(): - result[name] = rope.base.builtins.BuiltinName(module) - if self.resource is None: - return result - for name, resource in self._get_child_resources().items(): - result[name] = pynames.ImportedModule(self, resource=resource) - return result - - def _create_concluded_attributes(self): - result = {} - init_dot_py = self._get_init_dot_py() - if init_dot_py: - init_object = self.pycore.project.get_pymodule(init_dot_py) - result.update(init_object.get_attributes()) - return result - - def _get_child_resources(self): - result = {} - for child in self.resource.get_children(): - if child.is_folder(): - result[child.name] = child - elif child.name.endswith('.py') and \ - child.name != '__init__.py': - name = child.name[:-3] - result[name] = child - return result - - def _get_init_dot_py(self): - if self.resource is not None and \ - self.resource.has_child('__init__.py'): - return self.resource.get_child('__init__.py') - else: - return None - - def _create_scope(self): - return self.get_module().get_scope() - - def get_module(self): - init_dot_py = self._get_init_dot_py() - if init_dot_py: - return self.pycore.project.get_pymodule(init_dot_py) - return self - - -class _AssignVisitor(object): - - def __init__(self, scope_visitor): - self.scope_visitor = scope_visitor - self.assigned_ast = None - - def _Assign(self, node): - self.assigned_ast = node.value - for child_node in node.targets: - ast.walk(child_node, self) - - def _assigned(self, name, assignment=None): - self.scope_visitor._assigned(name, assignment) - - def _Name(self, node): - assignment = None - if self.assigned_ast is not None: - assignment = pynames.AssignmentValue(self.assigned_ast) - self._assigned(node.id, assignment) - - def _Tuple(self, node): - names = astutils.get_name_levels(node) - for name, levels in names: - assignment = None - if self.assigned_ast is not None: - assignment = pynames.AssignmentValue(self.assigned_ast, levels) - self._assigned(name, assignment) - - def _Attribute(self, node): - pass - - def _Subscript(self, node): - pass - - def _Slice(self, node): - pass - - -class _ScopeVisitor(object): - - def __init__(self, pycore, owner_object): - self.pycore = pycore - self.owner_object = owner_object - self.names = {} - self.defineds = [] - - def get_module(self): - if self.owner_object is not None: - return self.owner_object.get_module() - else: - return None - - def _ClassDef(self, node): - pyclass = PyClass(self.pycore, node, self.owner_object) - self.names[node.name] = pynames.DefinedName(pyclass) - self.defineds.append(pyclass) - - def _FunctionDef(self, node): - pyfunction = PyFunction(self.pycore, node, self.owner_object) - for decorator in pyfunction.decorators: - if isinstance(decorator, ast.Name) and decorator.id == 'property': - if isinstance(self, _ClassVisitor): - type_ = rope.base.builtins.Property(pyfunction) - arg = pynames.UnboundName( - rope.base.pyobjects.PyObject(self.owner_object)) - - def _eval(type_=type_, arg=arg): - return type_.get_property_object( - arguments.ObjectArguments([arg])) - self.names[node.name] = pynames.EvaluatedName( - _eval, module=self.get_module(), lineno=node.lineno) - break - else: - self.names[node.name] = pynames.DefinedName(pyfunction) - self.defineds.append(pyfunction) - - def _Assign(self, node): - ast.walk(node, _AssignVisitor(self)) - - def _AugAssign(self, node): - pass - - def _For(self, node): - names = self._update_evaluated(node.target, node.iter, # noqa - '.__iter__().next()') - for child in node.body + node.orelse: - ast.walk(child, self) - - def _assigned(self, name, assignment): - pyname = self.names.get(name, None) - if pyname is None: - pyname = pynames.AssignedName(module=self.get_module()) - if isinstance(pyname, pynames.AssignedName): - if assignment is not None: - pyname.assignments.append(assignment) - self.names[name] = pyname - - def _update_evaluated(self, targets, assigned, - evaluation='', eval_type=False): - result = {} - if isinstance(targets, str): - assignment = pynames.AssignmentValue(assigned, [], - evaluation, eval_type) - self._assigned(targets, assignment) - else: - names = astutils.get_name_levels(targets) - for name, levels in names: - assignment = pynames.AssignmentValue(assigned, levels, - evaluation, eval_type) - self._assigned(name, assignment) - return result - - def _With(self, node): - for item in pycompat.get_ast_with_items(node): - if item.optional_vars: - self._update_evaluated(item.optional_vars, - item.context_expr, '.__enter__()') - for child in node.body: - ast.walk(child, self) - - def _excepthandler(self, node): - node_name_type = str if pycompat.PY3 else ast.Name - if node.name is not None and isinstance(node.name, node_name_type): - type_node = node.type - if isinstance(node.type, ast.Tuple) and type_node.elts: - type_node = type_node.elts[0] - self._update_evaluated(node.name, type_node, eval_type=True) - - for child in node.body: - ast.walk(child, self) - - def _ExceptHandler(self, node): - self._excepthandler(node) - - def _Import(self, node): - for import_pair in node.names: - module_name = import_pair.name - alias = import_pair.asname - first_package = module_name.split('.')[0] - if alias is not None: - imported = pynames.ImportedModule(self.get_module(), - module_name) - if not self._is_ignored_import(imported): - self.names[alias] = imported - else: - imported = pynames.ImportedModule(self.get_module(), - first_package) - if not self._is_ignored_import(imported): - self.names[first_package] = imported - - def _ImportFrom(self, node): - level = 0 - if node.level: - level = node.level - imported_module = pynames.ImportedModule(self.get_module(), - node.module, level) - if self._is_ignored_import(imported_module): - return - if len(node.names) == 1 and node.names[0].name == '*': - if isinstance(self.owner_object, PyModule): - self.owner_object.star_imports.append( - StarImport(imported_module)) - else: - for imported_name in node.names: - imported = imported_name.name - alias = imported_name.asname - if alias is not None: - imported = alias - self.names[imported] = pynames.ImportedName(imported_module, - imported_name.name) - - def _is_ignored_import(self, imported_module): - if not self.pycore.project.prefs.get('ignore_bad_imports', False): - return False - return not isinstance(imported_module.get_object(), - rope.base.pyobjects.AbstractModule) - - def _Global(self, node): - module = self.get_module() - for name in node.names: - if module is not None: - try: - pyname = module[name] - except exceptions.AttributeNotFoundError: - pyname = pynames.AssignedName(node.lineno) - self.names[name] = pyname - - -class _GlobalVisitor(_ScopeVisitor): - - def __init__(self, pycore, owner_object): - super(_GlobalVisitor, self).__init__(pycore, owner_object) - - -class _ClassVisitor(_ScopeVisitor): - - def __init__(self, pycore, owner_object): - super(_ClassVisitor, self).__init__(pycore, owner_object) - - def _FunctionDef(self, node): - _ScopeVisitor._FunctionDef(self, node) - if len(node.args.args) > 0: - first = node.args.args[0] - new_visitor = None - if isinstance(first, pycompat.ast_arg_type): - new_visitor = _ClassInitVisitor(self, pycompat.get_ast_arg_arg(first)) - if new_visitor is not None: - for child in ast.get_child_nodes(node): - ast.walk(child, new_visitor) - - -class _FunctionVisitor(_ScopeVisitor): - - def __init__(self, pycore, owner_object): - super(_FunctionVisitor, self).__init__(pycore, owner_object) - self.returned_asts = [] - self.generator = False - - def _Return(self, node): - if node.value is not None: - self.returned_asts.append(node.value) - - def _Yield(self, node): - if node.value is not None: - self.returned_asts.append(node.value) - self.generator = True - - -class _ClassInitVisitor(_AssignVisitor): - - def __init__(self, scope_visitor, self_name): - super(_ClassInitVisitor, self).__init__(scope_visitor) - self.self_name = self_name - - def _Attribute(self, node): - if not isinstance(node.ctx, ast.Store): - return - if isinstance(node.value, ast.Name) and \ - node.value.id == self.self_name: - if node.attr not in self.scope_visitor.names: - self.scope_visitor.names[node.attr] = pynames.AssignedName( - lineno=node.lineno, module=self.scope_visitor.get_module()) - if self.assigned_ast is not None: - pyname = self.scope_visitor.names[node.attr] - if isinstance(pyname, pynames.AssignedName): - pyname.assignments.append( - pynames.AssignmentValue(self.assigned_ast)) - - def _Tuple(self, node): - if not isinstance(node.ctx, ast.Store): - return - for child in ast.get_child_nodes(node): - ast.walk(child, self) - - def _Name(self, node): - pass - - def _FunctionDef(self, node): - pass - - def _ClassDef(self, node): - pass - - def _For(self, node): - pass - - def _With(self, node): - pass - - -class StarImport(object): - - def __init__(self, imported_module): - self.imported_module = imported_module - - def get_names(self): - result = {} - imported = self.imported_module.get_object() - for name in imported: - if not name.startswith('_'): - result[name] = pynames.ImportedName(self.imported_module, name) - return result diff --git a/pythonFiles/rope/base/pyscopes.py b/pythonFiles/rope/base/pyscopes.py deleted file mode 100644 index 0bed19a92912..000000000000 --- a/pythonFiles/rope/base/pyscopes.py +++ /dev/null @@ -1,314 +0,0 @@ -import rope.base.builtins -import rope.base.codeanalyze -import rope.base.pynames -from rope.base import ast, exceptions, utils - - -class Scope(object): - - def __init__(self, pycore, pyobject, parent_scope): - self.pycore = pycore - self.pyobject = pyobject - self.parent = parent_scope - - def get_names(self): - """Return the names defined or imported in this scope""" - return self.pyobject.get_attributes() - - def get_defined_names(self): - """Return the names defined in this scope""" - return self.pyobject._get_structural_attributes() - - def get_name(self, name): - """Return name `PyName` defined in this scope""" - if name not in self.get_names(): - raise exceptions.NameNotFoundError('name %s not found' % name) - return self.get_names()[name] - - def __getitem__(self, key): - """The same as ``get_name(key)``""" - return self.get_name(key) - - def __contains__(self, key): - """The same as ``key in self.get_names()``""" - return key in self.get_names() - - @utils.saveit - def get_scopes(self): - """Return the subscopes of this scope - - The returned scopes should be sorted by the order they appear. - """ - return self._create_scopes() - - def lookup(self, name): - if name in self.get_names(): - return self.get_names()[name] - if self.parent is not None: - return self.parent._propagated_lookup(name) - return None - - def get_propagated_names(self): - """Return the visible names of this scope - - Return the names defined in this scope that are visible from - scopes containing this scope. This method returns the same - dictionary returned by `get_names()` except for `ClassScope` - which returns an empty dict. - """ - return self.get_names() - - def _propagated_lookup(self, name): - if name in self.get_propagated_names(): - return self.get_propagated_names()[name] - if self.parent is not None: - return self.parent._propagated_lookup(name) - return None - - def _create_scopes(self): - return [pydefined.get_scope() - for pydefined in self.pyobject._get_defined_objects()] - - def _get_global_scope(self): - current = self - while current.parent is not None: - current = current.parent - return current - - def get_start(self): - return self.pyobject.get_ast().lineno - - def get_body_start(self): - body = self.pyobject.get_ast().body - if body: - return body[0].lineno - return self.get_start() - - def get_end(self): - pymodule = self._get_global_scope().pyobject - return pymodule.logical_lines.logical_line_in(self.logical_end)[1] - - @utils.saveit - def get_logical_end(self): - global_scope = self._get_global_scope() - return global_scope._scope_finder.find_scope_end(self) - - start = property(get_start) - end = property(get_end) - logical_end = property(get_logical_end) - - def get_kind(self): - pass - - -class GlobalScope(Scope): - - def __init__(self, pycore, module): - super(GlobalScope, self).__init__(pycore, module, None) - self.names = module._get_concluded_data() - - def get_start(self): - return 1 - - def get_kind(self): - return 'Module' - - def get_name(self, name): - try: - return self.pyobject[name] - except exceptions.AttributeNotFoundError: - if name in self.builtin_names: - return self.builtin_names[name] - raise exceptions.NameNotFoundError('name %s not found' % name) - - def get_names(self): - if self.names.get() is None: - result = dict(self.builtin_names) - result.update(super(GlobalScope, self).get_names()) - self.names.set(result) - return self.names.get() - - def get_inner_scope_for_line(self, lineno, indents=None): - return self._scope_finder.get_holding_scope(self, lineno, indents) - - def get_inner_scope_for_offset(self, offset): - return self._scope_finder.get_holding_scope_for_offset(self, offset) - - @property - @utils.saveit - def _scope_finder(self): - return _HoldingScopeFinder(self.pyobject) - - @property - def builtin_names(self): - return rope.base.builtins.builtins.get_attributes() - - -class FunctionScope(Scope): - - def __init__(self, pycore, pyobject, visitor): - super(FunctionScope, self).__init__(pycore, pyobject, - pyobject.parent.get_scope()) - self.names = None - self.returned_asts = None - self.is_generator = None - self.defineds = None - self.visitor = visitor - - def _get_names(self): - if self.names is None: - self._visit_function() - return self.names - - def _visit_function(self): - if self.names is None: - new_visitor = self.visitor(self.pycore, self.pyobject) - for n in ast.get_child_nodes(self.pyobject.get_ast()): - ast.walk(n, new_visitor) - self.names = new_visitor.names - self.names.update(self.pyobject.get_parameters()) - self.returned_asts = new_visitor.returned_asts - self.is_generator = new_visitor.generator - self.defineds = new_visitor.defineds - - def _get_returned_asts(self): - if self.names is None: - self._visit_function() - return self.returned_asts - - def _is_generator(self): - if self.is_generator is None: - self._get_returned_asts() - return self.is_generator - - def get_names(self): - return self._get_names() - - def _create_scopes(self): - if self.defineds is None: - self._visit_function() - return [pydefined.get_scope() for pydefined in self.defineds] - - def get_kind(self): - return 'Function' - - def invalidate_data(self): - for pyname in self.get_names().values(): - if isinstance(pyname, (rope.base.pynames.AssignedName, - rope.base.pynames.EvaluatedName)): - pyname.invalidate() - - -class ClassScope(Scope): - - def __init__(self, pycore, pyobject): - super(ClassScope, self).__init__(pycore, pyobject, - pyobject.parent.get_scope()) - - def get_kind(self): - return 'Class' - - def get_propagated_names(self): - return {} - - -class _HoldingScopeFinder(object): - - def __init__(self, pymodule): - self.pymodule = pymodule - - def get_indents(self, lineno): - return rope.base.codeanalyze.count_line_indents( - self.lines.get_line(lineno)) - - def _get_scope_indents(self, scope): - return self.get_indents(scope.get_start()) - - def get_holding_scope(self, module_scope, lineno, line_indents=None): - if line_indents is None: - line_indents = self.get_indents(lineno) - current_scope = module_scope - new_scope = current_scope - while new_scope is not None and \ - (new_scope.get_kind() == 'Module' or - self._get_scope_indents(new_scope) <= line_indents): - current_scope = new_scope - if current_scope.get_start() == lineno and \ - current_scope.get_kind() != 'Module': - return current_scope - new_scope = None - for scope in current_scope.get_scopes(): - if scope.get_start() <= lineno: - if lineno <= scope.get_end(): - new_scope = scope - break - else: - break - return current_scope - - def _is_empty_line(self, lineno): - line = self.lines.get_line(lineno) - return line.strip() == '' or line.lstrip().startswith('#') - - def _get_body_indents(self, scope): - return self.get_indents(scope.get_body_start()) - - def get_holding_scope_for_offset(self, scope, offset): - return self.get_holding_scope( - scope, self.lines.get_line_number(offset)) - - def find_scope_end(self, scope): - if not scope.parent: - return self.lines.length() - end = scope.pyobject.get_ast().body[-1].lineno - scope_start = self.pymodule.logical_lines.logical_line_in(scope.start) - if scope_start[1] >= end: - # handling one-liners - body_indents = self._get_scope_indents(scope) + 4 - else: - body_indents = self._get_body_indents(scope) - for l in self.logical_lines.generate_starts( - min(end + 1, self.lines.length()), self.lines.length() + 1): - if not self._is_empty_line(l): - if self.get_indents(l) < body_indents: - return end - else: - end = l - return end - - @property - def lines(self): - return self.pymodule.lines - - @property - def code(self): - return self.pymodule.source_code - - @property - def logical_lines(self): - return self.pymodule.logical_lines - - -class TemporaryScope(Scope): - """Currently used for list comprehensions and generator expressions - - These scopes do not appear in the `get_scopes()` method of their - parent scopes. - """ - - def __init__(self, pycore, parent_scope, names): - super(TemporaryScope, self).__init__( - pycore, parent_scope.pyobject, parent_scope) - self.names = names - - def get_names(self): - return self.names - - def get_defined_names(self): - return self.names - - def _create_scopes(self): - return [] - - def get_kind(self): - return 'Temporary' diff --git a/pythonFiles/rope/base/resourceobserver.py b/pythonFiles/rope/base/resourceobserver.py deleted file mode 100644 index 7c0937d5bbac..000000000000 --- a/pythonFiles/rope/base/resourceobserver.py +++ /dev/null @@ -1,272 +0,0 @@ -import os - - -class ResourceObserver(object): - """Provides the interface for observing resources - - `ResourceObserver`\s can be registered using `Project. - add_observer()`. But most of the time `FilteredResourceObserver` - should be used. `ResourceObserver`\s report all changes passed - to them and they don't report changes to all resources. For - example if a folder is removed, it only calls `removed()` for that - folder and not its contents. You can use - `FilteredResourceObserver` if you are interested in changes only - to a list of resources. And you want changes to be reported on - individual resources. - - """ - - def __init__(self, changed=None, moved=None, created=None, - removed=None, validate=None): - self.changed = changed - self.moved = moved - self.created = created - self.removed = removed - self._validate = validate - - def resource_changed(self, resource): - """It is called when the resource changes""" - if self.changed is not None: - self.changed(resource) - - def resource_moved(self, resource, new_resource): - """It is called when a resource is moved""" - if self.moved is not None: - self.moved(resource, new_resource) - - def resource_created(self, resource): - """Is called when a new resource is created""" - if self.created is not None: - self.created(resource) - - def resource_removed(self, resource): - """Is called when a new resource is removed""" - if self.removed is not None: - self.removed(resource) - - def validate(self, resource): - """Validate the existence of this resource and its children. - - This function is called when rope need to update its resource - cache about the files that might have been changed or removed - by other processes. - - """ - if self._validate is not None: - self._validate(resource) - - -class FilteredResourceObserver(object): - """A useful decorator for `ResourceObserver` - - Most resource observers have a list of resources and are - interested only in changes to those files. This class satisfies - this need. It dispatches resource changed and removed messages. - It performs these tasks: - - * Changes to files and folders are analyzed to check whether any - of the interesting resources are changed or not. If they are, - it reports these changes to `resource_observer` passed to the - constructor. - * When a resource is removed it checks whether any of the - interesting resources are contained in that folder and reports - them to `resource_observer`. - * When validating a folder it validates all of the interesting - files in that folder. - - Since most resource observers are interested in a list of - resources that change over time, `add_resource` and - `remove_resource` might be useful. - - """ - - def __init__(self, resource_observer, initial_resources=None, - timekeeper=None): - self.observer = resource_observer - self.resources = {} - if timekeeper is not None: - self.timekeeper = timekeeper - else: - self.timekeeper = ChangeIndicator() - if initial_resources is not None: - for resource in initial_resources: - self.add_resource(resource) - - def add_resource(self, resource): - """Add a resource to the list of interesting resources""" - if resource.exists(): - self.resources[resource] = self.timekeeper.get_indicator(resource) - else: - self.resources[resource] = None - - def remove_resource(self, resource): - """Add a resource to the list of interesting resources""" - if resource in self.resources: - del self.resources[resource] - - def clear_resources(self): - """Removes all registered resources""" - self.resources.clear() - - def resource_changed(self, resource): - changes = _Changes() - self._update_changes_caused_by_changed(changes, resource) - self._perform_changes(changes) - - def _update_changes_caused_by_changed(self, changes, changed): - if changed in self.resources: - changes.add_changed(changed) - if self._is_parent_changed(changed): - changes.add_changed(changed.parent) - - def _update_changes_caused_by_moved(self, changes, resource, - new_resource=None): - if resource in self.resources: - changes.add_removed(resource, new_resource) - if new_resource in self.resources: - changes.add_created(new_resource) - if resource.is_folder(): - for file in list(self.resources): - if resource.contains(file): - new_file = self._calculate_new_resource( - resource, new_resource, file) - changes.add_removed(file, new_file) - if self._is_parent_changed(resource): - changes.add_changed(resource.parent) - if new_resource is not None: - if self._is_parent_changed(new_resource): - changes.add_changed(new_resource.parent) - - def _is_parent_changed(self, child): - return child.parent in self.resources - - def resource_moved(self, resource, new_resource): - changes = _Changes() - self._update_changes_caused_by_moved(changes, resource, new_resource) - self._perform_changes(changes) - - def resource_created(self, resource): - changes = _Changes() - self._update_changes_caused_by_created(changes, resource) - self._perform_changes(changes) - - def _update_changes_caused_by_created(self, changes, resource): - if resource in self.resources: - changes.add_created(resource) - if self._is_parent_changed(resource): - changes.add_changed(resource.parent) - - def resource_removed(self, resource): - changes = _Changes() - self._update_changes_caused_by_moved(changes, resource) - self._perform_changes(changes) - - def _perform_changes(self, changes): - for resource in changes.changes: - self.observer.resource_changed(resource) - self.resources[resource] = self.timekeeper.get_indicator(resource) - for resource, new_resource in changes.moves.items(): - self.resources[resource] = None - if new_resource is not None: - self.observer.resource_moved(resource, new_resource) - else: - self.observer.resource_removed(resource) - for resource in changes.creations: - self.observer.resource_created(resource) - self.resources[resource] = self.timekeeper.get_indicator(resource) - - def validate(self, resource): - changes = _Changes() - for file in self._search_resource_moves(resource): - if file in self.resources: - self._update_changes_caused_by_moved(changes, file) - for file in self._search_resource_changes(resource): - if file in self.resources: - self._update_changes_caused_by_changed(changes, file) - for file in self._search_resource_creations(resource): - if file in self.resources: - changes.add_created(file) - self._perform_changes(changes) - - def _search_resource_creations(self, resource): - creations = set() - if resource in self.resources and resource.exists() and \ - self.resources[resource] is None: - creations.add(resource) - if resource.is_folder(): - for file in self.resources: - if file.exists() and resource.contains(file) and \ - self.resources[file] is None: - creations.add(file) - return creations - - def _search_resource_moves(self, resource): - all_moved = set() - if resource in self.resources and not resource.exists(): - all_moved.add(resource) - if resource.is_folder(): - for file in self.resources: - if resource.contains(file): - if not file.exists(): - all_moved.add(file) - moved = set(all_moved) - for folder in [file for file in all_moved if file.is_folder()]: - if folder in moved: - for file in list(moved): - if folder.contains(file): - moved.remove(file) - return moved - - def _search_resource_changes(self, resource): - changed = set() - if resource in self.resources and self._is_changed(resource): - changed.add(resource) - if resource.is_folder(): - for file in self.resources: - if file.exists() and resource.contains(file): - if self._is_changed(file): - changed.add(file) - return changed - - def _is_changed(self, resource): - if self.resources[resource] is None: - return False - return self.resources[resource] != \ - self.timekeeper.get_indicator(resource) - - def _calculate_new_resource(self, main, new_main, resource): - if new_main is None: - return None - diff = resource.path[len(main.path):] - return resource.project.get_resource(new_main.path + diff) - - -class ChangeIndicator(object): - - def get_indicator(self, resource): - """Return the modification time and size of a `Resource`.""" - path = resource.real_path - # on dos, mtime does not change for a folder when files are added - if os.name != 'posix' and os.path.isdir(path): - return (os.path.getmtime(path), - len(os.listdir(path)), - os.path.getsize(path)) - return (os.path.getmtime(path), - os.path.getsize(path)) - - -class _Changes(object): - - def __init__(self): - self.changes = set() - self.creations = set() - self.moves = {} - - def add_changed(self, resource): - self.changes.add(resource) - - def add_removed(self, resource, new_resource=None): - self.moves[resource] = new_resource - - def add_created(self, resource): - self.creations.add(resource) diff --git a/pythonFiles/rope/base/resources.py b/pythonFiles/rope/base/resources.py deleted file mode 100644 index af8ac0a27698..000000000000 --- a/pythonFiles/rope/base/resources.py +++ /dev/null @@ -1,243 +0,0 @@ -"""Files and folders in a project are represented as resource objects. - -Files and folders are access through `Resource` objects. `Resource` has -two subclasses: `File` and `Folder`. What we care about is that -refactorings and `rope.base.change.Change`s use resources. - -There are two options to create a `Resource` for a path in a project. -Note that in these examples `path` is the path to a file or folder -relative to the project's root. A project's root folder is represented -by an empty string. - - 1) Use the `rope.base.Project.get_resource()` method. E.g.: - - myresource = myproject.get_resource(path) - - - 2) Use the `rope.base.libutils` module. `libutils` has a function - named `path_to_resource()`. It takes a project and a path: - - from rope.base import libutils - - myresource = libutils.path_to_resource(myproject, path) - -Once we have a `Resource`, we can retrieve information from it, like -getting the path relative to the project's root (via `path`), reading -from and writing to the resource, moving the resource, etc. -""" - -import os -import re - -from rope.base import change -from rope.base import exceptions -from rope.base import fscommands - - -class Resource(object): - """Represents files and folders in a project""" - - def __init__(self, project, path): - self.project = project - self._path = path - - def move(self, new_location): - """Move resource to `new_location`""" - self._perform_change(change.MoveResource(self, new_location), - 'Moving <%s> to <%s>' % (self.path, new_location)) - - def remove(self): - """Remove resource from the project""" - self._perform_change(change.RemoveResource(self), - 'Removing <%s>' % self.path) - - def is_folder(self): - """Return true if the resource is a folder""" - - def create(self): - """Create this resource""" - - def exists(self): - return os.path.exists(self.real_path) - - @property - def parent(self): - parent = '/'.join(self.path.split('/')[0:-1]) - return self.project.get_folder(parent) - - @property - def path(self): - """Return the path of this resource relative to the project root - - The path is the list of parent directories separated by '/' followed - by the resource name. - """ - return self._path - - @property - def name(self): - """Return the name of this resource""" - return self.path.split('/')[-1] - - @property - def real_path(self): - """Return the file system path of this resource""" - return self.project._get_resource_path(self.path) - - def __eq__(self, obj): - return self.__class__ == obj.__class__ and self.path == obj.path - - def __ne__(self, obj): - return not self.__eq__(obj) - - def __hash__(self): - return hash(self.path) - - def _perform_change(self, change_, description): - changes = change.ChangeSet(description) - changes.add_change(change_) - self.project.do(changes) - - -class File(Resource): - """Represents a file""" - - def __init__(self, project, name): - super(File, self).__init__(project, name) - - def read(self): - data = self.read_bytes() - try: - return fscommands.file_data_to_unicode(data) - except UnicodeDecodeError as e: - raise exceptions.ModuleDecodeError(self.path, e.reason) - - def read_bytes(self): - return open(self.real_path, 'rb').read() - - def write(self, contents): - try: - if contents == self.read(): - return - except IOError: - pass - self._perform_change(change.ChangeContents(self, contents), - 'Writing file <%s>' % self.path) - - def is_folder(self): - return False - - def create(self): - self.parent.create_file(self.name) - - -class Folder(Resource): - """Represents a folder""" - - def __init__(self, project, name): - super(Folder, self).__init__(project, name) - - def is_folder(self): - return True - - def get_children(self): - """Return the children of this folder""" - try: - children = os.listdir(self.real_path) - except OSError: - return [] - result = [] - for name in children: - try: - child = self.get_child(name) - except exceptions.ResourceNotFoundError: - continue - if not self.project.is_ignored(child): - result.append(self.get_child(name)) - return result - - def create_file(self, file_name): - self._perform_change( - change.CreateFile(self, file_name), - 'Creating file <%s>' % self._get_child_path(file_name)) - return self.get_child(file_name) - - def create_folder(self, folder_name): - self._perform_change( - change.CreateFolder(self, folder_name), - 'Creating folder <%s>' % self._get_child_path(folder_name)) - return self.get_child(folder_name) - - def _get_child_path(self, name): - if self.path: - return self.path + '/' + name - else: - return name - - def get_child(self, name): - return self.project.get_resource(self._get_child_path(name)) - - def has_child(self, name): - try: - self.get_child(name) - return True - except exceptions.ResourceNotFoundError: - return False - - def get_files(self): - return [resource for resource in self.get_children() - if not resource.is_folder()] - - def get_folders(self): - return [resource for resource in self.get_children() - if resource.is_folder()] - - def contains(self, resource): - if self == resource: - return False - return self.path == '' or resource.path.startswith(self.path + '/') - - def create(self): - self.parent.create_folder(self.name) - - -class _ResourceMatcher(object): - - def __init__(self): - self.patterns = [] - self._compiled_patterns = [] - - def set_patterns(self, patterns): - """Specify which resources to match - - `patterns` is a `list` of `str`\s that can contain ``*`` and - ``?`` signs for matching resource names. - - """ - self._compiled_patterns = None - self.patterns = patterns - - def _add_pattern(self, pattern): - re_pattern = pattern.replace('.', '\\.').\ - replace('*', '[^/]*').replace('?', '[^/]').\ - replace('//', '/(.*/)?') - re_pattern = '^(.*/)?' + re_pattern + '(/.*)?$' - self.compiled_patterns.append(re.compile(re_pattern)) - - def does_match(self, resource): - for pattern in self.compiled_patterns: - if pattern.match(resource.path): - return True - path = os.path.join(resource.project.address, - *resource.path.split('/')) - if os.path.islink(path): - return True - return False - - @property - def compiled_patterns(self): - if self._compiled_patterns is None: - self._compiled_patterns = [] - for pattern in self.patterns: - self._add_pattern(pattern) - return self._compiled_patterns diff --git a/pythonFiles/rope/base/simplify.py b/pythonFiles/rope/base/simplify.py deleted file mode 100644 index bc4cade4ab91..000000000000 --- a/pythonFiles/rope/base/simplify.py +++ /dev/null @@ -1,55 +0,0 @@ -"""A module to ease code analysis - -This module is here to help source code analysis. -""" -import re - -from rope.base import codeanalyze, utils - - -@utils.cached(7) -def real_code(source): - """Simplify `source` for analysis - - It replaces: - - * comments with spaces - * strs with a new str filled with spaces - * implicit and explicit continuations with spaces - * tabs and semicolons with spaces - - The resulting code is a lot easier to analyze if we are interested - only in offsets. - """ - collector = codeanalyze.ChangeCollector(source) - for start, end in ignored_regions(source): - if source[start] == '#': - replacement = ' ' * (end - start) - else: - replacement = '"%s"' % (' ' * (end - start - 2)) - collector.add_change(start, end, replacement) - source = collector.get_changed() or source - collector = codeanalyze.ChangeCollector(source) - parens = 0 - for match in _parens.finditer(source): - i = match.start() - c = match.group() - if c in '({[': - parens += 1 - if c in ')}]': - parens -= 1 - if c == '\n' and parens > 0: - collector.add_change(i, i + 1, ' ') - source = collector.get_changed() or source - return source.replace('\\\n', ' ').replace('\t', ' ').replace(';', '\n') - - -@utils.cached(7) -def ignored_regions(source): - """Return ignored regions like strings and comments in `source` """ - return [(match.start(), match.end()) for match in _str.finditer(source)] - - -_str = re.compile('%s|%s' % (codeanalyze.get_comment_pattern(), - codeanalyze.get_string_pattern())) -_parens = re.compile(r'[\({\[\]}\)\n]') diff --git a/pythonFiles/rope/base/stdmods.py b/pythonFiles/rope/base/stdmods.py deleted file mode 100644 index 5e868c2adfe0..000000000000 --- a/pythonFiles/rope/base/stdmods.py +++ /dev/null @@ -1,61 +0,0 @@ -import os -import re -import sys - -from rope.base import utils -from rope.base.utils import pycompat - - -def _stdlib_path(): - if pycompat.PY2: - from distutils import sysconfig - return sysconfig.get_python_lib(standard_lib=True, - plat_specific=True) - elif pycompat.PY3: - import inspect - return os.path.dirname(inspect.getsourcefile(inspect)) - - -@utils.cached(1) -def standard_modules(): - return python_modules() | dynload_modules() - - -@utils.cached(1) -def python_modules(): - result = set() - lib_path = _stdlib_path() - if os.path.exists(lib_path): - for name in os.listdir(lib_path): - path = os.path.join(lib_path, name) - if os.path.isdir(path): - if '-' not in name: - result.add(name) - else: - if name.endswith('.py'): - result.add(name[:-3]) - return result - - -def normalize_so_name(name): - """ - Handle different types of python installations - """ - if "cpython" in name: - return os.path.splitext(os.path.splitext(name)[0])[0] - return os.path.splitext(name)[0] - - -@utils.cached(1) -def dynload_modules(): - result = set(sys.builtin_module_names) - dynload_path = os.path.join(_stdlib_path(), 'lib-dynload') - if os.path.exists(dynload_path): - for name in os.listdir(dynload_path): - path = os.path.join(dynload_path, name) - if os.path.isfile(path): - if name.endswith('.dll'): - result.add(normalize_so_name(name)) - if name.endswith('.so'): - result.add(normalize_so_name(name)) - return result diff --git a/pythonFiles/rope/base/taskhandle.py b/pythonFiles/rope/base/taskhandle.py deleted file mode 100644 index c1f01b984399..000000000000 --- a/pythonFiles/rope/base/taskhandle.py +++ /dev/null @@ -1,131 +0,0 @@ -from rope.base import exceptions - - -class TaskHandle(object): - - def __init__(self, name='Task', interrupts=True): - """Construct a TaskHandle - - If `interrupts` is `False` the task won't be interrupted by - calling `TaskHandle.stop()`. - - """ - self.name = name - self.interrupts = interrupts - self.stopped = False - self.job_sets = [] - self.observers = [] - - def stop(self): - """Interrupts the refactoring""" - if self.interrupts: - self.stopped = True - self._inform_observers() - - def current_jobset(self): - """Return the current `JobSet`""" - if self.job_sets: - return self.job_sets[-1] - - def add_observer(self, observer): - """Register an observer for this task handle - - The observer is notified whenever the task is stopped or - a job gets finished. - - """ - self.observers.append(observer) - - def is_stopped(self): - return self.stopped - - def get_jobsets(self): - return self.job_sets - - def create_jobset(self, name='JobSet', count=None): - result = JobSet(self, name=name, count=count) - self.job_sets.append(result) - self._inform_observers() - return result - - def _inform_observers(self): - for observer in list(self.observers): - observer() - - -class JobSet(object): - - def __init__(self, handle, name, count): - self.handle = handle - self.name = name - self.count = count - self.done = 0 - self.job_name = None - - def started_job(self, name): - self.check_status() - self.job_name = name - self.handle._inform_observers() - - def finished_job(self): - self.check_status() - self.done += 1 - self.handle._inform_observers() - self.job_name = None - - def check_status(self): - if self.handle.is_stopped(): - raise exceptions.InterruptedTaskError() - - def get_active_job_name(self): - return self.job_name - - def get_percent_done(self): - if self.count is not None and self.count > 0: - percent = self.done * 100 // self.count - return min(percent, 100) - - def get_name(self): - return self.name - - -class NullTaskHandle(object): - - def __init__(self): - pass - - def is_stopped(self): - return False - - def stop(self): - pass - - def create_jobset(self, *args, **kwds): - return NullJobSet() - - def get_jobsets(self): - return [] - - def add_observer(self, observer): - pass - - -class NullJobSet(object): - - def started_job(self, name): - pass - - def finished_job(self): - pass - - def check_status(self): - pass - - def get_active_job_name(self): - pass - - def get_percent_done(self): - pass - - def get_name(self): - pass diff --git a/pythonFiles/rope/base/utils/__init__.py b/pythonFiles/rope/base/utils/__init__.py deleted file mode 100644 index 11556c132781..000000000000 --- a/pythonFiles/rope/base/utils/__init__.py +++ /dev/null @@ -1,83 +0,0 @@ -import warnings - - -def saveit(func): - """A decorator that caches the return value of a function""" - - name = '_' + func.__name__ - - def _wrapper(self, *args, **kwds): - if not hasattr(self, name): - setattr(self, name, func(self, *args, **kwds)) - return getattr(self, name) - return _wrapper - -cacheit = saveit - - -def prevent_recursion(default): - """A decorator that returns the return value of `default` in recursions""" - def decorator(func): - name = '_calling_%s_' % func.__name__ - - def newfunc(self, *args, **kwds): - if getattr(self, name, False): - return default() - setattr(self, name, True) - try: - return func(self, *args, **kwds) - finally: - setattr(self, name, False) - return newfunc - return decorator - - -def ignore_exception(exception_class): - """A decorator that ignores `exception_class` exceptions""" - def _decorator(func): - def newfunc(*args, **kwds): - try: - return func(*args, **kwds) - except exception_class: - pass - return newfunc - return _decorator - - -def deprecated(message=None): - """A decorator for deprecated functions""" - def _decorator(func, message=message): - if message is None: - message = '%s is deprecated' % func.__name__ - - def newfunc(*args, **kwds): - warnings.warn(message, DeprecationWarning, stacklevel=2) - return func(*args, **kwds) - return newfunc - return _decorator - - -def cached(count): - """A caching decorator based on parameter objects""" - def decorator(func): - return _Cached(func, count) - return decorator - - -class _Cached(object): - - def __init__(self, func, count): - self.func = func - self.cache = [] - self.count = count - - def __call__(self, *args, **kwds): - key = (args, kwds) - for cached_key, cached_result in self.cache: - if cached_key == key: - return cached_result - result = self.func(*args, **kwds) - self.cache.append((key, result)) - if len(self.cache) > self.count: - del self.cache[0] - return result diff --git a/pythonFiles/rope/base/utils/datastructures.py b/pythonFiles/rope/base/utils/datastructures.py deleted file mode 100644 index 0cb16cf2b19e..000000000000 --- a/pythonFiles/rope/base/utils/datastructures.py +++ /dev/null @@ -1,67 +0,0 @@ -# this snippet was taken from this link -# http://code.activestate.com/recipes/576694/ - -import collections - - -class OrderedSet(collections.MutableSet): - - def __init__(self, iterable=None): - self.end = end = [] - end += [None, end, end] # sentinel - # node for doubly linked list - self.map = {} # key --> [key, prev, next] - if iterable is not None: - self |= iterable - - def __len__(self): - return len(self.map) - - def __contains__(self, key): - return key in self.map - - def add(self, key): - if key not in self.map: - end = self.end - curr = end[1] - curr[2] = end[1] = self.map[key] = [key, curr, end] - - def intersection(self, set_b): - return OrderedSet([item for item in self if item in set_b]) - - def discard(self, key): - if key in self.map: - key, prev, next = self.map.pop(key) - prev[2] = next - next[1] = prev - - def __iter__(self): - end = self.end - curr = end[2] - while curr is not end: - yield curr[0] - curr = curr[2] - - def __reversed__(self): - end = self.end - curr = end[1] - while curr is not end: - yield curr[0] - curr = curr[1] - - def pop(self, last=True): - if not self: - raise KeyError('set is empty') - key = self.end[1][0] if last else self.end[2][0] - self.discard(key) - return key - - def __repr__(self): - if not self: - return '%s()' % (self.__class__.__name__,) - return '%s(%r)' % (self.__class__.__name__, list(self)) - - def __eq__(self, other): - if isinstance(other, OrderedSet): - return len(self) == len(other) and list(self) == list(other) - return set(self) == set(other) diff --git a/pythonFiles/rope/base/utils/pycompat.py b/pythonFiles/rope/base/utils/pycompat.py deleted file mode 100644 index 367cf09228f4..000000000000 --- a/pythonFiles/rope/base/utils/pycompat.py +++ /dev/null @@ -1,45 +0,0 @@ -import sys -import _ast -# from rope.base import ast - -PY2 = sys.version_info[0] == 2 -PY27 = sys.version_info[0:2] >= (2, 7) -PY3 = sys.version_info[0] == 3 -PY34 = sys.version_info[0:2] >= (3, 4) - -try: - str = unicode -except NameError: # PY3 - - str = str - string_types = (str,) - import builtins - ast_arg_type = _ast.arg - - def execfile(fn, global_vars=None, local_vars=None): - with open(fn) as f: - code = compile(f.read(), fn, 'exec') - exec(code, global_vars or {}, local_vars) - - def get_ast_arg_arg(node): - if isinstance(node, string_types): # TODO: G21: Understand the Algorithm (Where it's used?) - return node - return node.arg - - def get_ast_with_items(node): - return node.items - -else: # PY2 - - string_types = (basestring,) - builtins = __import__('__builtin__') - ast_arg_type = _ast.Name - execfile = execfile - - def get_ast_arg_arg(node): - if isinstance(node, string_types): # Python2 arguments.vararg, arguments.kwarg - return node - return node.id - - def get_ast_with_items(node): - return [node] diff --git a/pythonFiles/rope/base/worder.py b/pythonFiles/rope/base/worder.py deleted file mode 100644 index c85c6b366283..000000000000 --- a/pythonFiles/rope/base/worder.py +++ /dev/null @@ -1,525 +0,0 @@ -import bisect -import keyword - -import rope.base.simplify - - -def get_name_at(resource, offset): - source_code = resource.read() - word_finder = Worder(source_code) - return word_finder.get_word_at(offset) - - -class Worder(object): - """A class for finding boundaries of words and expressions - - Note that in these methods, offset should be the index of the - character not the index of the character after it. - """ - - def __init__(self, code, handle_ignores=False): - simplified = rope.base.simplify.real_code(code) - self.code_finder = _RealFinder(simplified, code) - self.handle_ignores = handle_ignores - self.code = code - - def _init_ignores(self): - ignores = rope.base.simplify.ignored_regions(self.code) - self.dumb_finder = _RealFinder(self.code, self.code) - self.starts = [ignored[0] for ignored in ignores] - self.ends = [ignored[1] for ignored in ignores] - - def _context_call(self, name, offset): - if self.handle_ignores: - if not hasattr(self, 'starts'): - self._init_ignores() - start = bisect.bisect(self.starts, offset) - if start > 0 and offset < self.ends[start - 1]: - return getattr(self.dumb_finder, name)(offset) - return getattr(self.code_finder, name)(offset) - - def get_primary_at(self, offset): - return self._context_call('get_primary_at', offset) - - def get_word_at(self, offset): - return self._context_call('get_word_at', offset) - - def get_primary_range(self, offset): - return self._context_call('get_primary_range', offset) - - def get_splitted_primary_before(self, offset): - return self._context_call('get_splitted_primary_before', offset) - - def get_word_range(self, offset): - return self._context_call('get_word_range', offset) - - def is_function_keyword_parameter(self, offset): - return self.code_finder.is_function_keyword_parameter(offset) - - def is_a_class_or_function_name_in_header(self, offset): - return self.code_finder.is_a_class_or_function_name_in_header(offset) - - def is_from_statement_module(self, offset): - return self.code_finder.is_from_statement_module(offset) - - def is_from_aliased(self, offset): - return self.code_finder.is_from_aliased(offset) - - def find_parens_start_from_inside(self, offset): - return self.code_finder.find_parens_start_from_inside(offset) - - def is_a_name_after_from_import(self, offset): - return self.code_finder.is_a_name_after_from_import(offset) - - def is_from_statement(self, offset): - return self.code_finder.is_from_statement(offset) - - def get_from_aliased(self, offset): - return self.code_finder.get_from_aliased(offset) - - def is_import_statement(self, offset): - return self.code_finder.is_import_statement(offset) - - def is_assigned_here(self, offset): - return self.code_finder.is_assigned_here(offset) - - def is_a_function_being_called(self, offset): - return self.code_finder.is_a_function_being_called(offset) - - def get_word_parens_range(self, offset): - return self.code_finder.get_word_parens_range(offset) - - def is_name_assigned_in_class_body(self, offset): - return self.code_finder.is_name_assigned_in_class_body(offset) - - def is_on_function_call_keyword(self, offset): - return self.code_finder.is_on_function_call_keyword(offset) - - def _find_parens_start(self, offset): - return self.code_finder._find_parens_start(offset) - - def get_parameters(self, first, last): - return self.code_finder.get_parameters(first, last) - - def get_from_module(self, offset): - return self.code_finder.get_from_module(offset) - - def is_assigned_in_a_tuple_assignment(self, offset): - return self.code_finder.is_assigned_in_a_tuple_assignment(offset) - - def get_assignment_type(self, offset): - return self.code_finder.get_assignment_type(offset) - - def get_function_and_args_in_header(self, offset): - return self.code_finder.get_function_and_args_in_header(offset) - - def get_lambda_and_args(self, offset): - return self.code_finder.get_lambda_and_args(offset) - - def find_function_offset(self, offset): - return self.code_finder.find_function_offset(offset) - - -class _RealFinder(object): - - def __init__(self, code, raw): - self.code = code - self.raw = raw - - def _find_word_start(self, offset): - current_offset = offset - while current_offset >= 0 and self._is_id_char(current_offset): - current_offset -= 1 - return current_offset + 1 - - def _find_word_end(self, offset): - while offset + 1 < len(self.code) and self._is_id_char(offset + 1): - offset += 1 - return offset - - def _find_last_non_space_char(self, offset): - while offset >= 0 and self.code[offset].isspace(): - if self.code[offset] == '\n': - return offset - offset -= 1 - return max(-1, offset) - - def get_word_at(self, offset): - offset = self._get_fixed_offset(offset) - return self.raw[self._find_word_start(offset): - self._find_word_end(offset) + 1] - - def _get_fixed_offset(self, offset): - if offset >= len(self.code): - return offset - 1 - if not self._is_id_char(offset): - if offset > 0 and self._is_id_char(offset - 1): - return offset - 1 - if offset < len(self.code) - 1 and self._is_id_char(offset + 1): - return offset + 1 - return offset - - def _is_id_char(self, offset): - return self.code[offset].isalnum() or self.code[offset] == '_' - - def _find_string_start(self, offset): - kind = self.code[offset] - try: - return self.code.rindex(kind, 0, offset) - except ValueError: - return 0 - - def _find_parens_start(self, offset): - offset = self._find_last_non_space_char(offset - 1) - while offset >= 0 and self.code[offset] not in '[({': - if self.code[offset] not in ':,': - offset = self._find_primary_start(offset) - offset = self._find_last_non_space_char(offset - 1) - return offset - - def _find_atom_start(self, offset): - old_offset = offset - if self.code[offset] == '\n': - return offset + 1 - if self.code[offset].isspace(): - offset = self._find_last_non_space_char(offset) - if self.code[offset] in '\'"': - return self._find_string_start(offset) - if self.code[offset] in ')]}': - return self._find_parens_start(offset) - if self._is_id_char(offset): - return self._find_word_start(offset) - return old_offset - - def _find_primary_without_dot_start(self, offset): - """It tries to find the undotted primary start - - It is different from `self._get_atom_start()` in that it - follows function calls, too; such as in ``f(x)``. - - """ - last_atom = offset - offset = self._find_last_non_space_char(last_atom) - while offset > 0 and self.code[offset] in ')]': - last_atom = self._find_parens_start(offset) - offset = self._find_last_non_space_char(last_atom - 1) - if offset >= 0 and (self.code[offset] in '"\'})]' or - self._is_id_char(offset)): - atom_start = self._find_atom_start(offset) - if not keyword.iskeyword(self.code[atom_start:offset + 1]): - return atom_start - return last_atom - - def _find_primary_start(self, offset): - if offset >= len(self.code): - offset = len(self.code) - 1 - if self.code[offset] != '.': - offset = self._find_primary_without_dot_start(offset) - else: - offset = offset + 1 - while offset > 0: - prev = self._find_last_non_space_char(offset - 1) - if offset <= 0 or self.code[prev] != '.': - break - offset = self._find_primary_without_dot_start(prev - 1) - if not self._is_id_char(offset): - break - - return offset - - def get_primary_at(self, offset): - offset = self._get_fixed_offset(offset) - start, end = self.get_primary_range(offset) - return self.raw[start:end].strip() - - def get_splitted_primary_before(self, offset): - """returns expression, starting, starting_offset - - This function is used in `rope.codeassist.assist` function. - """ - if offset == 0: - return ('', '', 0) - end = offset - 1 - word_start = self._find_atom_start(end) - real_start = self._find_primary_start(end) - if self.code[word_start:offset].strip() == '': - word_start = end - if self.code[end].isspace(): - word_start = end - if self.code[real_start:word_start].strip() == '': - real_start = word_start - if real_start == word_start == end and not self._is_id_char(end): - return ('', '', offset) - if real_start == word_start: - return ('', self.raw[word_start:offset], word_start) - else: - if self.code[end] == '.': - return (self.raw[real_start:end], '', offset) - last_dot_position = word_start - if self.code[word_start] != '.': - last_dot_position = \ - self._find_last_non_space_char(word_start - 1) - last_char_position = \ - self._find_last_non_space_char(last_dot_position - 1) - if self.code[word_start].isspace(): - word_start = offset - return (self.raw[real_start:last_char_position + 1], - self.raw[word_start:offset], word_start) - - def _get_line_start(self, offset): - try: - return self.code.rindex('\n', 0, offset + 1) - except ValueError: - return 0 - - def _get_line_end(self, offset): - try: - return self.code.index('\n', offset) - except ValueError: - return len(self.code) - - def is_name_assigned_in_class_body(self, offset): - word_start = self._find_word_start(offset - 1) - word_end = self._find_word_end(offset) + 1 - if '.' in self.code[word_start:word_end]: - return False - line_start = self._get_line_start(word_start) - line = self.code[line_start:word_start].strip() - return not line and self.get_assignment_type(offset) == '=' - - def is_a_class_or_function_name_in_header(self, offset): - word_start = self._find_word_start(offset - 1) - line_start = self._get_line_start(word_start) - prev_word = self.code[line_start:word_start].strip() - return prev_word in ['def', 'class'] - - def _find_first_non_space_char(self, offset): - if offset >= len(self.code): - return len(self.code) - while offset < len(self.code) and self.code[offset].isspace(): - if self.code[offset] == '\n': - return offset - offset += 1 - return offset - - def is_a_function_being_called(self, offset): - word_end = self._find_word_end(offset) + 1 - next_char = self._find_first_non_space_char(word_end) - return next_char < len(self.code) and \ - self.code[next_char] == '(' and \ - not self.is_a_class_or_function_name_in_header(offset) - - def _find_import_end(self, start): - return self._get_line_end(start) - - def is_import_statement(self, offset): - try: - last_import = self.code.rindex('import ', 0, offset) - except ValueError: - return False - return self._find_import_end(last_import + 7) >= offset - - def is_from_statement(self, offset): - try: - last_from = self.code.rindex('from ', 0, offset) - from_import = self.code.index(' import ', last_from) - from_names = from_import + 8 - except ValueError: - return False - from_names = self._find_first_non_space_char(from_names) - return self._find_import_end(from_names) >= offset - - def is_from_statement_module(self, offset): - if offset >= len(self.code) - 1: - return False - stmt_start = self._find_primary_start(offset) - line_start = self._get_line_start(stmt_start) - prev_word = self.code[line_start:stmt_start].strip() - return prev_word == 'from' - - def is_a_name_after_from_import(self, offset): - try: - if len(self.code) > offset and self.code[offset] == '\n': - line_start = self._get_line_start(offset - 1) - else: - line_start = self._get_line_start(offset) - last_from = self.code.rindex('from ', line_start, offset) - from_import = self.code.index(' import ', last_from) - from_names = from_import + 8 - except ValueError: - return False - if from_names - 1 > offset: - return False - return self._find_import_end(from_names) >= offset - - def get_from_module(self, offset): - try: - last_from = self.code.rindex('from ', 0, offset) - import_offset = self.code.index(' import ', last_from) - end = self._find_last_non_space_char(import_offset) - return self.get_primary_at(end) - except ValueError: - pass - - def is_from_aliased(self, offset): - if not self.is_a_name_after_from_import(offset): - return False - try: - end = self._find_word_end(offset) - as_end = min(self._find_word_end(end + 1), len(self.code)) - as_start = self._find_word_start(as_end) - if self.code[as_start:as_end + 1] == 'as': - return True - except ValueError: - return False - - def get_from_aliased(self, offset): - try: - end = self._find_word_end(offset) - as_ = self._find_word_end(end + 1) - alias = self._find_word_end(as_ + 1) - start = self._find_word_start(alias) - return self.raw[start:alias + 1] - except ValueError: - pass - - def is_function_keyword_parameter(self, offset): - word_end = self._find_word_end(offset) - if word_end + 1 == len(self.code): - return False - next_char = self._find_first_non_space_char(word_end + 1) - equals = self.code[next_char:next_char + 2] - if equals == '==' or not equals.startswith('='): - return False - word_start = self._find_word_start(offset) - prev_char = self._find_last_non_space_char(word_start - 1) - return prev_char - 1 >= 0 and self.code[prev_char] in ',(' - - def is_on_function_call_keyword(self, offset): - stop = self._get_line_start(offset) - if self._is_id_char(offset): - offset = self._find_word_start(offset) - 1 - offset = self._find_last_non_space_char(offset) - if offset <= stop or self.code[offset] not in '(,': - return False - parens_start = self.find_parens_start_from_inside(offset) - return stop < parens_start - - def find_parens_start_from_inside(self, offset): - stop = self._get_line_start(offset) - while offset > stop: - if self.code[offset] == '(': - break - if self.code[offset] != ',': - offset = self._find_primary_start(offset) - offset -= 1 - return max(stop, offset) - - def is_assigned_here(self, offset): - return self.get_assignment_type(offset) is not None - - def get_assignment_type(self, offset): - # XXX: does not handle tuple assignments - word_end = self._find_word_end(offset) - next_char = self._find_first_non_space_char(word_end + 1) - single = self.code[next_char:next_char + 1] - double = self.code[next_char:next_char + 2] - triple = self.code[next_char:next_char + 3] - if double not in ('==', '<=', '>=', '!='): - for op in [single, double, triple]: - if op.endswith('='): - return op - - def get_primary_range(self, offset): - start = self._find_primary_start(offset) - end = self._find_word_end(offset) + 1 - return (start, end) - - def get_word_range(self, offset): - offset = max(0, offset) - start = self._find_word_start(offset) - end = self._find_word_end(offset) + 1 - return (start, end) - - def get_word_parens_range(self, offset, opening='(', closing=')'): - end = self._find_word_end(offset) - start_parens = self.code.index(opening, end) - index = start_parens - open_count = 0 - while index < len(self.code): - if self.code[index] == opening: - open_count += 1 - if self.code[index] == closing: - open_count -= 1 - if open_count == 0: - return (start_parens, index + 1) - index += 1 - return (start_parens, index) - - def get_parameters(self, first, last): - keywords = [] - args = [] - current = self._find_last_non_space_char(last - 1) - while current > first: - primary_start = current - current = self._find_primary_start(current) - while current != first and self.code[current] not in '=,': - current = self._find_last_non_space_char(current - 1) - primary = self.raw[current + 1:primary_start + 1].strip() - if self.code[current] == '=': - primary_start = current - 1 - current -= 1 - while current != first and self.code[current] not in ',': - current = self._find_last_non_space_char(current - 1) - param_name = self.raw[current + 1:primary_start + 1].strip() - keywords.append((param_name, primary)) - else: - args.append(primary) - current = self._find_last_non_space_char(current - 1) - args.reverse() - keywords.reverse() - return args, keywords - - def is_assigned_in_a_tuple_assignment(self, offset): - start = self._get_line_start(offset) - end = self._get_line_end(offset) - primary_start = self._find_primary_start(offset) - primary_end = self._find_word_end(offset) - - prev_char_offset = self._find_last_non_space_char(primary_start - 1) - next_char_offset = self._find_first_non_space_char(primary_end + 1) - next_char = prev_char = '' - if prev_char_offset >= start: - prev_char = self.code[prev_char_offset] - if next_char_offset < end: - next_char = self.code[next_char_offset] - try: - equals_offset = self.code.index('=', start, end) - except ValueError: - return False - if prev_char not in '(,' and next_char not in ',)': - return False - parens_start = self.find_parens_start_from_inside(offset) - # XXX: only handling (x, y) = value - return offset < equals_offset and \ - self.code[start:parens_start].strip() == '' - - def get_function_and_args_in_header(self, offset): - offset = self.find_function_offset(offset) - lparens, rparens = self.get_word_parens_range(offset) - return self.raw[offset:rparens + 1] - - def find_function_offset(self, offset, definition='def '): - while True: - offset = self.code.index(definition, offset) - if offset == 0 or not self._is_id_char(offset - 1): - break - offset += 1 - def_ = offset + 4 - return self._find_first_non_space_char(def_) - - def get_lambda_and_args(self, offset): - offset = self.find_function_offset(offset, definition='lambda ') - lparens, rparens = self.get_word_parens_range(offset, opening=' ', - closing=':') - return self.raw[offset:rparens + 1] diff --git a/pythonFiles/rope/contrib/__init__.py b/pythonFiles/rope/contrib/__init__.py deleted file mode 100644 index 0d3f837ef48a..000000000000 --- a/pythonFiles/rope/contrib/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -"""rope IDE tools package - -This package contains modules that can be used in IDEs -but do not depend on the UI. So these modules will be used -by `rope.ui` modules. - -""" diff --git a/pythonFiles/rope/contrib/autoimport.py b/pythonFiles/rope/contrib/autoimport.py deleted file mode 100644 index 9670080cf7f3..000000000000 --- a/pythonFiles/rope/contrib/autoimport.py +++ /dev/null @@ -1,222 +0,0 @@ -import re - -from rope.base import builtins -from rope.base import exceptions -from rope.base import libutils -from rope.base import pynames -from rope.base import pyobjects -from rope.base import resources -from rope.base import resourceobserver -from rope.base import taskhandle -from rope.refactor import importutils - - -class AutoImport(object): - """A class for finding the module that provides a name - - This class maintains a cache of global names in python modules. - Note that this cache is not accurate and might be out of date. - - """ - - def __init__(self, project, observe=True, underlined=False): - """Construct an AutoImport object - - If `observe` is `True`, listen for project changes and update - the cache. - - If `underlined` is `True`, underlined names are cached, too. - """ - self.project = project - self.underlined = underlined - self.names = project.data_files.read_data('globalnames') - if self.names is None: - self.names = {} - project.data_files.add_write_hook(self._write) - # XXX: using a filtered observer - observer = resourceobserver.ResourceObserver( - changed=self._changed, moved=self._moved, removed=self._removed) - if observe: - project.add_observer(observer) - - def import_assist(self, starting): - """Return a list of ``(name, module)`` tuples - - This function tries to find modules that have a global name - that starts with `starting`. - """ - # XXX: breaking if gave up! use generators - result = [] - for module in self.names: - for global_name in self.names[module]: - if global_name.startswith(starting): - result.append((global_name, module)) - return result - - def get_modules(self, name): - """Return the list of modules that have global `name`""" - result = [] - for module in self.names: - if name in self.names[module]: - result.append(module) - return result - - def get_all_names(self): - """Return the list of all cached global names""" - result = set() - for module in self.names: - result.update(set(self.names[module])) - return result - - def get_name_locations(self, name): - """Return a list of ``(resource, lineno)`` tuples""" - result = [] - for module in self.names: - if name in self.names[module]: - try: - pymodule = self.project.get_module(module) - if name in pymodule: - pyname = pymodule[name] - module, lineno = pyname.get_definition_location() - if module is not None: - resource = module.get_module().get_resource() - if resource is not None and lineno is not None: - result.append((resource, lineno)) - except exceptions.ModuleNotFoundError: - pass - return result - - def generate_cache(self, resources=None, underlined=None, - task_handle=taskhandle.NullTaskHandle()): - """Generate global name cache for project files - - If `resources` is a list of `rope.base.resource.File`\s, only - those files are searched; otherwise all python modules in the - project are cached. - - """ - if resources is None: - resources = self.project.get_python_files() - job_set = task_handle.create_jobset( - 'Generatig autoimport cache', len(resources)) - for file in resources: - job_set.started_job('Working on <%s>' % file.path) - self.update_resource(file, underlined) - job_set.finished_job() - - def generate_modules_cache(self, modules, underlined=None, - task_handle=taskhandle.NullTaskHandle()): - """Generate global name cache for modules listed in `modules`""" - job_set = task_handle.create_jobset( - 'Generatig autoimport cache for modules', len(modules)) - for modname in modules: - job_set.started_job('Working on <%s>' % modname) - if modname.endswith('.*'): - mod = self.project.find_module(modname[:-2]) - if mod: - for sub in submodules(mod): - self.update_resource(sub, underlined) - else: - self.update_module(modname, underlined) - job_set.finished_job() - - def clear_cache(self): - """Clear all entries in global-name cache - - It might be a good idea to use this function before - regenerating global names. - - """ - self.names.clear() - - def find_insertion_line(self, code): - """Guess at what line the new import should be inserted""" - match = re.search(r'^(def|class)\s+', code) - if match is not None: - code = code[:match.start()] - try: - pymodule = libutils.get_string_module(self.project, code) - except exceptions.ModuleSyntaxError: - return 1 - testmodname = '__rope_testmodule_rope' - importinfo = importutils.NormalImport(((testmodname, None),)) - module_imports = importutils.get_module_imports(self.project, - pymodule) - module_imports.add_import(importinfo) - code = module_imports.get_changed_source() - offset = code.index(testmodname) - lineno = code.count('\n', 0, offset) + 1 - return lineno - - def update_resource(self, resource, underlined=None): - """Update the cache for global names in `resource`""" - try: - pymodule = self.project.get_pymodule(resource) - modname = self._module_name(resource) - self._add_names(pymodule, modname, underlined) - except exceptions.ModuleSyntaxError: - pass - - def update_module(self, modname, underlined=None): - """Update the cache for global names in `modname` module - - `modname` is the name of a module. - """ - try: - pymodule = self.project.get_module(modname) - self._add_names(pymodule, modname, underlined) - except exceptions.ModuleNotFoundError: - pass - - def _module_name(self, resource): - return libutils.modname(resource) - - def _add_names(self, pymodule, modname, underlined): - if underlined is None: - underlined = self.underlined - globals = [] - if isinstance(pymodule, pyobjects.PyDefinedObject): - attributes = pymodule._get_structural_attributes() - else: - attributes = pymodule.get_attributes() - for name, pyname in attributes.items(): - if not underlined and name.startswith('_'): - continue - if isinstance(pyname, (pynames.AssignedName, pynames.DefinedName)): - globals.append(name) - if isinstance(pymodule, builtins.BuiltinModule): - globals.append(name) - self.names[modname] = globals - - def _write(self): - self.project.data_files.write_data('globalnames', self.names) - - def _changed(self, resource): - if not resource.is_folder(): - self.update_resource(resource) - - def _moved(self, resource, newresource): - if not resource.is_folder(): - modname = self._module_name(resource) - if modname in self.names: - del self.names[modname] - self.update_resource(newresource) - - def _removed(self, resource): - if not resource.is_folder(): - modname = self._module_name(resource) - if modname in self.names: - del self.names[modname] - - -def submodules(mod): - if isinstance(mod, resources.File): - if mod.name.endswith('.py') and mod.name != '__init__.py': - return set([mod]) - return set() - if not mod.has_child('__init__.py'): - return set() - result = set([mod]) - for child in mod.get_children(): - result |= submodules(child) - return result diff --git a/pythonFiles/rope/contrib/changestack.py b/pythonFiles/rope/contrib/changestack.py deleted file mode 100644 index 70f2271f7c64..000000000000 --- a/pythonFiles/rope/contrib/changestack.py +++ /dev/null @@ -1,52 +0,0 @@ -"""For performing many refactorings as a single command - -`changestack` module can be used to perform many refactorings on top -of each other as one bigger command. It can be used like:: - - stack = ChangeStack(project, 'my big command') - - #.. - stack.push(refactoring1.get_changes()) - #.. - stack.push(refactoring2.get_changes()) - #.. - stack.push(refactoringX.get_changes()) - - stack.pop_all() - changes = stack.merged() - -Now `changes` can be previewed or performed as before. -""" - -from rope.base import change - - -class ChangeStack(object): - - def __init__(self, project, description='merged changes'): - self.project = project - self.description = description - self.stack = [] - - def push(self, changes): - self.stack.append(changes) - self.project.do(changes) - - def pop_all(self): - for i in range(len(self.stack)): - self.project.history.undo(drop=True) - - def merged(self): - result = change.ChangeSet(self.description) - for changes in self.stack: - for c in self._basic_changes(changes): - result.add_change(c) - return result - - def _basic_changes(self, changes): - if isinstance(changes, change.ChangeSet): - for child in changes.changes: - for atom in self._basic_changes(child): - yield atom - else: - yield changes diff --git a/pythonFiles/rope/contrib/codeassist.py b/pythonFiles/rope/contrib/codeassist.py deleted file mode 100644 index 92c1bfc275c7..000000000000 --- a/pythonFiles/rope/contrib/codeassist.py +++ /dev/null @@ -1,695 +0,0 @@ -import keyword -import sys -import warnings - -import rope.base.codeanalyze -import rope.base.evaluate -from rope.base import builtins -from rope.base import exceptions -from rope.base import libutils -from rope.base import pynames -from rope.base import pynamesdef -from rope.base import pyobjects -from rope.base import pyobjectsdef -from rope.base import pyscopes -from rope.base import worder -from rope.contrib import fixsyntax -from rope.refactor import functionutils - - -def code_assist(project, source_code, offset, resource=None, - templates=None, maxfixes=1, later_locals=True): - """Return python code completions as a list of `CodeAssistProposal`\s - - `resource` is a `rope.base.resources.Resource` object. If - provided, relative imports are handled. - - `maxfixes` is the maximum number of errors to fix if the code has - errors in it. - - If `later_locals` is `False` names defined in this scope and after - this line is ignored. - - """ - if templates is not None: - warnings.warn('Codeassist no longer supports templates', - DeprecationWarning, stacklevel=2) - assist = _PythonCodeAssist( - project, source_code, offset, resource=resource, - maxfixes=maxfixes, later_locals=later_locals) - return assist() - - -def starting_offset(source_code, offset): - """Return the offset in which the completion should be inserted - - Usually code assist proposals should be inserted like:: - - completion = proposal.name - result = (source_code[:starting_offset] + - completion + source_code[offset:]) - - Where starting_offset is the offset returned by this function. - - """ - word_finder = worder.Worder(source_code, True) - expression, starting, starting_offset = \ - word_finder.get_splitted_primary_before(offset) - return starting_offset - - -def get_doc(project, source_code, offset, resource=None, maxfixes=1): - """Get the pydoc""" - fixer = fixsyntax.FixSyntax(project, source_code, resource, maxfixes) - pyname = fixer.pyname_at(offset) - if pyname is None: - return None - pyobject = pyname.get_object() - return PyDocExtractor().get_doc(pyobject) - - -def get_calltip(project, source_code, offset, resource=None, - maxfixes=1, ignore_unknown=False, remove_self=False): - """Get the calltip of a function - - The format of the returned string is - ``module_name.holding_scope_names.function_name(arguments)``. For - classes `__init__()` and for normal objects `__call__()` function - is used. - - Note that the offset is on the function itself *not* after the its - open parenthesis. (Actually it used to be the other way but it - was easily confused when string literals were involved. So I - decided it is better for it not to try to be too clever when it - cannot be clever enough). You can use a simple search like:: - - offset = source_code.rindex('(', 0, offset) - 1 - - to handle simple situations. - - If `ignore_unknown` is `True`, `None` is returned for functions - without source-code like builtins and extensions. - - If `remove_self` is `True`, the first parameter whose name is self - will be removed for methods. - """ - fixer = fixsyntax.FixSyntax(project, source_code, resource, maxfixes) - pyname = fixer.pyname_at(offset) - if pyname is None: - return None - pyobject = pyname.get_object() - return PyDocExtractor().get_calltip(pyobject, ignore_unknown, remove_self) - - -def get_definition_location(project, source_code, offset, - resource=None, maxfixes=1): - """Return the definition location of the python name at `offset` - - Return a (`rope.base.resources.Resource`, lineno) tuple. If no - `resource` is given and the definition is inside the same module, - the first element of the returned tuple would be `None`. If the - location cannot be determined ``(None, None)`` is returned. - - """ - fixer = fixsyntax.FixSyntax(project, source_code, resource, maxfixes) - pyname = fixer.pyname_at(offset) - if pyname is not None: - module, lineno = pyname.get_definition_location() - if module is not None: - return module.get_module().get_resource(), lineno - return (None, None) - - -def find_occurrences(*args, **kwds): - import rope.contrib.findit - warnings.warn('Use `rope.contrib.findit.find_occurrences()` instead', - DeprecationWarning, stacklevel=2) - return rope.contrib.findit.find_occurrences(*args, **kwds) - - -def get_canonical_path(project, resource, offset): - """Get the canonical path to an object. - - Given the offset of the object, this returns a list of - (name, name_type) tuples representing the canonical path to the - object. For example, the 'x' in the following code: - - class Foo(object): - def bar(self): - class Qux(object): - def mux(self, x): - pass - - we will return: - - [('Foo', 'CLASS'), ('bar', 'FUNCTION'), ('Qux', 'CLASS'), - ('mux', 'FUNCTION'), ('x', 'PARAMETER')] - - `resource` is a `rope.base.resources.Resource` object. - - `offset` is the offset of the pyname you want the path to. - - """ - # Retrieve the PyName. - pymod = project.get_pymodule(resource) - pyname = rope.base.evaluate.eval_location(pymod, offset) - - # Now get the location of the definition and its containing scope. - defmod, lineno = pyname.get_definition_location() - if not defmod: - return None - scope = defmod.get_scope().get_inner_scope_for_line(lineno) - - # Start with the name of the object we're interested in. - names = [] - if isinstance(pyname, pynamesdef.ParameterName): - names = [(worder.get_name_at(pymod.get_resource(), offset), - 'PARAMETER') ] - elif isinstance(pyname, pynamesdef.AssignedName): - names = [(worder.get_name_at(pymod.get_resource(), offset), - 'VARIABLE')] - - # Collect scope names. - while scope.parent: - if isinstance(scope, pyscopes.FunctionScope): - scope_type = 'FUNCTION' - elif isinstance(scope, pyscopes.ClassScope): - scope_type = 'CLASS' - else: - scope_type = None - names.append((scope.pyobject.get_name(), scope_type)) - scope = scope.parent - - names.append((defmod.get_resource().real_path, 'MODULE')) - names.reverse() - return names - - -class CompletionProposal(object): - """A completion proposal - - The `scope` instance variable shows where proposed name came from - and can be 'global', 'local', 'builtin', 'attribute', 'keyword', - 'imported', 'parameter_keyword'. - - The `type` instance variable shows the approximate type of the - proposed object and can be 'instance', 'class', 'function', 'module', - and `None`. - - All possible relations between proposal's `scope` and `type` are shown - in the table below (different scopes in rows and types in columns): - - | instance | class | function | module | None - local | + | + | + | + | - global | + | + | + | + | - builtin | + | + | + | | - attribute | + | + | + | + | - imported | + | + | + | + | - keyword | | | | | + - parameter_keyword | | | | | + - - """ - - def __init__(self, name, scope, pyname=None): - self.name = name - self.pyname = pyname - self.scope = self._get_scope(scope) - - def __str__(self): - return '%s (%s, %s)' % (self.name, self.scope, self.type) - - def __repr__(self): - return str(self) - - @property - def parameters(self): - """The names of the parameters the function takes. - - Returns None if this completion is not a function. - """ - pyname = self.pyname - if isinstance(pyname, pynames.ImportedName): - pyname = pyname._get_imported_pyname() - if isinstance(pyname, pynames.DefinedName): - pyobject = pyname.get_object() - if isinstance(pyobject, pyobjects.AbstractFunction): - return pyobject.get_param_names() - - @property - def type(self): - pyname = self.pyname - if isinstance(pyname, builtins.BuiltinName): - pyobject = pyname.get_object() - if isinstance(pyobject, builtins.BuiltinFunction): - return 'function' - elif isinstance(pyobject, builtins.BuiltinClass): - return 'class' - elif isinstance(pyobject, builtins.BuiltinObject) or \ - isinstance(pyobject, builtins.BuiltinName): - return 'instance' - elif isinstance(pyname, pynames.ImportedModule): - return 'module' - elif isinstance(pyname, pynames.ImportedName) or \ - isinstance(pyname, pynames.DefinedName): - pyobject = pyname.get_object() - if isinstance(pyobject, pyobjects.AbstractFunction): - return 'function' - if isinstance(pyobject, pyobjects.AbstractClass): - return 'class' - return 'instance' - - def _get_scope(self, scope): - if isinstance(self.pyname, builtins.BuiltinName): - return 'builtin' - if isinstance(self.pyname, pynames.ImportedModule) or \ - isinstance(self.pyname, pynames.ImportedName): - return 'imported' - return scope - - def get_doc(self): - """Get the proposed object's docstring. - - Returns None if it can not be get. - """ - if not self.pyname: - return None - pyobject = self.pyname.get_object() - if not hasattr(pyobject, 'get_doc'): - return None - return self.pyname.get_object().get_doc() - - @property - def kind(self): - warnings.warn("the proposal's `kind` property is deprecated, " - "use `scope` instead") - return self.scope - - -# leaved for backward compatibility -CodeAssistProposal = CompletionProposal - - -class NamedParamProposal(CompletionProposal): - """A parameter keyword completion proposal - - Holds reference to ``_function`` -- the function which - parameter ``name`` belongs to. This allows to determine - default value for this parameter. - """ - def __init__(self, name, function): - self.argname = name - name = '%s=' % name - super(NamedParamProposal, self).__init__(name, 'parameter_keyword') - self._function = function - - def get_default(self): - """Get a string representation of a param's default value. - - Returns None if there is no default value for this param. - """ - definfo = functionutils.DefinitionInfo.read(self._function) - for arg, default in definfo.args_with_defaults: - if self.argname == arg: - return default - return None - - -def sorted_proposals(proposals, scopepref=None, typepref=None): - """Sort a list of proposals - - Return a sorted list of the given `CodeAssistProposal`\s. - - `scopepref` can be a list of proposal scopes. Defaults to - ``['parameter_keyword', 'local', 'global', 'imported', - 'attribute', 'builtin', 'keyword']``. - - `typepref` can be a list of proposal types. Defaults to - ``['class', 'function', 'instance', 'module', None]``. - (`None` stands for completions with no type like keywords.) - """ - sorter = _ProposalSorter(proposals, scopepref, typepref) - return sorter.get_sorted_proposal_list() - - -def starting_expression(source_code, offset): - """Return the expression to complete""" - word_finder = worder.Worder(source_code, True) - expression, starting, starting_offset = \ - word_finder.get_splitted_primary_before(offset) - if expression: - return expression + '.' + starting - return starting - - -def default_templates(): - warnings.warn('default_templates() is deprecated.', - DeprecationWarning, stacklevel=2) - return {} - - -class _PythonCodeAssist(object): - - def __init__(self, project, source_code, offset, resource=None, - maxfixes=1, later_locals=True): - self.project = project - self.code = source_code - self.resource = resource - self.maxfixes = maxfixes - self.later_locals = later_locals - self.word_finder = worder.Worder(source_code, True) - self.expression, self.starting, self.offset = \ - self.word_finder.get_splitted_primary_before(offset) - - keywords = keyword.kwlist - - def _find_starting_offset(self, source_code, offset): - current_offset = offset - 1 - while current_offset >= 0 and (source_code[current_offset].isalnum() or - source_code[current_offset] in '_'): - current_offset -= 1 - return current_offset + 1 - - def _matching_keywords(self, starting): - result = [] - for kw in self.keywords: - if kw.startswith(starting): - result.append(CompletionProposal(kw, 'keyword')) - return result - - def __call__(self): - if self.offset > len(self.code): - return [] - completions = list(self._code_completions().values()) - if self.expression.strip() == '' and self.starting.strip() != '': - completions.extend(self._matching_keywords(self.starting)) - return completions - - def _dotted_completions(self, module_scope, holding_scope): - result = {} - found_pyname = rope.base.evaluate.eval_str(holding_scope, - self.expression) - if found_pyname is not None: - element = found_pyname.get_object() - compl_scope = 'attribute' - if isinstance(element, (pyobjectsdef.PyModule, - pyobjectsdef.PyPackage)): - compl_scope = 'imported' - for name, pyname in element.get_attributes().items(): - if name.startswith(self.starting): - result[name] = CompletionProposal(name, compl_scope, - pyname) - return result - - def _undotted_completions(self, scope, result, lineno=None): - if scope.parent is not None: - self._undotted_completions(scope.parent, result) - if lineno is None: - names = scope.get_propagated_names() - else: - names = scope.get_names() - for name, pyname in names.items(): - if name.startswith(self.starting): - compl_scope = 'local' - if scope.get_kind() == 'Module': - compl_scope = 'global' - if lineno is None or self.later_locals or \ - not self._is_defined_after(scope, pyname, lineno): - result[name] = CompletionProposal(name, compl_scope, - pyname) - - def _from_import_completions(self, pymodule): - module_name = self.word_finder.get_from_module(self.offset) - if module_name is None: - return {} - pymodule = self._find_module(pymodule, module_name) - result = {} - for name in pymodule: - if name.startswith(self.starting): - result[name] = CompletionProposal(name, scope='global', - pyname=pymodule[name]) - return result - - def _find_module(self, pymodule, module_name): - dots = 0 - while module_name[dots] == '.': - dots += 1 - pyname = pynames.ImportedModule(pymodule, - module_name[dots:], dots) - return pyname.get_object() - - def _is_defined_after(self, scope, pyname, lineno): - location = pyname.get_definition_location() - if location is not None and location[1] is not None: - if location[0] == scope.pyobject.get_module() and \ - lineno <= location[1] <= scope.get_end(): - return True - - def _code_completions(self): - lineno = self.code.count('\n', 0, self.offset) + 1 - fixer = fixsyntax.FixSyntax(self.project, self.code, - self.resource, self.maxfixes) - pymodule = fixer.get_pymodule() - module_scope = pymodule.get_scope() - code = pymodule.source_code - lines = code.split('\n') - result = {} - start = fixsyntax._logical_start(lines, lineno) - indents = fixsyntax._get_line_indents(lines[start - 1]) - inner_scope = module_scope.get_inner_scope_for_line(start, indents) - if self.word_finder.is_a_name_after_from_import(self.offset): - return self._from_import_completions(pymodule) - if self.expression.strip() != '': - result.update(self._dotted_completions(module_scope, inner_scope)) - else: - result.update(self._keyword_parameters(module_scope.pyobject, - inner_scope)) - self._undotted_completions(inner_scope, result, lineno=lineno) - return result - - def _keyword_parameters(self, pymodule, scope): - offset = self.offset - if offset == 0: - return {} - word_finder = worder.Worder(self.code, True) - if word_finder.is_on_function_call_keyword(offset - 1): - function_parens = word_finder.\ - find_parens_start_from_inside(offset - 1) - primary = word_finder.get_primary_at(function_parens - 1) - try: - function_pyname = rope.base.evaluate.\ - eval_str(scope, primary) - except exceptions.BadIdentifierError: - return {} - if function_pyname is not None: - pyobject = function_pyname.get_object() - if isinstance(pyobject, pyobjects.AbstractFunction): - pass - elif isinstance(pyobject, pyobjects.AbstractClass) and \ - '__init__' in pyobject: - pyobject = pyobject['__init__'].get_object() - elif '__call__' in pyobject: - pyobject = pyobject['__call__'].get_object() - if isinstance(pyobject, pyobjects.AbstractFunction): - param_names = [] - param_names.extend( - pyobject.get_param_names(special_args=False)) - result = {} - for name in param_names: - if name.startswith(self.starting): - result[name + '='] = NamedParamProposal( - name, pyobject - ) - return result - return {} - - -class _ProposalSorter(object): - """Sort a list of code assist proposals""" - - def __init__(self, code_assist_proposals, scopepref=None, typepref=None): - self.proposals = code_assist_proposals - if scopepref is None: - scopepref = ['parameter_keyword', 'local', 'global', 'imported', - 'attribute', 'builtin', 'keyword'] - self.scopepref = scopepref - if typepref is None: - typepref = ['class', 'function', 'instance', 'module', None] - self.typerank = dict((type, index) - for index, type in enumerate(typepref)) - - def get_sorted_proposal_list(self): - """Return a list of `CodeAssistProposal`""" - proposals = {} - for proposal in self.proposals: - proposals.setdefault(proposal.scope, []).append(proposal) - result = [] - for scope in self.scopepref: - scope_proposals = proposals.get(scope, []) - scope_proposals = [proposal for proposal in scope_proposals - if proposal.type in self.typerank] - scope_proposals.sort(key=self._proposal_key) - result.extend(scope_proposals) - return result - - def _proposal_key(self, proposal1): - def _underline_count(name): - return sum(1 for c in name if c == "_") - return (self.typerank.get(proposal1.type, 100), - _underline_count(proposal1.name), - proposal1.name) - #if proposal1.type != proposal2.type: - # return cmp(self.typerank.get(proposal1.type, 100), - # self.typerank.get(proposal2.type, 100)) - #return self._compare_underlined_names(proposal1.name, - # proposal2.name) - - -class PyDocExtractor(object): - - def get_doc(self, pyobject): - if isinstance(pyobject, pyobjects.AbstractFunction): - return self._get_function_docstring(pyobject) - elif isinstance(pyobject, pyobjects.AbstractClass): - return self._get_class_docstring(pyobject) - elif isinstance(pyobject, pyobjects.AbstractModule): - return self._trim_docstring(pyobject.get_doc()) - return None - - def get_calltip(self, pyobject, ignore_unknown=False, remove_self=False): - try: - if isinstance(pyobject, pyobjects.AbstractClass): - pyobject = pyobject['__init__'].get_object() - if not isinstance(pyobject, pyobjects.AbstractFunction): - pyobject = pyobject['__call__'].get_object() - except exceptions.AttributeNotFoundError: - return None - if ignore_unknown and not isinstance(pyobject, pyobjects.PyFunction): - return - if isinstance(pyobject, pyobjects.AbstractFunction): - result = self._get_function_signature(pyobject, add_module=True) - if remove_self and self._is_method(pyobject): - return result.replace('(self)', '()').replace('(self, ', '(') - return result - - def _get_class_docstring(self, pyclass): - contents = self._trim_docstring(pyclass.get_doc(), 2) - supers = [super.get_name() for super in pyclass.get_superclasses()] - doc = 'class %s(%s):\n\n' % (pyclass.get_name(), ', '.join(supers)) \ - + contents - - if '__init__' in pyclass: - init = pyclass['__init__'].get_object() - if isinstance(init, pyobjects.AbstractFunction): - doc += '\n\n' + self._get_single_function_docstring(init) - return doc - - def _get_function_docstring(self, pyfunction): - functions = [pyfunction] - if self._is_method(pyfunction): - functions.extend(self._get_super_methods(pyfunction.parent, - pyfunction.get_name())) - return '\n\n'.join([self._get_single_function_docstring(function) - for function in functions]) - - def _is_method(self, pyfunction): - return isinstance(pyfunction, pyobjects.PyFunction) and \ - isinstance(pyfunction.parent, pyobjects.PyClass) - - def _get_single_function_docstring(self, pyfunction): - signature = self._get_function_signature(pyfunction) - docs = self._trim_docstring(pyfunction.get_doc(), indents=2) - return signature + ':\n\n' + docs - - def _get_super_methods(self, pyclass, name): - result = [] - for super_class in pyclass.get_superclasses(): - if name in super_class: - function = super_class[name].get_object() - if isinstance(function, pyobjects.AbstractFunction): - result.append(function) - result.extend(self._get_super_methods(super_class, name)) - return result - - def _get_function_signature(self, pyfunction, add_module=False): - location = self._location(pyfunction, add_module) - if isinstance(pyfunction, pyobjects.PyFunction): - info = functionutils.DefinitionInfo.read(pyfunction) - return location + info.to_string() - else: - return '%s(%s)' % (location + pyfunction.get_name(), - ', '.join(pyfunction.get_param_names())) - - def _location(self, pyobject, add_module=False): - location = [] - parent = pyobject.parent - while parent and not isinstance(parent, pyobjects.AbstractModule): - location.append(parent.get_name()) - location.append('.') - parent = parent.parent - if add_module: - if isinstance(pyobject, pyobjects.PyFunction): - location.insert(0, self._get_module(pyobject)) - if isinstance(parent, builtins.BuiltinModule): - location.insert(0, parent.get_name() + '.') - return ''.join(location) - - def _get_module(self, pyfunction): - module = pyfunction.get_module() - if module is not None: - resource = module.get_resource() - if resource is not None: - return libutils.modname(resource) + '.' - return '' - - def _trim_docstring(self, docstring, indents=0): - """The sample code from :PEP:`257`""" - if not docstring: - return '' - # Convert tabs to spaces (following normal Python rules) - # and split into a list of lines: - lines = docstring.expandtabs().splitlines() - # Determine minimum indentation (first line doesn't count): - indent = sys.maxsize - for line in lines[1:]: - stripped = line.lstrip() - if stripped: - indent = min(indent, len(line) - len(stripped)) - # Remove indentation (first line is special): - trimmed = [lines[0].strip()] - if indent < sys.maxsize: - for line in lines[1:]: - trimmed.append(line[indent:].rstrip()) - # Strip off trailing and leading blank lines: - while trimmed and not trimmed[-1]: - trimmed.pop() - while trimmed and not trimmed[0]: - trimmed.pop(0) - # Return a single string: - return '\n'.join((' ' * indents + line for line in trimmed)) - - -# Deprecated classes - -class TemplateProposal(CodeAssistProposal): - def __init__(self, name, template): - warnings.warn('TemplateProposal is deprecated.', - DeprecationWarning, stacklevel=2) - super(TemplateProposal, self).__init__(name, 'template') - self.template = template - - -class Template(object): - - def __init__(self, template): - self.template = template - warnings.warn('Template is deprecated.', - DeprecationWarning, stacklevel=2) - - def variables(self): - return [] - - def substitute(self, mapping): - return self.template - - def get_cursor_location(self, mapping): - return len(self.template) diff --git a/pythonFiles/rope/contrib/finderrors.py b/pythonFiles/rope/contrib/finderrors.py deleted file mode 100644 index 109a3e8ac724..000000000000 --- a/pythonFiles/rope/contrib/finderrors.py +++ /dev/null @@ -1,91 +0,0 @@ -"""Finding bad name and attribute accesses - -`find_errors` function can be used to find possible bad name and -attribute accesses. As an example:: - - errors = find_errors(project, project.get_resource('mod.py')) - for error in errors: - print('%s: %s' % (error.lineno, error.error)) - -prints possible errors for ``mod.py`` file. - -TODO: - -* use task handles -* reporting names at most once -* attributes of extension modules that don't appear in - extension_modules project config can be ignored -* not calling `PyScope.get_inner_scope_for_line()` if it is a - bottleneck; needs profiling -* not reporting occurrences where rope cannot infer the object -* rope saves multiple objects for some of the names in its objectdb - use all of them not to give false positives -* ... ;-) - -""" -from rope.base import ast, evaluate, pyobjects - - -def find_errors(project, resource): - """Find possible bad name and attribute accesses - - It returns a list of `Error`\s. - """ - pymodule = project.get_pymodule(resource) - finder = _BadAccessFinder(pymodule) - ast.walk(pymodule.get_ast(), finder) - return finder.errors - - -class _BadAccessFinder(object): - - def __init__(self, pymodule): - self.pymodule = pymodule - self.scope = pymodule.get_scope() - self.errors = [] - - def _Name(self, node): - if isinstance(node.ctx, (ast.Store, ast.Param)): - return - scope = self.scope.get_inner_scope_for_line(node.lineno) - pyname = scope.lookup(node.id) - if pyname is None: - self._add_error(node, 'Unresolved variable') - elif self._is_defined_after(scope, pyname, node.lineno): - self._add_error(node, 'Defined later') - - def _Attribute(self, node): - if not isinstance(node.ctx, ast.Store): - scope = self.scope.get_inner_scope_for_line(node.lineno) - pyname = evaluate.eval_node(scope, node.value) - if pyname is not None and \ - pyname.get_object() != pyobjects.get_unknown(): - if node.attr not in pyname.get_object(): - self._add_error(node, 'Unresolved attribute') - ast.walk(node.value, self) - - def _add_error(self, node, msg): - if isinstance(node, ast.Attribute): - name = node.attr - else: - name = node.id - if name != 'None': - error = Error(node.lineno, msg + ' ' + name) - self.errors.append(error) - - def _is_defined_after(self, scope, pyname, lineno): - location = pyname.get_definition_location() - if location is not None and location[1] is not None: - if location[0] == self.pymodule and \ - lineno <= location[1] <= scope.get_end(): - return True - - -class Error(object): - - def __init__(self, lineno, error): - self.lineno = lineno - self.error = error - - def __str__(self): - return '%s: %s' % (self.lineno, self.error) diff --git a/pythonFiles/rope/contrib/findit.py b/pythonFiles/rope/contrib/findit.py deleted file mode 100644 index 93eb01a84676..000000000000 --- a/pythonFiles/rope/contrib/findit.py +++ /dev/null @@ -1,114 +0,0 @@ -import rope.base.codeanalyze -import rope.base.evaluate -import rope.base.pyobjects -from rope.base import taskhandle, exceptions, worder -from rope.contrib import fixsyntax -from rope.refactor import occurrences - - -def find_occurrences(project, resource, offset, unsure=False, resources=None, - in_hierarchy=False, - task_handle=taskhandle.NullTaskHandle()): - """Return a list of `Location`\s - - If `unsure` is `True`, possible matches are returned, too. You - can use `Location.unsure` to see which are unsure occurrences. - `resources` can be a list of `rope.base.resource.File`\s that - should be searched for occurrences; if `None` all python files - in the project are searched. - - """ - name = worder.get_name_at(resource, offset) - this_pymodule = project.get_pymodule(resource) - primary, pyname = rope.base.evaluate.eval_location2( - this_pymodule, offset) - - def is_match(occurrence): - return unsure - finder = occurrences.create_finder( - project, name, pyname, unsure=is_match, - in_hierarchy=in_hierarchy, instance=primary) - if resources is None: - resources = project.get_python_files() - job_set = task_handle.create_jobset('Finding Occurrences', - count=len(resources)) - return _find_locations(finder, resources, job_set) - - -def find_implementations(project, resource, offset, resources=None, - task_handle=taskhandle.NullTaskHandle()): - """Find the places a given method is overridden. - - Finds the places a method is implemented. Returns a list of - `Location`\s. - """ - name = worder.get_name_at(resource, offset) - this_pymodule = project.get_pymodule(resource) - pyname = rope.base.evaluate.eval_location(this_pymodule, offset) - if pyname is not None: - pyobject = pyname.get_object() - if not isinstance(pyobject, rope.base.pyobjects.PyFunction) or \ - pyobject.get_kind() != 'method': - raise exceptions.BadIdentifierError('Not a method!') - else: - raise exceptions.BadIdentifierError('Cannot resolve the identifier!') - - def is_defined(occurrence): - if not occurrence.is_defined(): - return False - - def not_self(occurrence): - if occurrence.get_pyname().get_object() == pyname.get_object(): - return False - filters = [is_defined, not_self, - occurrences.InHierarchyFilter(pyname, True)] - finder = occurrences.Finder(project, name, filters=filters) - if resources is None: - resources = project.get_python_files() - job_set = task_handle.create_jobset('Finding Implementations', - count=len(resources)) - return _find_locations(finder, resources, job_set) - - -def find_definition(project, code, offset, resource=None, maxfixes=1): - """Return the definition location of the python name at `offset` - - A `Location` object is returned if the definition location can be - determined, otherwise ``None`` is returned. - """ - fixer = fixsyntax.FixSyntax(project, code, resource, maxfixes) - pyname = fixer.pyname_at(offset) - if pyname is not None: - module, lineno = pyname.get_definition_location() - name = rope.base.worder.Worder(code).get_word_at(offset) - if lineno is not None: - start = module.lines.get_line_start(lineno) - - def check_offset(occurrence): - if occurrence.offset < start: - return False - pyname_filter = occurrences.PyNameFilter(pyname) - finder = occurrences.Finder(project, name, - [check_offset, pyname_filter]) - for occurrence in finder.find_occurrences(pymodule=module): - return Location(occurrence) - - -class Location(object): - - def __init__(self, occurrence): - self.resource = occurrence.resource - self.region = occurrence.get_word_range() - self.offset = self.region[0] - self.unsure = occurrence.is_unsure() - self.lineno = occurrence.lineno - - -def _find_locations(finder, resources, job_set): - result = [] - for resource in resources: - job_set.started_job(resource.path) - for occurrence in finder.find_occurrences(resource): - result.append(Location(occurrence)) - job_set.finished_job() - return result diff --git a/pythonFiles/rope/contrib/fixmodnames.py b/pythonFiles/rope/contrib/fixmodnames.py deleted file mode 100644 index d8bd3da109e0..000000000000 --- a/pythonFiles/rope/contrib/fixmodnames.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Fix the name of modules - -This module is useful when you want to rename many of the modules in -your project. That can happen specially when you want to change their -naming style. - -For instance:: - - fixer = FixModuleNames(project) - changes = fixer.get_changes(fixer=str.lower) - project.do(changes) - -Here it renames all modules and packages to use lower-cased chars. -You can tell it to use any other style by using the ``fixer`` -argument. - -""" -from rope.base import taskhandle -from rope.contrib import changestack -from rope.refactor import rename - - -class FixModuleNames(object): - - def __init__(self, project): - self.project = project - - def get_changes(self, fixer=str.lower, - task_handle=taskhandle.NullTaskHandle()): - """Fix module names - - `fixer` is a function that takes and returns a `str`. Given - the name of a module, it should return the fixed name. - - """ - stack = changestack.ChangeStack(self.project, 'Fixing module names') - jobset = task_handle.create_jobset('Fixing module names', - self._count_fixes(fixer) + 1) - try: - while True: - for resource in self._tobe_fixed(fixer): - jobset.started_job(resource.path) - renamer = rename.Rename(self.project, resource) - changes = renamer.get_changes(fixer(self._name(resource))) - stack.push(changes) - jobset.finished_job() - break - else: - break - finally: - jobset.started_job('Reverting to original state') - stack.pop_all() - jobset.finished_job() - return stack.merged() - - def _count_fixes(self, fixer): - return len(list(self._tobe_fixed(fixer))) - - def _tobe_fixed(self, fixer): - for resource in self.project.get_python_files(): - modname = self._name(resource) - if modname != fixer(modname): - yield resource - - def _name(self, resource): - modname = resource.name.rsplit('.', 1)[0] - if modname == '__init__': - modname = resource.parent.name - return modname diff --git a/pythonFiles/rope/contrib/fixsyntax.py b/pythonFiles/rope/contrib/fixsyntax.py deleted file mode 100644 index fa2a17d93c12..000000000000 --- a/pythonFiles/rope/contrib/fixsyntax.py +++ /dev/null @@ -1,181 +0,0 @@ -import rope.base.codeanalyze -import rope.base.evaluate -from rope.base import exceptions -from rope.base import libutils -from rope.base import utils -from rope.base import worder -from rope.base.codeanalyze import ArrayLinesAdapter, LogicalLineFinder - - -class FixSyntax(object): - - def __init__(self, project, code, resource, maxfixes=1): - self.project = project - self.code = code - self.resource = resource - self.maxfixes = maxfixes - - @utils.saveit - def get_pymodule(self): - """Get a `PyModule`""" - msg = None - code = self.code - tries = 0 - while True: - try: - if tries == 0 and self.resource is not None and \ - self.resource.read() == code: - return self.project.get_pymodule(self.resource, - force_errors=True) - return libutils.get_string_module( - self.project, code, resource=self.resource, - force_errors=True) - except exceptions.ModuleSyntaxError as e: - if msg is None: - msg = '%s:%s %s' % (e.filename, e.lineno, e.message_) - if tries < self.maxfixes: - tries += 1 - self.commenter.comment(e.lineno) - code = '\n'.join(self.commenter.lines) - else: - raise exceptions.ModuleSyntaxError( - e.filename, e.lineno, - 'Failed to fix error: {0}'.format(msg)) - - @property - @utils.saveit - def commenter(self): - return _Commenter(self.code) - - def pyname_at(self, offset): - pymodule = self.get_pymodule() - - def old_pyname(): - word_finder = worder.Worder(self.code, True) - expression = word_finder.get_primary_at(offset) - expression = expression.replace('\\\n', ' ').replace('\n', ' ') - lineno = self.code.count('\n', 0, offset) - scope = pymodule.get_scope().get_inner_scope_for_line(lineno) - return rope.base.evaluate.eval_str(scope, expression) - new_code = pymodule.source_code - - def new_pyname(): - newoffset = self.commenter.transfered_offset(offset) - return rope.base.evaluate.eval_location(pymodule, newoffset) - if new_code.startswith(self.code[:offset + 1]): - return new_pyname() - result = old_pyname() - if result is None: - return new_pyname() - return result - - -class _Commenter(object): - - def __init__(self, code): - self.code = code - self.lines = self.code.split('\n') - self.lines.append('\n') - self.origs = list(range(len(self.lines) + 1)) - self.diffs = [0] * (len(self.lines) + 1) - - def comment(self, lineno): - start = _logical_start(self.lines, lineno, check_prev=True) - 1 - # using self._get_stmt_end() instead of self._get_block_end() - # to lower commented lines - end = self._get_stmt_end(start) - indents = _get_line_indents(self.lines[start]) - if 0 < start: - last_lineno = self._last_non_blank(start - 1) - last_line = self.lines[last_lineno] - if last_line.rstrip().endswith(':'): - indents = _get_line_indents(last_line) + 4 - self._set(start, ' ' * indents + 'pass') - for line in range(start + 1, end + 1): - self._set(line, self.lines[start]) - self._fix_incomplete_try_blocks(lineno, indents) - - def transfered_offset(self, offset): - lineno = self.code.count('\n', 0, offset) - diff = sum(self.diffs[:lineno]) - return offset + diff - - def _last_non_blank(self, start): - while start > 0 and self.lines[start].strip() == '': - start -= 1 - return start - - def _get_block_end(self, lineno): - end_line = lineno - base_indents = _get_line_indents(self.lines[lineno]) - for i in range(lineno + 1, len(self.lines)): - if _get_line_indents(self.lines[i]) >= base_indents: - end_line = i - else: - break - return end_line - - def _get_stmt_end(self, lineno): - base_indents = _get_line_indents(self.lines[lineno]) - for i in range(lineno + 1, len(self.lines)): - if _get_line_indents(self.lines[i]) <= base_indents: - return i - 1 - return lineno - - def _fix_incomplete_try_blocks(self, lineno, indents): - block_start = lineno - last_indents = indents - while block_start > 0: - block_start = rope.base.codeanalyze.get_block_start( - ArrayLinesAdapter(self.lines), block_start) - 1 - if self.lines[block_start].strip().startswith('try:'): - indents = _get_line_indents(self.lines[block_start]) - if indents > last_indents: - continue - last_indents = indents - block_end = self._find_matching_deindent(block_start) - line = self.lines[block_end].strip() - if not (line.startswith('finally:') or - line.startswith('except ') or - line.startswith('except:')): - self._insert(block_end, ' ' * indents + 'finally:') - self._insert(block_end + 1, ' ' * indents + ' pass') - - def _find_matching_deindent(self, line_number): - indents = _get_line_indents(self.lines[line_number]) - current_line = line_number + 1 - while current_line < len(self.lines): - line = self.lines[current_line] - if not line.strip().startswith('#') and not line.strip() == '': - # HACK: We should have used logical lines here - if _get_line_indents(self.lines[current_line]) <= indents: - return current_line - current_line += 1 - return len(self.lines) - 1 - - def _set(self, lineno, line): - self.diffs[self.origs[lineno]] += len(line) - len(self.lines[lineno]) - self.lines[lineno] = line - - def _insert(self, lineno, line): - self.diffs[self.origs[lineno]] += len(line) + 1 - self.origs.insert(lineno, self.origs[lineno]) - self.lines.insert(lineno, line) - - -def _logical_start(lines, lineno, check_prev=False): - logical_finder = LogicalLineFinder(ArrayLinesAdapter(lines)) - if check_prev: - prev = lineno - 1 - while prev > 0: - start, end = logical_finder.logical_line_in(prev) - if end is None or start <= lineno < end: - return start - if start <= prev: - break - prev -= 1 - return logical_finder.logical_line_in(lineno)[0] - - -def _get_line_indents(line): - return rope.base.codeanalyze.count_line_indents(line) diff --git a/pythonFiles/rope/contrib/generate.py b/pythonFiles/rope/contrib/generate.py deleted file mode 100644 index 825f26d6ddad..000000000000 --- a/pythonFiles/rope/contrib/generate.py +++ /dev/null @@ -1,362 +0,0 @@ -import rope.base.evaluate -from rope.base import libutils -from rope.base import (change, pyobjects, exceptions, pynames, worder, - codeanalyze) -from rope.refactor import sourceutils, importutils, functionutils, suites - - -def create_generate(kind, project, resource, offset): - """A factory for creating `Generate` objects - - `kind` can be 'variable', 'function', 'class', 'module' or - 'package'. - - """ - generate = eval('Generate' + kind.title()) - return generate(project, resource, offset) - - -def create_module(project, name, sourcefolder=None): - """Creates a module and returns a `rope.base.resources.File`""" - if sourcefolder is None: - sourcefolder = project.root - packages = name.split('.') - parent = sourcefolder - for package in packages[:-1]: - parent = parent.get_child(package) - return parent.create_file(packages[-1] + '.py') - - -def create_package(project, name, sourcefolder=None): - """Creates a package and returns a `rope.base.resources.Folder`""" - if sourcefolder is None: - sourcefolder = project.root - packages = name.split('.') - parent = sourcefolder - for package in packages[:-1]: - parent = parent.get_child(package) - made_packages = parent.create_folder(packages[-1]) - made_packages.create_file('__init__.py') - return made_packages - - -class _Generate(object): - - def __init__(self, project, resource, offset): - self.project = project - self.resource = resource - self.info = self._generate_info(project, resource, offset) - self.name = self.info.get_name() - self._check_exceptional_conditions() - - def _generate_info(self, project, resource, offset): - return _GenerationInfo(project.pycore, resource, offset) - - def _check_exceptional_conditions(self): - if self.info.element_already_exists(): - raise exceptions.RefactoringError( - 'Element <%s> already exists.' % self.name) - if not self.info.primary_is_found(): - raise exceptions.RefactoringError( - 'Cannot determine the scope <%s> should be defined in.' % - self.name) - - def get_changes(self): - changes = change.ChangeSet('Generate %s <%s>' % - (self._get_element_kind(), self.name)) - indents = self.info.get_scope_indents() - blanks = self.info.get_blank_lines() - base_definition = sourceutils.fix_indentation(self._get_element(), - indents) - definition = '\n' * blanks[0] + base_definition + '\n' * blanks[1] - - resource = self.info.get_insertion_resource() - start, end = self.info.get_insertion_offsets() - - collector = codeanalyze.ChangeCollector(resource.read()) - collector.add_change(start, end, definition) - changes.add_change(change.ChangeContents( - resource, collector.get_changed())) - return changes - - def get_location(self): - return (self.info.get_insertion_resource(), - self.info.get_insertion_lineno()) - - def _get_element_kind(self): - raise NotImplementedError() - - def _get_element(self): - raise NotImplementedError() - - -class GenerateFunction(_Generate): - - def _generate_info(self, project, resource, offset): - return _FunctionGenerationInfo(project.pycore, resource, offset) - - def _get_element(self): - decorator = '' - args = [] - if self.info.is_static_method(): - decorator = '@staticmethod\n' - if self.info.is_method() or self.info.is_constructor() or \ - self.info.is_instance(): - args.append('self') - args.extend(self.info.get_passed_args()) - definition = '%sdef %s(%s):\n pass\n' % (decorator, self.name, - ', '.join(args)) - return definition - - def _get_element_kind(self): - return 'Function' - - -class GenerateVariable(_Generate): - - def _get_element(self): - return '%s = None\n' % self.name - - def _get_element_kind(self): - return 'Variable' - - -class GenerateClass(_Generate): - - def _get_element(self): - return 'class %s(object):\n pass\n' % self.name - - def _get_element_kind(self): - return 'Class' - - -class GenerateModule(_Generate): - - def get_changes(self): - package = self.info.get_package() - changes = change.ChangeSet('Generate Module <%s>' % self.name) - new_resource = self.project.get_file('%s/%s.py' % - (package.path, self.name)) - if new_resource.exists(): - raise exceptions.RefactoringError( - 'Module <%s> already exists' % new_resource.path) - changes.add_change(change.CreateResource(new_resource)) - changes.add_change(_add_import_to_module( - self.project, self.resource, new_resource)) - return changes - - def get_location(self): - package = self.info.get_package() - return (package.get_child('%s.py' % self.name), 1) - - -class GeneratePackage(_Generate): - - def get_changes(self): - package = self.info.get_package() - changes = change.ChangeSet('Generate Package <%s>' % self.name) - new_resource = self.project.get_folder('%s/%s' % - (package.path, self.name)) - if new_resource.exists(): - raise exceptions.RefactoringError( - 'Package <%s> already exists' % new_resource.path) - changes.add_change(change.CreateResource(new_resource)) - changes.add_change(_add_import_to_module( - self.project, self.resource, new_resource)) - child = self.project.get_folder(package.path + '/' + self.name) - changes.add_change(change.CreateFile(child, '__init__.py')) - return changes - - def get_location(self): - package = self.info.get_package() - child = package.get_child(self.name) - return (child.get_child('__init__.py'), 1) - - -def _add_import_to_module(project, resource, imported): - pymodule = project.get_pymodule(resource) - import_tools = importutils.ImportTools(project) - module_imports = import_tools.module_imports(pymodule) - module_name = libutils.modname(imported) - new_import = importutils.NormalImport(((module_name, None), )) - module_imports.add_import(new_import) - return change.ChangeContents(resource, module_imports.get_changed_source()) - - -class _GenerationInfo(object): - - def __init__(self, pycore, resource, offset): - self.pycore = pycore - self.resource = resource - self.offset = offset - self.source_pymodule = self.pycore.project.get_pymodule(resource) - finder = rope.base.evaluate.ScopeNameFinder(self.source_pymodule) - self.primary, self.pyname = finder.get_primary_and_pyname_at(offset) - self._init_fields() - - def _init_fields(self): - self.source_scope = self._get_source_scope() - self.goal_scope = self._get_goal_scope() - self.goal_pymodule = self._get_goal_module(self.goal_scope) - - def _get_goal_scope(self): - if self.primary is None: - return self._get_source_scope() - pyobject = self.primary.get_object() - if isinstance(pyobject, pyobjects.PyDefinedObject): - return pyobject.get_scope() - elif isinstance(pyobject.get_type(), pyobjects.PyClass): - return pyobject.get_type().get_scope() - - def _get_goal_module(self, scope): - if scope is None: - return - while scope.parent is not None: - scope = scope.parent - return scope.pyobject - - def _get_source_scope(self): - module_scope = self.source_pymodule.get_scope() - lineno = self.source_pymodule.lines.get_line_number(self.offset) - return module_scope.get_inner_scope_for_line(lineno) - - def get_insertion_lineno(self): - lines = self.goal_pymodule.lines - if self.goal_scope == self.source_scope: - line_finder = self.goal_pymodule.logical_lines - lineno = lines.get_line_number(self.offset) - lineno = line_finder.logical_line_in(lineno)[0] - root = suites.ast_suite_tree(self.goal_scope.pyobject.get_ast()) - suite = root.find_suite(lineno) - indents = sourceutils.get_indents(lines, lineno) - while self.get_scope_indents() < indents: - lineno = suite.get_start() - indents = sourceutils.get_indents(lines, lineno) - suite = suite.parent - return lineno - else: - return min(self.goal_scope.get_end() + 1, lines.length()) - - def get_insertion_resource(self): - return self.goal_pymodule.get_resource() - - def get_insertion_offsets(self): - if self.goal_scope.get_kind() == 'Class': - start, end = sourceutils.get_body_region(self.goal_scope.pyobject) - if self.goal_pymodule.source_code[start:end].strip() == 'pass': - return start, end - lines = self.goal_pymodule.lines - start = lines.get_line_start(self.get_insertion_lineno()) - return (start, start) - - def get_scope_indents(self): - if self.goal_scope.get_kind() == 'Module': - return 0 - return sourceutils.get_indents(self.goal_pymodule.lines, - self.goal_scope.get_start()) + 4 - - def get_blank_lines(self): - if self.goal_scope.get_kind() == 'Module': - base_blanks = 2 - if self.goal_pymodule.source_code.strip() == '': - base_blanks = 0 - if self.goal_scope.get_kind() == 'Class': - base_blanks = 1 - if self.goal_scope.get_kind() == 'Function': - base_blanks = 0 - if self.goal_scope == self.source_scope: - return (0, base_blanks) - return (base_blanks, 0) - - def get_package(self): - primary = self.primary - if self.primary is None: - return self.pycore.project.get_source_folders()[0] - if isinstance(primary.get_object(), pyobjects.PyPackage): - return primary.get_object().get_resource() - raise exceptions.RefactoringError( - 'A module/package can be only created in a package.') - - def primary_is_found(self): - return self.goal_scope is not None - - def element_already_exists(self): - if self.pyname is None or isinstance(self.pyname, pynames.UnboundName): - return False - return self.get_name() in self.goal_scope.get_defined_names() - - def get_name(self): - return worder.get_name_at(self.resource, self.offset) - - -class _FunctionGenerationInfo(_GenerationInfo): - - def _get_goal_scope(self): - if self.is_constructor(): - return self.pyname.get_object().get_scope() - if self.is_instance(): - return self.pyname.get_object().get_type().get_scope() - if self.primary is None: - return self._get_source_scope() - pyobject = self.primary.get_object() - if isinstance(pyobject, pyobjects.PyDefinedObject): - return pyobject.get_scope() - elif isinstance(pyobject.get_type(), pyobjects.PyClass): - return pyobject.get_type().get_scope() - - def element_already_exists(self): - if self.pyname is None or isinstance(self.pyname, pynames.UnboundName): - return False - return self.get_name() in self.goal_scope.get_defined_names() - - def is_static_method(self): - return self.primary is not None and \ - isinstance(self.primary.get_object(), pyobjects.PyClass) - - def is_method(self): - return self.primary is not None and \ - isinstance(self.primary.get_object().get_type(), pyobjects.PyClass) - - def is_constructor(self): - return self.pyname is not None and \ - isinstance(self.pyname.get_object(), pyobjects.PyClass) - - def is_instance(self): - if self.pyname is None: - return False - pyobject = self.pyname.get_object() - return isinstance(pyobject.get_type(), pyobjects.PyClass) - - def get_name(self): - if self.is_constructor(): - return '__init__' - if self.is_instance(): - return '__call__' - return worder.get_name_at(self.resource, self.offset) - - def get_passed_args(self): - result = [] - source = self.source_pymodule.source_code - finder = worder.Worder(source) - if finder.is_a_function_being_called(self.offset): - start, end = finder.get_primary_range(self.offset) - parens_start, parens_end = finder.get_word_parens_range(end - 1) - call = source[start:parens_end] - parser = functionutils._FunctionParser(call, False) - args, keywords = parser.get_parameters() - for arg in args: - if self._is_id(arg): - result.append(arg) - else: - result.append('arg%d' % len(result)) - for name, value in keywords: - result.append(name) - return result - - def _is_id(self, arg): - def id_or_underline(c): - return c.isalpha() or c == '_' - for c in arg: - if not id_or_underline(c) and not c.isdigit(): - return False - return id_or_underline(arg[0]) diff --git a/pythonFiles/rope/refactor/__init__.py b/pythonFiles/rope/refactor/__init__.py deleted file mode 100644 index 4ef675134814..000000000000 --- a/pythonFiles/rope/refactor/__init__.py +++ /dev/null @@ -1,55 +0,0 @@ -"""rope refactor package - -This package contains modules that perform python refactorings. -Refactoring classes perform refactorings in 4 steps: - -1. Collect some data for performing the refactoring and use them - to construct a refactoring class. Like:: - - renamer = Rename(project, resource, offset) - -2. Some refactorings give you useful information about the - refactoring after their construction. Like:: - - print(renamer.get_old_name()) - -3. Give the refactoring class more information about how to - perform the refactoring and get the changes this refactoring is - going to make. This is done by calling `get_changes` method of the - refactoring class. Like:: - - changes = renamer.get_changes(new_name) - -4. You can commit the changes. Like:: - - project.do(changes) - -These steps are like the steps IDEs usually do for performing a -refactoring. These are the things an IDE does in each step: - -1. Construct a refactoring object by giving it information like - resource, offset and ... . Some of the refactoring problems (like - performing rename refactoring on language keywords) can be reported - here. -2. Print some information about the refactoring and ask the user - about the information that are necessary for completing the - refactoring (like new name). -3. Call the `get_changes` by passing it information asked from - the user (if necessary) and get and preview the changes returned by - it. -4. perform the refactoring. - -From ``0.5m5`` release the `get_changes()` method of some time- -consuming refactorings take an optional `rope.base.taskhandle. -TaskHandle` parameter. You can use this object for stopping or -monitoring the progress of refactorings. - -""" -from rope.refactor.importutils import ImportOrganizer # noqa -from rope.refactor.topackage import ModuleToPackage # noqa - - -__all__ = ['rename', 'move', 'inline', 'extract', 'restructure', 'topackage', - 'importutils', 'usefunction', 'change_signature', - 'encapsulate_field', 'introduce_factory', 'introduce_parameter', - 'localtofield', 'method_object', 'multiproject'] diff --git a/pythonFiles/rope/refactor/change_signature.py b/pythonFiles/rope/refactor/change_signature.py deleted file mode 100644 index b5ba1856a807..000000000000 --- a/pythonFiles/rope/refactor/change_signature.py +++ /dev/null @@ -1,352 +0,0 @@ -import copy - -import rope.base.exceptions -from rope.base import codeanalyze -from rope.base import evaluate -from rope.base import pyobjects -from rope.base import taskhandle -from rope.base import utils -from rope.base import worder -from rope.base.change import ChangeContents, ChangeSet -from rope.refactor import occurrences, functionutils - - -class ChangeSignature(object): - - def __init__(self, project, resource, offset): - self.project = project - self.resource = resource - self.offset = offset - self._set_name_and_pyname() - if self.pyname is None or self.pyname.get_object() is None or \ - not isinstance(self.pyname.get_object(), pyobjects.PyFunction): - raise rope.base.exceptions.RefactoringError( - 'Change method signature should be performed on functions') - - def _set_name_and_pyname(self): - self.name = worder.get_name_at(self.resource, self.offset) - this_pymodule = self.project.get_pymodule(self.resource) - self.primary, self.pyname = evaluate.eval_location2( - this_pymodule, self.offset) - if self.pyname is None: - return - pyobject = self.pyname.get_object() - if isinstance(pyobject, pyobjects.PyClass) and \ - '__init__' in pyobject: - self.pyname = pyobject['__init__'] - self.name = '__init__' - pyobject = self.pyname.get_object() - self.others = None - if self.name == '__init__' and \ - isinstance(pyobject, pyobjects.PyFunction) and \ - isinstance(pyobject.parent, pyobjects.PyClass): - pyclass = pyobject.parent - self.others = (pyclass.get_name(), - pyclass.parent[pyclass.get_name()]) - - def _change_calls(self, call_changer, in_hierarchy=None, resources=None, - handle=taskhandle.NullTaskHandle()): - if resources is None: - resources = self.project.get_python_files() - changes = ChangeSet('Changing signature of <%s>' % self.name) - job_set = handle.create_jobset('Collecting Changes', len(resources)) - finder = occurrences.create_finder( - self.project, self.name, self.pyname, instance=self.primary, - in_hierarchy=in_hierarchy and self.is_method()) - if self.others: - name, pyname = self.others - constructor_finder = occurrences.create_finder( - self.project, name, pyname, only_calls=True) - finder = _MultipleFinders([finder, constructor_finder]) - for file in resources: - job_set.started_job(file.path) - change_calls = _ChangeCallsInModule( - self.project, finder, file, call_changer) - changed_file = change_calls.get_changed_module() - if changed_file is not None: - changes.add_change(ChangeContents(file, changed_file)) - job_set.finished_job() - return changes - - def get_args(self): - """Get function arguments. - - Return a list of ``(name, default)`` tuples for all but star - and double star arguments. For arguments that don't have a - default, `None` will be used. - """ - return self._definfo().args_with_defaults - - def is_method(self): - pyfunction = self.pyname.get_object() - return isinstance(pyfunction.parent, pyobjects.PyClass) - - @utils.deprecated('Use `ChangeSignature.get_args()` instead') - def get_definition_info(self): - return self._definfo() - - def _definfo(self): - return functionutils.DefinitionInfo.read(self.pyname.get_object()) - - @utils.deprecated() - def normalize(self): - changer = _FunctionChangers( - self.pyname.get_object(), self.get_definition_info(), - [ArgumentNormalizer()]) - return self._change_calls(changer) - - @utils.deprecated() - def remove(self, index): - changer = _FunctionChangers( - self.pyname.get_object(), self.get_definition_info(), - [ArgumentRemover(index)]) - return self._change_calls(changer) - - @utils.deprecated() - def add(self, index, name, default=None, value=None): - changer = _FunctionChangers( - self.pyname.get_object(), self.get_definition_info(), - [ArgumentAdder(index, name, default, value)]) - return self._change_calls(changer) - - @utils.deprecated() - def inline_default(self, index): - changer = _FunctionChangers( - self.pyname.get_object(), self.get_definition_info(), - [ArgumentDefaultInliner(index)]) - return self._change_calls(changer) - - @utils.deprecated() - def reorder(self, new_ordering): - changer = _FunctionChangers( - self.pyname.get_object(), self.get_definition_info(), - [ArgumentReorderer(new_ordering)]) - return self._change_calls(changer) - - def get_changes(self, changers, in_hierarchy=False, resources=None, - task_handle=taskhandle.NullTaskHandle()): - """Get changes caused by this refactoring - - `changers` is a list of `_ArgumentChanger`\s. If `in_hierarchy` - is `True` the changers are applyed to all matching methods in - the class hierarchy. - `resources` can be a list of `rope.base.resource.File`\s that - should be searched for occurrences; if `None` all python files - in the project are searched. - - """ - function_changer = _FunctionChangers(self.pyname.get_object(), - self._definfo(), changers) - return self._change_calls(function_changer, in_hierarchy, - resources, task_handle) - - -class _FunctionChangers(object): - - def __init__(self, pyfunction, definition_info, changers=None): - self.pyfunction = pyfunction - self.definition_info = definition_info - self.changers = changers - self.changed_definition_infos = self._get_changed_definition_infos() - - def _get_changed_definition_infos(self): - result = [] - definition_info = self.definition_info - result.append(definition_info) - for changer in self.changers: - definition_info = copy.deepcopy(definition_info) - changer.change_definition_info(definition_info) - result.append(definition_info) - return result - - def change_definition(self, call): - return self.changed_definition_infos[-1].to_string() - - def change_call(self, primary, pyname, call): - call_info = functionutils.CallInfo.read( - primary, pyname, self.definition_info, call) - mapping = functionutils.ArgumentMapping(self.definition_info, - call_info) - - for definition_info, changer in zip(self.changed_definition_infos, - self.changers): - changer.change_argument_mapping(definition_info, mapping) - - return mapping.to_call_info( - self.changed_definition_infos[-1]).to_string() - - -class _ArgumentChanger(object): - - def change_definition_info(self, definition_info): - pass - - def change_argument_mapping(self, definition_info, argument_mapping): - pass - - -class ArgumentNormalizer(_ArgumentChanger): - pass - - -class ArgumentRemover(_ArgumentChanger): - - def __init__(self, index): - self.index = index - - def change_definition_info(self, call_info): - if self.index < len(call_info.args_with_defaults): - del call_info.args_with_defaults[self.index] - elif self.index == len(call_info.args_with_defaults) and \ - call_info.args_arg is not None: - call_info.args_arg = None - elif (self.index == len(call_info.args_with_defaults) and - call_info.args_arg is None and - call_info.keywords_arg is not None) or \ - (self.index == len(call_info.args_with_defaults) + 1 and - call_info.args_arg is not None and - call_info.keywords_arg is not None): - call_info.keywords_arg = None - - def change_argument_mapping(self, definition_info, mapping): - if self.index < len(definition_info.args_with_defaults): - name = definition_info.args_with_defaults[0] - if name in mapping.param_dict: - del mapping.param_dict[name] - - -class ArgumentAdder(_ArgumentChanger): - - def __init__(self, index, name, default=None, value=None): - self.index = index - self.name = name - self.default = default - self.value = value - - def change_definition_info(self, definition_info): - for pair in definition_info.args_with_defaults: - if pair[0] == self.name: - raise rope.base.exceptions.RefactoringError( - 'Adding duplicate parameter: <%s>.' % self.name) - definition_info.args_with_defaults.insert(self.index, - (self.name, self.default)) - - def change_argument_mapping(self, definition_info, mapping): - if self.value is not None: - mapping.param_dict[self.name] = self.value - - -class ArgumentDefaultInliner(_ArgumentChanger): - - def __init__(self, index): - self.index = index - self.remove = False - - def change_definition_info(self, definition_info): - if self.remove: - definition_info.args_with_defaults[self.index] = \ - (definition_info.args_with_defaults[self.index][0], None) - - def change_argument_mapping(self, definition_info, mapping): - default = definition_info.args_with_defaults[self.index][1] - name = definition_info.args_with_defaults[self.index][0] - if default is not None and name not in mapping.param_dict: - mapping.param_dict[name] = default - - -class ArgumentReorderer(_ArgumentChanger): - - def __init__(self, new_order, autodef=None): - """Construct an `ArgumentReorderer` - - Note that the `new_order` is a list containing the new - position of parameters; not the position each parameter - is going to be moved to. (changed in ``0.5m4``) - - For example changing ``f(a, b, c)`` to ``f(c, a, b)`` - requires passing ``[2, 0, 1]`` and *not* ``[1, 2, 0]``. - - The `autodef` (automatic default) argument, forces rope to use - it as a default if a default is needed after the change. That - happens when an argument without default is moved after - another that has a default value. Note that `autodef` should - be a string or `None`; the latter disables adding automatic - default. - - """ - self.new_order = new_order - self.autodef = autodef - - def change_definition_info(self, definition_info): - new_args = list(definition_info.args_with_defaults) - for new_index, index in enumerate(self.new_order): - new_args[new_index] = definition_info.args_with_defaults[index] - seen_default = False - for index, (arg, default) in enumerate(list(new_args)): - if default is not None: - seen_default = True - if seen_default and default is None and self.autodef is not None: - new_args[index] = (arg, self.autodef) - definition_info.args_with_defaults = new_args - - -class _ChangeCallsInModule(object): - - def __init__(self, project, occurrence_finder, resource, call_changer): - self.project = project - self.occurrence_finder = occurrence_finder - self.resource = resource - self.call_changer = call_changer - - def get_changed_module(self): - word_finder = worder.Worder(self.source) - change_collector = codeanalyze.ChangeCollector(self.source) - for occurrence in self.occurrence_finder.find_occurrences( - self.resource): - if not occurrence.is_called() and not occurrence.is_defined(): - continue - start, end = occurrence.get_primary_range() - begin_parens, end_parens = word_finder.\ - get_word_parens_range(end - 1) - if occurrence.is_called(): - primary, pyname = occurrence.get_primary_and_pyname() - changed_call = self.call_changer.change_call( - primary, pyname, self.source[start:end_parens]) - else: - changed_call = self.call_changer.change_definition( - self.source[start:end_parens]) - if changed_call is not None: - change_collector.add_change(start, end_parens, changed_call) - return change_collector.get_changed() - - @property - @utils.saveit - def pymodule(self): - return self.project.get_pymodule(self.resource) - - @property - @utils.saveit - def source(self): - if self.resource is not None: - return self.resource.read() - else: - return self.pymodule.source_code - - @property - @utils.saveit - def lines(self): - return self.pymodule.lines - - -class _MultipleFinders(object): - - def __init__(self, finders): - self.finders = finders - - def find_occurrences(self, resource=None, pymodule=None): - all_occurrences = [] - for finder in self.finders: - all_occurrences.extend(finder.find_occurrences(resource, pymodule)) - all_occurrences.sort(key=lambda x: x.get_primary_range()) - return all_occurrences - diff --git a/pythonFiles/rope/refactor/encapsulate_field.py b/pythonFiles/rope/refactor/encapsulate_field.py deleted file mode 100644 index 32cb7a957b5f..000000000000 --- a/pythonFiles/rope/refactor/encapsulate_field.py +++ /dev/null @@ -1,209 +0,0 @@ -from rope.base import evaluate -from rope.base import exceptions -from rope.base import libutils -from rope.base import pynames -from rope.base import taskhandle -from rope.base import utils -from rope.base import worder -from rope.base.change import ChangeSet, ChangeContents -from rope.refactor import sourceutils, occurrences - - -class EncapsulateField(object): - - def __init__(self, project, resource, offset): - self.project = project - self.name = worder.get_name_at(resource, offset) - this_pymodule = self.project.get_pymodule(resource) - self.pyname = evaluate.eval_location(this_pymodule, offset) - if not self._is_an_attribute(self.pyname): - raise exceptions.RefactoringError( - 'Encapsulate field should be performed on class attributes.') - self.resource = self.pyname.get_definition_location()[0].get_resource() - - def get_changes(self, getter=None, setter=None, resources=None, - task_handle=taskhandle.NullTaskHandle()): - """Get the changes this refactoring makes - - If `getter` is not `None`, that will be the name of the - getter, otherwise ``get_${field_name}`` will be used. The - same is true for `setter` and if it is None set_${field_name} is - used. - - `resources` can be a list of `rope.base.resource.File`\s that - the refactoring should be applied on; if `None` all python - files in the project are searched. - - """ - if resources is None: - resources = self.project.get_python_files() - changes = ChangeSet('Encapsulate field <%s>' % self.name) - job_set = task_handle.create_jobset('Collecting Changes', - len(resources)) - if getter is None: - getter = 'get_' + self.name - if setter is None: - setter = 'set_' + self.name - renamer = GetterSetterRenameInModule( - self.project, self.name, self.pyname, getter, setter) - for file in resources: - job_set.started_job(file.path) - if file == self.resource: - result = self._change_holding_module(changes, renamer, - getter, setter) - changes.add_change(ChangeContents(self.resource, result)) - else: - result = renamer.get_changed_module(file) - if result is not None: - changes.add_change(ChangeContents(file, result)) - job_set.finished_job() - return changes - - def get_field_name(self): - """Get the name of the field to be encapsulated""" - return self.name - - def _is_an_attribute(self, pyname): - if pyname is not None and isinstance(pyname, pynames.AssignedName): - pymodule, lineno = self.pyname.get_definition_location() - scope = pymodule.get_scope().\ - get_inner_scope_for_line(lineno) - if scope.get_kind() == 'Class': - return pyname in scope.get_names().values() - parent = scope.parent - if parent is not None and parent.get_kind() == 'Class': - return pyname in parent.get_names().values() - return False - - def _get_defining_class_scope(self): - defining_scope = self._get_defining_scope() - if defining_scope.get_kind() == 'Function': - defining_scope = defining_scope.parent - return defining_scope - - def _get_defining_scope(self): - pymodule, line = self.pyname.get_definition_location() - return pymodule.get_scope().get_inner_scope_for_line(line) - - def _change_holding_module(self, changes, renamer, getter, setter): - pymodule = self.project.get_pymodule(self.resource) - class_scope = self._get_defining_class_scope() - defining_object = self._get_defining_scope().pyobject - start, end = sourceutils.get_body_region(defining_object) - - new_source = renamer.get_changed_module(pymodule=pymodule, - skip_start=start, skip_end=end) - if new_source is not None: - pymodule = libutils.get_string_module( - self.project, new_source, self.resource) - class_scope = pymodule.get_scope().\ - get_inner_scope_for_line(class_scope.get_start()) - indents = sourceutils.get_indent(self.project) * ' ' - getter = 'def %s(self):\n%sreturn self.%s' % \ - (getter, indents, self.name) - setter = 'def %s(self, value):\n%sself.%s = value' % \ - (setter, indents, self.name) - new_source = sourceutils.add_methods(pymodule, class_scope, - [getter, setter]) - return new_source - - -class GetterSetterRenameInModule(object): - - def __init__(self, project, name, pyname, getter, setter): - self.project = project - self.name = name - self.finder = occurrences.create_finder(project, name, pyname) - self.getter = getter - self.setter = setter - - def get_changed_module(self, resource=None, pymodule=None, - skip_start=0, skip_end=0): - change_finder = _FindChangesForModule(self, resource, pymodule, - skip_start, skip_end) - return change_finder.get_changed_module() - - -class _FindChangesForModule(object): - - def __init__(self, finder, resource, pymodule, skip_start, skip_end): - self.project = finder.project - self.finder = finder.finder - self.getter = finder.getter - self.setter = finder.setter - self.resource = resource - self.pymodule = pymodule - self.last_modified = 0 - self.last_set = None - self.set_index = None - self.skip_start = skip_start - self.skip_end = skip_end - - def get_changed_module(self): - result = [] - for occurrence in self.finder.find_occurrences(self.resource, - self.pymodule): - start, end = occurrence.get_word_range() - if self.skip_start <= start < self.skip_end: - continue - self._manage_writes(start, result) - result.append(self.source[self.last_modified:start]) - if self._is_assigned_in_a_tuple_assignment(occurrence): - raise exceptions.RefactoringError( - 'Cannot handle tuple assignments in encapsulate field.') - if occurrence.is_written(): - assignment_type = self.worder.get_assignment_type(start) - if assignment_type == '=': - result.append(self.setter + '(') - else: - var_name = self.source[occurrence.get_primary_range()[0]: - start] + self.getter + '()' - result.append(self.setter + '(' + var_name - + ' %s ' % assignment_type[:-1]) - current_line = self.lines.get_line_number(start) - start_line, end_line = self.pymodule.logical_lines.\ - logical_line_in(current_line) - self.last_set = self.lines.get_line_end(end_line) - end = self.source.index('=', end) + 1 - self.set_index = len(result) - else: - result.append(self.getter + '()') - self.last_modified = end - if self.last_modified != 0: - self._manage_writes(len(self.source), result) - result.append(self.source[self.last_modified:]) - return ''.join(result) - return None - - def _manage_writes(self, offset, result): - if self.last_set is not None and self.last_set <= offset: - result.append(self.source[self.last_modified:self.last_set]) - set_value = ''.join(result[self.set_index:]).strip() - del result[self.set_index:] - result.append(set_value + ')') - self.last_modified = self.last_set - self.last_set = None - - def _is_assigned_in_a_tuple_assignment(self, occurance): - offset = occurance.get_word_range()[0] - return self.worder.is_assigned_in_a_tuple_assignment(offset) - - @property - @utils.saveit - def source(self): - if self.resource is not None: - return self.resource.read() - else: - return self.pymodule.source_code - - @property - @utils.saveit - def lines(self): - if self.pymodule is None: - self.pymodule = self.project.get_pymodule(self.resource) - return self.pymodule.lines - - @property - @utils.saveit - def worder(self): - return worder.Worder(self.source) diff --git a/pythonFiles/rope/refactor/extract.py b/pythonFiles/rope/refactor/extract.py deleted file mode 100644 index 80e74317bdca..000000000000 --- a/pythonFiles/rope/refactor/extract.py +++ /dev/null @@ -1,810 +0,0 @@ -import re - -from rope.base.utils.datastructures import OrderedSet -from rope.base import ast, codeanalyze -from rope.base.change import ChangeSet, ChangeContents -from rope.base.exceptions import RefactoringError -from rope.base.utils import pycompat -from rope.refactor import (sourceutils, similarfinder, - patchedast, suites, usefunction) - - -# Extract refactoring has lots of special cases. I tried to split it -# to smaller parts to make it more manageable: -# -# _ExtractInfo: holds information about the refactoring; it is passed -# to the parts that need to have information about the refactoring -# -# _ExtractCollector: merely saves all of the information necessary for -# performing the refactoring. -# -# _DefinitionLocationFinder: finds where to insert the definition. -# -# _ExceptionalConditionChecker: checks for exceptional conditions in -# which the refactoring cannot be applied. -# -# _ExtractMethodParts: generates the pieces of code (like definition) -# needed for performing extract method. -# -# _ExtractVariableParts: like _ExtractMethodParts for variables. -# -# _ExtractPerformer: Uses above classes to collect refactoring -# changes. -# -# There are a few more helper functions and classes used by above -# classes. -class _ExtractRefactoring(object): - - def __init__(self, project, resource, start_offset, end_offset, - variable=False): - self.project = project - self.resource = resource - self.start_offset = self._fix_start(resource.read(), start_offset) - self.end_offset = self._fix_end(resource.read(), end_offset) - - def _fix_start(self, source, offset): - while offset < len(source) and source[offset].isspace(): - offset += 1 - return offset - - def _fix_end(self, source, offset): - while offset > 0 and source[offset - 1].isspace(): - offset -= 1 - return offset - - def get_changes(self, extracted_name, similar=False, global_=False): - """Get the changes this refactoring makes - - :parameters: - - `similar`: if `True`, similar expressions/statements are also - replaced. - - `global_`: if `True`, the extracted method/variable will - be global. - - """ - info = _ExtractInfo( - self.project, self.resource, self.start_offset, self.end_offset, - extracted_name, variable=self.kind == 'variable', - similar=similar, make_global=global_) - new_contents = _ExtractPerformer(info).extract() - changes = ChangeSet('Extract %s <%s>' % (self.kind, - extracted_name)) - changes.add_change(ChangeContents(self.resource, new_contents)) - return changes - - -class ExtractMethod(_ExtractRefactoring): - - def __init__(self, *args, **kwds): - super(ExtractMethod, self).__init__(*args, **kwds) - - kind = 'method' - - -class ExtractVariable(_ExtractRefactoring): - - def __init__(self, *args, **kwds): - kwds = dict(kwds) - kwds['variable'] = True - super(ExtractVariable, self).__init__(*args, **kwds) - - kind = 'variable' - - -class _ExtractInfo(object): - """Holds information about the extract to be performed""" - - def __init__(self, project, resource, start, end, new_name, - variable, similar, make_global): - self.project = project - self.resource = resource - self.pymodule = project.get_pymodule(resource) - self.global_scope = self.pymodule.get_scope() - self.source = self.pymodule.source_code - self.lines = self.pymodule.lines - self.new_name = new_name - self.variable = variable - self.similar = similar - self._init_parts(start, end) - self._init_scope() - self.make_global = make_global - - def _init_parts(self, start, end): - self.region = (self._choose_closest_line_end(start), - self._choose_closest_line_end(end, end=True)) - - start = self.logical_lines.logical_line_in( - self.lines.get_line_number(self.region[0]))[0] - end = self.logical_lines.logical_line_in( - self.lines.get_line_number(self.region[1]))[1] - self.region_lines = (start, end) - - self.lines_region = (self.lines.get_line_start(self.region_lines[0]), - self.lines.get_line_end(self.region_lines[1])) - - @property - def logical_lines(self): - return self.pymodule.logical_lines - - def _init_scope(self): - start_line = self.region_lines[0] - scope = self.global_scope.get_inner_scope_for_line(start_line) - if scope.get_kind() != 'Module' and scope.get_start() == start_line: - scope = scope.parent - self.scope = scope - self.scope_region = self._get_scope_region(self.scope) - - def _get_scope_region(self, scope): - return (self.lines.get_line_start(scope.get_start()), - self.lines.get_line_end(scope.get_end()) + 1) - - def _choose_closest_line_end(self, offset, end=False): - lineno = self.lines.get_line_number(offset) - line_start = self.lines.get_line_start(lineno) - line_end = self.lines.get_line_end(lineno) - if self.source[line_start:offset].strip() == '': - if end: - return line_start - 1 - else: - return line_start - elif self.source[offset:line_end].strip() == '': - return min(line_end, len(self.source)) - return offset - - @property - def one_line(self): - return self.region != self.lines_region and \ - (self.logical_lines.logical_line_in(self.region_lines[0]) == - self.logical_lines.logical_line_in(self.region_lines[1])) - - @property - def global_(self): - return self.scope.parent is None - - @property - def method(self): - return self.scope.parent is not None and \ - self.scope.parent.get_kind() == 'Class' - - @property - def indents(self): - return sourceutils.get_indents(self.pymodule.lines, - self.region_lines[0]) - - @property - def scope_indents(self): - if self.global_: - return 0 - return sourceutils.get_indents(self.pymodule.lines, - self.scope.get_start()) - - @property - def extracted(self): - return self.source[self.region[0]:self.region[1]] - - _returned = None - - @property - def returned(self): - """Does the extracted piece contain return statement""" - if self._returned is None: - node = _parse_text(self.extracted) - self._returned = usefunction._returns_last(node) - return self._returned - - -class _ExtractCollector(object): - """Collects information needed for performing the extract""" - - def __init__(self, info): - self.definition = None - self.body_pattern = None - self.checks = {} - self.replacement_pattern = None - self.matches = None - self.replacements = None - self.definition_location = None - - -class _ExtractPerformer(object): - - def __init__(self, info): - self.info = info - _ExceptionalConditionChecker()(self.info) - - def extract(self): - extract_info = self._collect_info() - content = codeanalyze.ChangeCollector(self.info.source) - definition = extract_info.definition - lineno, indents = extract_info.definition_location - offset = self.info.lines.get_line_start(lineno) - indented = sourceutils.fix_indentation(definition, indents) - content.add_change(offset, offset, indented) - self._replace_occurrences(content, extract_info) - return content.get_changed() - - def _replace_occurrences(self, content, extract_info): - for match in extract_info.matches: - replacement = similarfinder.CodeTemplate( - extract_info.replacement_pattern) - mapping = {} - for name in replacement.get_names(): - node = match.get_ast(name) - if node: - start, end = patchedast.node_region(match.get_ast(name)) - mapping[name] = self.info.source[start:end] - else: - mapping[name] = name - region = match.get_region() - content.add_change(region[0], region[1], - replacement.substitute(mapping)) - - def _collect_info(self): - extract_collector = _ExtractCollector(self.info) - self._find_definition(extract_collector) - self._find_matches(extract_collector) - self._find_definition_location(extract_collector) - return extract_collector - - def _find_matches(self, collector): - regions = self._where_to_search() - finder = similarfinder.SimilarFinder(self.info.pymodule) - matches = [] - for start, end in regions: - matches.extend((finder.get_matches(collector.body_pattern, - collector.checks, start, end))) - collector.matches = matches - - def _where_to_search(self): - if self.info.similar: - if self.info.make_global or self.info.global_: - return [(0, len(self.info.pymodule.source_code))] - if self.info.method and not self.info.variable: - class_scope = self.info.scope.parent - regions = [] - method_kind = _get_function_kind(self.info.scope) - for scope in class_scope.get_scopes(): - if method_kind == 'method' and \ - _get_function_kind(scope) != 'method': - continue - start = self.info.lines.get_line_start(scope.get_start()) - end = self.info.lines.get_line_end(scope.get_end()) - regions.append((start, end)) - return regions - else: - if self.info.variable: - return [self.info.scope_region] - else: - return [self.info._get_scope_region( - self.info.scope.parent)] - else: - return [self.info.region] - - def _find_definition_location(self, collector): - matched_lines = [] - for match in collector.matches: - start = self.info.lines.get_line_number(match.get_region()[0]) - start_line = self.info.logical_lines.logical_line_in(start)[0] - matched_lines.append(start_line) - location_finder = _DefinitionLocationFinder(self.info, matched_lines) - collector.definition_location = (location_finder.find_lineno(), - location_finder.find_indents()) - - def _find_definition(self, collector): - if self.info.variable: - parts = _ExtractVariableParts(self.info) - else: - parts = _ExtractMethodParts(self.info) - collector.definition = parts.get_definition() - collector.body_pattern = parts.get_body_pattern() - collector.replacement_pattern = parts.get_replacement_pattern() - collector.checks = parts.get_checks() - - -class _DefinitionLocationFinder(object): - - def __init__(self, info, matched_lines): - self.info = info - self.matched_lines = matched_lines - # This only happens when subexpressions cannot be matched - if not matched_lines: - self.matched_lines.append(self.info.region_lines[0]) - - def find_lineno(self): - if self.info.variable and not self.info.make_global: - return self._get_before_line() - if self.info.make_global or self.info.global_: - toplevel = self._find_toplevel(self.info.scope) - ast = self.info.pymodule.get_ast() - newlines = sorted(self.matched_lines + [toplevel.get_end() + 1]) - return suites.find_visible(ast, newlines) - return self._get_after_scope() - - def _find_toplevel(self, scope): - toplevel = scope - if toplevel.parent is not None: - while toplevel.parent.parent is not None: - toplevel = toplevel.parent - return toplevel - - def find_indents(self): - if self.info.variable and not self.info.make_global: - return sourceutils.get_indents(self.info.lines, - self._get_before_line()) - else: - if self.info.global_ or self.info.make_global: - return 0 - return self.info.scope_indents - - def _get_before_line(self): - ast = self.info.scope.pyobject.get_ast() - return suites.find_visible(ast, self.matched_lines) - - def _get_after_scope(self): - return self.info.scope.get_end() + 1 - - -class _ExceptionalConditionChecker(object): - - def __call__(self, info): - self.base_conditions(info) - if info.one_line: - self.one_line_conditions(info) - else: - self.multi_line_conditions(info) - - def base_conditions(self, info): - if info.region[1] > info.scope_region[1]: - raise RefactoringError('Bad region selected for extract method') - end_line = info.region_lines[1] - end_scope = info.global_scope.get_inner_scope_for_line(end_line) - if end_scope != info.scope and end_scope.get_end() != end_line: - raise RefactoringError('Bad region selected for extract method') - try: - extracted = info.source[info.region[0]:info.region[1]] - if info.one_line: - extracted = '(%s)' % extracted - if _UnmatchedBreakOrContinueFinder.has_errors(extracted): - raise RefactoringError('A break/continue without having a ' - 'matching for/while loop.') - except SyntaxError: - raise RefactoringError('Extracted piece should ' - 'contain complete statements.') - - def one_line_conditions(self, info): - if self._is_region_on_a_word(info): - raise RefactoringError('Should extract complete statements.') - if info.variable and not info.one_line: - raise RefactoringError('Extract variable should not ' - 'span multiple lines.') - - def multi_line_conditions(self, info): - node = _parse_text(info.source[info.region[0]:info.region[1]]) - count = usefunction._return_count(node) - if count > 1: - raise RefactoringError('Extracted piece can have only one ' - 'return statement.') - if usefunction._yield_count(node): - raise RefactoringError('Extracted piece cannot ' - 'have yield statements.') - if count == 1 and not usefunction._returns_last(node): - raise RefactoringError('Return should be the last statement.') - if info.region != info.lines_region: - raise RefactoringError('Extracted piece should ' - 'contain complete statements.') - - def _is_region_on_a_word(self, info): - if info.region[0] > 0 and \ - self._is_on_a_word(info, info.region[0] - 1) or \ - self._is_on_a_word(info, info.region[1] - 1): - return True - - def _is_on_a_word(self, info, offset): - prev = info.source[offset] - if not (prev.isalnum() or prev == '_') or \ - offset + 1 == len(info.source): - return False - next = info.source[offset + 1] - return next.isalnum() or next == '_' - - -class _ExtractMethodParts(object): - - def __init__(self, info): - self.info = info - self.info_collector = self._create_info_collector() - - def get_definition(self): - if self.info.global_: - return '\n%s\n' % self._get_function_definition() - else: - return '\n%s' % self._get_function_definition() - - def get_replacement_pattern(self): - variables = [] - variables.extend(self._find_function_arguments()) - variables.extend(self._find_function_returns()) - return similarfinder.make_pattern(self._get_call(), variables) - - def get_body_pattern(self): - variables = [] - variables.extend(self._find_function_arguments()) - variables.extend(self._find_function_returns()) - variables.extend(self._find_temps()) - return similarfinder.make_pattern(self._get_body(), variables) - - def _get_body(self): - result = sourceutils.fix_indentation(self.info.extracted, 0) - if self.info.one_line: - result = '(%s)' % result - return result - - def _find_temps(self): - return usefunction.find_temps(self.info.project, - self._get_body()) - - def get_checks(self): - if self.info.method and not self.info.make_global: - if _get_function_kind(self.info.scope) == 'method': - class_name = similarfinder._pydefined_to_str( - self.info.scope.parent.pyobject) - return {self._get_self_name(): 'type=' + class_name} - return {} - - def _create_info_collector(self): - zero = self.info.scope.get_start() - 1 - start_line = self.info.region_lines[0] - zero - end_line = self.info.region_lines[1] - zero - info_collector = _FunctionInformationCollector(start_line, end_line, - self.info.global_) - body = self.info.source[self.info.scope_region[0]: - self.info.scope_region[1]] - node = _parse_text(body) - ast.walk(node, info_collector) - return info_collector - - def _get_function_definition(self): - args = self._find_function_arguments() - returns = self._find_function_returns() - result = [] - if self.info.method and not self.info.make_global and \ - _get_function_kind(self.info.scope) != 'method': - result.append('@staticmethod\n') - result.append('def %s:\n' % self._get_function_signature(args)) - unindented_body = self._get_unindented_function_body(returns) - indents = sourceutils.get_indent(self.info.project) - function_body = sourceutils.indent_lines(unindented_body, indents) - result.append(function_body) - definition = ''.join(result) - - return definition + '\n' - - def _get_function_signature(self, args): - args = list(args) - prefix = '' - if self._extracting_method(): - self_name = self._get_self_name() - if self_name is None: - raise RefactoringError('Extracting a method from a function ' - 'with no self argument.') - if self_name in args: - args.remove(self_name) - args.insert(0, self_name) - return prefix + self.info.new_name + \ - '(%s)' % self._get_comma_form(args) - - def _extracting_method(self): - return self.info.method and not self.info.make_global and \ - _get_function_kind(self.info.scope) == 'method' - - def _get_self_name(self): - param_names = self.info.scope.pyobject.get_param_names() - if param_names: - return param_names[0] - - def _get_function_call(self, args): - prefix = '' - if self.info.method and not self.info.make_global: - if _get_function_kind(self.info.scope) == 'method': - self_name = self._get_self_name() - if self_name in args: - args.remove(self_name) - prefix = self_name + '.' - else: - prefix = self.info.scope.parent.pyobject.get_name() + '.' - return prefix + '%s(%s)' % (self.info.new_name, - self._get_comma_form(args)) - - def _get_comma_form(self, names): - result = '' - if names: - result += names[0] - for name in names[1:]: - result += ', ' + name - return result - - def _get_call(self): - if self.info.one_line: - args = self._find_function_arguments() - return self._get_function_call(args) - args = self._find_function_arguments() - returns = self._find_function_returns() - call_prefix = '' - if returns: - call_prefix = self._get_comma_form(returns) + ' = ' - if self.info.returned: - call_prefix = 'return ' - return call_prefix + self._get_function_call(args) - - def _find_function_arguments(self): - # if not make_global, do not pass any global names; they are - # all visible. - if self.info.global_ and not self.info.make_global: - return () - if not self.info.one_line: - result = (self.info_collector.prewritten & - self.info_collector.read) - result |= (self.info_collector.prewritten & - self.info_collector.postread & - (self.info_collector.maybe_written - - self.info_collector.written)) - return list(result) - start = self.info.region[0] - if start == self.info.lines_region[0]: - start = start + re.search('\S', self.info.extracted).start() - function_definition = self.info.source[start:self.info.region[1]] - read = _VariableReadsAndWritesFinder.find_reads_for_one_liners( - function_definition) - return list(self.info_collector.prewritten.intersection(read)) - - def _find_function_returns(self): - if self.info.one_line or self.info.returned: - return [] - written = self.info_collector.written | \ - self.info_collector.maybe_written - return list(written & self.info_collector.postread) - - def _get_unindented_function_body(self, returns): - if self.info.one_line: - return 'return ' + _join_lines(self.info.extracted) - extracted_body = self.info.extracted - unindented_body = sourceutils.fix_indentation(extracted_body, 0) - if returns: - unindented_body += '\nreturn %s' % self._get_comma_form(returns) - return unindented_body - - -class _ExtractVariableParts(object): - - def __init__(self, info): - self.info = info - - def get_definition(self): - result = self.info.new_name + ' = ' + \ - _join_lines(self.info.extracted) + '\n' - return result - - def get_body_pattern(self): - return '(%s)' % self.info.extracted.strip() - - def get_replacement_pattern(self): - return self.info.new_name - - def get_checks(self): - return {} - - -class _FunctionInformationCollector(object): - - def __init__(self, start, end, is_global): - self.start = start - self.end = end - self.is_global = is_global - self.prewritten = OrderedSet() - self.maybe_written = OrderedSet() - self.written = OrderedSet() - self.read = OrderedSet() - self.postread = OrderedSet() - self.postwritten = OrderedSet() - self.host_function = True - self.conditional = False - - def _read_variable(self, name, lineno): - if self.start <= lineno <= self.end: - if name not in self.written: - if not self.conditional or name not in self.maybe_written: - self.read.add(name) - if self.end < lineno: - if name not in self.postwritten: - self.postread.add(name) - - def _written_variable(self, name, lineno): - if self.start <= lineno <= self.end: - if self.conditional: - self.maybe_written.add(name) - else: - self.written.add(name) - if self.start > lineno: - self.prewritten.add(name) - if self.end < lineno: - self.postwritten.add(name) - - def _FunctionDef(self, node): - if not self.is_global and self.host_function: - self.host_function = False - for name in _get_argnames(node.args): - self._written_variable(name, node.lineno) - for child in node.body: - ast.walk(child, self) - else: - self._written_variable(node.name, node.lineno) - visitor = _VariableReadsAndWritesFinder() - for child in node.body: - ast.walk(child, visitor) - for name in visitor.read - visitor.written: - self._read_variable(name, node.lineno) - - def _Name(self, node): - if isinstance(node.ctx, (ast.Store, ast.AugStore)): - self._written_variable(node.id, node.lineno) - if not isinstance(node.ctx, ast.Store): - self._read_variable(node.id, node.lineno) - - def _Assign(self, node): - ast.walk(node.value, self) - for child in node.targets: - ast.walk(child, self) - - def _ClassDef(self, node): - self._written_variable(node.name, node.lineno) - - def _handle_conditional_node(self, node): - self.conditional = True - try: - for child in ast.get_child_nodes(node): - ast.walk(child, self) - finally: - self.conditional = False - - def _If(self, node): - self._handle_conditional_node(node) - - def _While(self, node): - self._handle_conditional_node(node) - - def _For(self, node): - self.conditional = True - try: - # iter has to be checked before the target variables - ast.walk(node.iter, self) - ast.walk(node.target, self) - - for child in node.body: - ast.walk(child, self) - for child in node.orelse: - ast.walk(child, self) - finally: - self.conditional = False - - -def _get_argnames(arguments): - result = [pycompat.get_ast_arg_arg(node) for node in arguments.args - if isinstance(node, pycompat.ast_arg_type)] - if arguments.vararg: - result.append(pycompat.get_ast_arg_arg(arguments.vararg)) - if arguments.kwarg: - result.append(pycompat.get_ast_arg_arg(arguments.kwarg)) - return result - - -class _VariableReadsAndWritesFinder(object): - - def __init__(self): - self.written = set() - self.read = set() - - def _Name(self, node): - if isinstance(node.ctx, (ast.Store, ast.AugStore)): - self.written.add(node.id) - if not isinstance(node, ast.Store): - self.read.add(node.id) - - def _FunctionDef(self, node): - self.written.add(node.name) - visitor = _VariableReadsAndWritesFinder() - for child in ast.get_child_nodes(node): - ast.walk(child, visitor) - self.read.update(visitor.read - visitor.written) - - def _Class(self, node): - self.written.add(node.name) - - @staticmethod - def find_reads_and_writes(code): - if code.strip() == '': - return set(), set() - if isinstance(code, unicode): - code = code.encode('utf-8') - node = _parse_text(code) - visitor = _VariableReadsAndWritesFinder() - ast.walk(node, visitor) - return visitor.read, visitor.written - - @staticmethod - def find_reads_for_one_liners(code): - if code.strip() == '': - return set(), set() - node = _parse_text(code) - visitor = _VariableReadsAndWritesFinder() - ast.walk(node, visitor) - return visitor.read - - -class _UnmatchedBreakOrContinueFinder(object): - - def __init__(self): - self.error = False - self.loop_count = 0 - - def _For(self, node): - self.loop_encountered(node) - - def _While(self, node): - self.loop_encountered(node) - - def loop_encountered(self, node): - self.loop_count += 1 - for child in node.body: - ast.walk(child, self) - self.loop_count -= 1 - if node.orelse: - if isinstance(node.orelse,(list,tuple)): - for node_ in node.orelse: - ast.walk(node_, self) - else: - ast.walk(node.orelse, self) - - def _Break(self, node): - self.check_loop() - - def _Continue(self, node): - self.check_loop() - - def check_loop(self): - if self.loop_count < 1: - self.error = True - - def _FunctionDef(self, node): - pass - - def _ClassDef(self, node): - pass - - @staticmethod - def has_errors(code): - if code.strip() == '': - return False - node = _parse_text(code) - visitor = _UnmatchedBreakOrContinueFinder() - ast.walk(node, visitor) - return visitor.error - - -def _get_function_kind(scope): - return scope.pyobject.get_kind() - - -def _parse_text(body): - body = sourceutils.fix_indentation(body, 0) - node = ast.parse(body) - return node - - -def _join_lines(code): - lines = [] - for line in code.splitlines(): - if line.endswith('\\'): - lines.append(line[:-1].strip()) - else: - lines.append(line.strip()) - return ' '.join(lines) diff --git a/pythonFiles/rope/refactor/functionutils.py b/pythonFiles/rope/refactor/functionutils.py deleted file mode 100644 index 58baf9174fd9..000000000000 --- a/pythonFiles/rope/refactor/functionutils.py +++ /dev/null @@ -1,222 +0,0 @@ -import rope.base.exceptions -import rope.base.pyobjects -from rope.base.builtins import Lambda -from rope.base import worder - - -class DefinitionInfo(object): - - def __init__(self, function_name, is_method, args_with_defaults, - args_arg, keywords_arg): - self.function_name = function_name - self.is_method = is_method - self.args_with_defaults = args_with_defaults - self.args_arg = args_arg - self.keywords_arg = keywords_arg - - def to_string(self): - return '%s(%s)' % (self.function_name, self.arguments_to_string()) - - def arguments_to_string(self, from_index=0): - params = [] - for arg, default in self.args_with_defaults: - if default is not None: - params.append('%s=%s' % (arg, default)) - else: - params.append(arg) - if self.args_arg is not None: - params.append('*' + self.args_arg) - if self.keywords_arg: - params.append('**' + self.keywords_arg) - return ', '.join(params[from_index:]) - - @staticmethod - def _read(pyfunction, code): - kind = pyfunction.get_kind() - is_method = kind == 'method' - is_lambda = kind == 'lambda' - info = _FunctionParser(code, is_method, is_lambda) - args, keywords = info.get_parameters() - args_arg = None - keywords_arg = None - if args and args[-1].startswith('**'): - keywords_arg = args[-1][2:] - del args[-1] - if args and args[-1].startswith('*'): - args_arg = args[-1][1:] - del args[-1] - args_with_defaults = [(name, None) for name in args] - args_with_defaults.extend(keywords) - return DefinitionInfo(info.get_function_name(), is_method, - args_with_defaults, args_arg, keywords_arg) - - @staticmethod - def read(pyfunction): - pymodule = pyfunction.get_module() - word_finder = worder.Worder(pymodule.source_code) - lineno = pyfunction.get_ast().lineno - start = pymodule.lines.get_line_start(lineno) - if isinstance(pyfunction, Lambda): - call = word_finder.get_lambda_and_args(start) - else: - call = word_finder.get_function_and_args_in_header(start) - return DefinitionInfo._read(pyfunction, call) - - -class CallInfo(object): - - def __init__(self, function_name, args, keywords, args_arg, - keywords_arg, implicit_arg, constructor): - self.function_name = function_name - self.args = args - self.keywords = keywords - self.args_arg = args_arg - self.keywords_arg = keywords_arg - self.implicit_arg = implicit_arg - self.constructor = constructor - - def to_string(self): - function = self.function_name - if self.implicit_arg: - function = self.args[0] + '.' + self.function_name - params = [] - start = 0 - if self.implicit_arg or self.constructor: - start = 1 - if self.args[start:]: - params.extend(self.args[start:]) - if self.keywords: - params.extend(['%s=%s' % (name, value) - for name, value in self.keywords]) - if self.args_arg is not None: - params.append('*' + self.args_arg) - if self.keywords_arg: - params.append('**' + self.keywords_arg) - return '%s(%s)' % (function, ', '.join(params)) - - @staticmethod - def read(primary, pyname, definition_info, code): - is_method_call = CallInfo._is_method_call(primary, pyname) - is_constructor = CallInfo._is_class(pyname) - is_classmethod = CallInfo._is_classmethod(pyname) - info = _FunctionParser(code, is_method_call or is_classmethod) - args, keywords = info.get_parameters() - args_arg = None - keywords_arg = None - if args and args[-1].startswith('**'): - keywords_arg = args[-1][2:] - del args[-1] - if args and args[-1].startswith('*'): - args_arg = args[-1][1:] - del args[-1] - if is_constructor: - args.insert(0, definition_info.args_with_defaults[0][0]) - return CallInfo(info.get_function_name(), args, keywords, args_arg, - keywords_arg, is_method_call or is_classmethod, - is_constructor) - - @staticmethod - def _is_method_call(primary, pyname): - return primary is not None and \ - isinstance(primary.get_object().get_type(), - rope.base.pyobjects.PyClass) and \ - CallInfo._is_method(pyname) - - @staticmethod - def _is_class(pyname): - return pyname is not None and \ - isinstance(pyname.get_object(), - rope.base.pyobjects.PyClass) - - @staticmethod - def _is_method(pyname): - if pyname is not None and \ - isinstance(pyname.get_object(), rope.base.pyobjects.PyFunction): - return pyname.get_object().get_kind() == 'method' - return False - - @staticmethod - def _is_classmethod(pyname): - if pyname is not None and \ - isinstance(pyname.get_object(), rope.base.pyobjects.PyFunction): - return pyname.get_object().get_kind() == 'classmethod' - return False - - -class ArgumentMapping(object): - - def __init__(self, definition_info, call_info): - self.call_info = call_info - self.param_dict = {} - self.keyword_args = [] - self.args_arg = [] - for index, value in enumerate(call_info.args): - if index < len(definition_info.args_with_defaults): - name = definition_info.args_with_defaults[index][0] - self.param_dict[name] = value - else: - self.args_arg.append(value) - for name, value in call_info.keywords: - index = -1 - for pair in definition_info.args_with_defaults: - if pair[0] == name: - self.param_dict[name] = value - break - else: - self.keyword_args.append((name, value)) - - def to_call_info(self, definition_info): - args = [] - keywords = [] - for index in range(len(definition_info.args_with_defaults)): - name = definition_info.args_with_defaults[index][0] - if name in self.param_dict: - args.append(self.param_dict[name]) - else: - for i in range(index, len(definition_info.args_with_defaults)): - name = definition_info.args_with_defaults[i][0] - if name in self.param_dict: - keywords.append((name, self.param_dict[name])) - break - args.extend(self.args_arg) - keywords.extend(self.keyword_args) - return CallInfo(self.call_info.function_name, args, keywords, - self.call_info.args_arg, self.call_info.keywords_arg, - self.call_info.implicit_arg, - self.call_info.constructor) - - -class _FunctionParser(object): - - def __init__(self, call, implicit_arg, is_lambda=False): - self.call = call - self.implicit_arg = implicit_arg - self.word_finder = worder.Worder(self.call) - if is_lambda: - self.last_parens = self.call.rindex(':') - else: - self.last_parens = self.call.rindex(')') - self.first_parens = self.word_finder._find_parens_start( - self.last_parens) - - def get_parameters(self): - args, keywords = self.word_finder.get_parameters(self.first_parens, - self.last_parens) - if self.is_called_as_a_method(): - instance = self.call[:self.call.rindex('.', 0, self.first_parens)] - args.insert(0, instance.strip()) - return args, keywords - - def get_instance(self): - if self.is_called_as_a_method(): - return self.word_finder.get_primary_at( - self.call.rindex('.', 0, self.first_parens) - 1) - - def get_function_name(self): - if self.is_called_as_a_method(): - return self.word_finder.get_word_at(self.first_parens - 1) - else: - return self.word_finder.get_primary_at(self.first_parens - 1) - - def is_called_as_a_method(self): - return self.implicit_arg and '.' in self.call[:self.first_parens] diff --git a/pythonFiles/rope/refactor/importutils/__init__.py b/pythonFiles/rope/refactor/importutils/__init__.py deleted file mode 100644 index 6a44f01b6cd9..000000000000 --- a/pythonFiles/rope/refactor/importutils/__init__.py +++ /dev/null @@ -1,316 +0,0 @@ -"""A package for handling imports - -This package provides tools for modifying module imports after -refactorings or as a separate task. - -""" -import rope.base.evaluate -from rope.base import libutils -from rope.base.change import ChangeSet, ChangeContents -from rope.refactor import occurrences, rename -from rope.refactor.importutils import module_imports, actions -from rope.refactor.importutils.importinfo import NormalImport, FromImport -import rope.base.codeanalyze - - -class ImportOrganizer(object): - """Perform some import-related commands - - Each method returns a `rope.base.change.Change` object. - - """ - - def __init__(self, project): - self.project = project - self.import_tools = ImportTools(self.project) - - def organize_imports(self, resource, offset=None): - return self._perform_command_on_import_tools( - self.import_tools.organize_imports, resource, offset) - - def expand_star_imports(self, resource, offset=None): - return self._perform_command_on_import_tools( - self.import_tools.expand_stars, resource, offset) - - def froms_to_imports(self, resource, offset=None): - return self._perform_command_on_import_tools( - self.import_tools.froms_to_imports, resource, offset) - - def relatives_to_absolutes(self, resource, offset=None): - return self._perform_command_on_import_tools( - self.import_tools.relatives_to_absolutes, resource, offset) - - def handle_long_imports(self, resource, offset=None): - return self._perform_command_on_import_tools( - self.import_tools.handle_long_imports, resource, offset) - - def _perform_command_on_import_tools(self, method, resource, offset): - pymodule = self.project.get_pymodule(resource) - before_performing = pymodule.source_code - import_filter = None - if offset is not None: - import_filter = self._line_filter( - pymodule.lines.get_line_number(offset)) - result = method(pymodule, import_filter=import_filter) - if result is not None and result != before_performing: - changes = ChangeSet(method.__name__.replace('_', ' ') + - ' in <%s>' % resource.path) - changes.add_change(ChangeContents(resource, result)) - return changes - - def _line_filter(self, lineno): - def import_filter(import_stmt): - return import_stmt.start_line <= lineno < import_stmt.end_line - return import_filter - - -class ImportTools(object): - - def __init__(self, project): - self.project = project - - def get_import(self, resource): - """The import statement for `resource`""" - module_name = libutils.modname(resource) - return NormalImport(((module_name, None), )) - - def get_from_import(self, resource, name): - """The from import statement for `name` in `resource`""" - module_name = libutils.modname(resource) - names = [] - if isinstance(name, list): - names = [(imported, None) for imported in name] - else: - names = [(name, None), ] - return FromImport(module_name, 0, tuple(names)) - - def module_imports(self, module, imports_filter=None): - return module_imports.ModuleImports(self.project, module, - imports_filter) - - def froms_to_imports(self, pymodule, import_filter=None): - pymodule = self._clean_up_imports(pymodule, import_filter) - module_imports = self.module_imports(pymodule, import_filter) - for import_stmt in module_imports.imports: - if import_stmt.readonly or \ - not self._is_transformable_to_normal(import_stmt.import_info): - continue - pymodule = self._from_to_normal(pymodule, import_stmt) - - # Adding normal imports in place of froms - module_imports = self.module_imports(pymodule, import_filter) - for import_stmt in module_imports.imports: - if not import_stmt.readonly and \ - self._is_transformable_to_normal(import_stmt.import_info): - import_stmt.import_info = \ - NormalImport(((import_stmt.import_info.module_name, - None),)) - module_imports.remove_duplicates() - return module_imports.get_changed_source() - - def expand_stars(self, pymodule, import_filter=None): - module_imports = self.module_imports(pymodule, import_filter) - module_imports.expand_stars() - return module_imports.get_changed_source() - - def _from_to_normal(self, pymodule, import_stmt): - resource = pymodule.get_resource() - from_import = import_stmt.import_info - module_name = from_import.module_name - for name, alias in from_import.names_and_aliases: - imported = name - if alias is not None: - imported = alias - occurrence_finder = occurrences.create_finder( - self.project, imported, pymodule[imported], imports=False) - source = rename.rename_in_module( - occurrence_finder, module_name + '.' + name, - pymodule=pymodule, replace_primary=True) - if source is not None: - pymodule = libutils.get_string_module( - self.project, source, resource) - return pymodule - - def _clean_up_imports(self, pymodule, import_filter): - resource = pymodule.get_resource() - module_with_imports = self.module_imports(pymodule, import_filter) - module_with_imports.expand_stars() - source = module_with_imports.get_changed_source() - if source is not None: - pymodule = libutils.get_string_module( - self.project, source, resource) - source = self.relatives_to_absolutes(pymodule) - if source is not None: - pymodule = libutils.get_string_module( - self.project, source, resource) - - module_with_imports = self.module_imports(pymodule, import_filter) - module_with_imports.remove_duplicates() - module_with_imports.remove_unused_imports() - source = module_with_imports.get_changed_source() - if source is not None: - pymodule = libutils.get_string_module( - self.project, source, resource) - return pymodule - - def relatives_to_absolutes(self, pymodule, import_filter=None): - module_imports = self.module_imports(pymodule, import_filter) - to_be_absolute_list = module_imports.get_relative_to_absolute_list() - for name, absolute_name in to_be_absolute_list: - pymodule = self._rename_in_module(pymodule, name, absolute_name) - module_imports = self.module_imports(pymodule, import_filter) - module_imports.get_relative_to_absolute_list() - source = module_imports.get_changed_source() - if source is None: - source = pymodule.source_code - return source - - def _is_transformable_to_normal(self, import_info): - if not isinstance(import_info, FromImport): - return False - return True - - def organize_imports(self, pymodule, - unused=True, duplicates=True, - selfs=True, sort=True, import_filter=None): - if unused or duplicates: - module_imports = self.module_imports(pymodule, import_filter) - if unused: - module_imports.remove_unused_imports() - if self.project.prefs.get("split_imports"): - module_imports.force_single_imports() - if duplicates: - module_imports.remove_duplicates() - source = module_imports.get_changed_source() - if source is not None: - pymodule = libutils.get_string_module( - self.project, source, pymodule.get_resource()) - if selfs: - pymodule = self._remove_self_imports(pymodule, import_filter) - if sort: - return self.sort_imports(pymodule, import_filter) - else: - return pymodule.source_code - - def _remove_self_imports(self, pymodule, import_filter=None): - module_imports = self.module_imports(pymodule, import_filter) - to_be_fixed, to_be_renamed = \ - module_imports.get_self_import_fix_and_rename_list() - for name in to_be_fixed: - try: - pymodule = self._rename_in_module(pymodule, name, '', - till_dot=True) - except ValueError: - # There is a self import with direct access to it - return pymodule - for name, new_name in to_be_renamed: - pymodule = self._rename_in_module(pymodule, name, new_name) - module_imports = self.module_imports(pymodule, import_filter) - module_imports.get_self_import_fix_and_rename_list() - source = module_imports.get_changed_source() - if source is not None: - pymodule = libutils.get_string_module( - self.project, source, pymodule.get_resource()) - return pymodule - - def _rename_in_module(self, pymodule, name, new_name, till_dot=False): - old_name = name.split('.')[-1] - old_pyname = rope.base.evaluate.eval_str(pymodule.get_scope(), name) - occurrence_finder = occurrences.create_finder( - self.project, old_name, old_pyname, imports=False) - changes = rope.base.codeanalyze.ChangeCollector(pymodule.source_code) - for occurrence in occurrence_finder.find_occurrences( - pymodule=pymodule): - start, end = occurrence.get_primary_range() - if till_dot: - new_end = pymodule.source_code.index('.', end) + 1 - space = pymodule.source_code[end:new_end - 1].strip() - if not space == '': - for c in space: - if not c.isspace() and c not in '\\': - raise ValueError() - end = new_end - changes.add_change(start, end, new_name) - source = changes.get_changed() - if source is not None: - pymodule = libutils.get_string_module( - self.project, source, pymodule.get_resource()) - return pymodule - - def sort_imports(self, pymodule, import_filter=None): - module_imports = self.module_imports(pymodule, import_filter) - module_imports.sort_imports() - return module_imports.get_changed_source() - - def handle_long_imports(self, pymodule, maxdots=2, maxlength=27, - import_filter=None): - # IDEA: `maxdots` and `maxlength` can be specified in project config - # adding new from imports - module_imports = self.module_imports(pymodule, import_filter) - to_be_fixed = module_imports.handle_long_imports(maxdots, maxlength) - # performing the renaming - pymodule = libutils.get_string_module( - self.project, module_imports.get_changed_source(), - resource=pymodule.get_resource()) - for name in to_be_fixed: - pymodule = self._rename_in_module(pymodule, name, - name.split('.')[-1]) - # organizing imports - return self.organize_imports(pymodule, selfs=False, sort=False, - import_filter=import_filter) - - -def get_imports(project, pydefined): - """A shortcut for getting the `ImportInfo`\s used in a scope""" - pymodule = pydefined.get_module() - module = module_imports.ModuleImports(project, pymodule) - if pymodule == pydefined: - return [stmt.import_info for stmt in module.imports] - return module.get_used_imports(pydefined) - - -def get_module_imports(project, pymodule): - """A shortcut for creating a `module_imports.ModuleImports` object""" - return module_imports.ModuleImports(project, pymodule) - - -def add_import(project, pymodule, module_name, name=None): - imports = get_module_imports(project, pymodule) - candidates = [] - names = [] - selected_import = None - # from mod import name - if name is not None: - from_import = FromImport(module_name, 0, [(name, None)]) - names.append(name) - candidates.append(from_import) - # from pkg import mod - if '.' in module_name: - pkg, mod = module_name.rsplit('.', 1) - from_import = FromImport(pkg, 0, [(mod, None)]) - if project.prefs.get('prefer_module_from_imports'): - selected_import = from_import - candidates.append(from_import) - if name: - names.append(mod + '.' + name) - else: - names.append(mod) - # import mod - normal_import = NormalImport([(module_name, None)]) - if name: - names.append(module_name + '.' + name) - else: - names.append(module_name) - - candidates.append(normal_import) - - visitor = actions.AddingVisitor(project, candidates) - if selected_import is None: - selected_import = normal_import - for import_statement in imports.imports: - if import_statement.accept(visitor): - selected_import = visitor.import_info - break - imports.add_import(selected_import) - imported_name = names[candidates.index(selected_import)] - return imports.get_changed_source(), imported_name diff --git a/pythonFiles/rope/refactor/importutils/actions.py b/pythonFiles/rope/refactor/importutils/actions.py deleted file mode 100644 index fd0f70542b10..000000000000 --- a/pythonFiles/rope/refactor/importutils/actions.py +++ /dev/null @@ -1,361 +0,0 @@ -from rope.base import libutils -from rope.base import pyobjects, exceptions, stdmods -from rope.refactor import occurrences -from rope.refactor.importutils import importinfo - - -class ImportInfoVisitor(object): - - def dispatch(self, import_): - try: - method_name = 'visit' + import_.import_info.__class__.__name__ - method = getattr(self, method_name) - return method(import_, import_.import_info) - except exceptions.ModuleNotFoundError: - pass - - def visitEmptyImport(self, import_stmt, import_info): - pass - - def visitNormalImport(self, import_stmt, import_info): - pass - - def visitFromImport(self, import_stmt, import_info): - pass - - -class RelativeToAbsoluteVisitor(ImportInfoVisitor): - - def __init__(self, project, current_folder): - self.to_be_absolute = [] - self.project = project - self.folder = current_folder - self.context = importinfo.ImportContext(project, current_folder) - - def visitNormalImport(self, import_stmt, import_info): - self.to_be_absolute.extend( - self._get_relative_to_absolute_list(import_info)) - new_pairs = [] - for name, alias in import_info.names_and_aliases: - resource = self.project.find_module(name, folder=self.folder) - if resource is None: - new_pairs.append((name, alias)) - continue - absolute_name = libutils.modname(resource) - new_pairs.append((absolute_name, alias)) - if not import_info._are_name_and_alias_lists_equal( - new_pairs, import_info.names_and_aliases): - import_stmt.import_info = importinfo.NormalImport(new_pairs) - - def _get_relative_to_absolute_list(self, import_info): - result = [] - for name, alias in import_info.names_and_aliases: - if alias is not None: - continue - resource = self.project.find_module(name, folder=self.folder) - if resource is None: - continue - absolute_name = libutils.modname(resource) - if absolute_name != name: - result.append((name, absolute_name)) - return result - - def visitFromImport(self, import_stmt, import_info): - resource = import_info.get_imported_resource(self.context) - if resource is None: - return None - absolute_name = libutils.modname(resource) - if import_info.module_name != absolute_name: - import_stmt.import_info = importinfo.FromImport( - absolute_name, 0, import_info.names_and_aliases) - - -class FilteringVisitor(ImportInfoVisitor): - - def __init__(self, project, folder, can_select): - self.to_be_absolute = [] - self.project = project - self.can_select = self._transform_can_select(can_select) - self.context = importinfo.ImportContext(project, folder) - - def _transform_can_select(self, can_select): - def can_select_name_and_alias(name, alias): - imported = name - if alias is not None: - imported = alias - return can_select(imported) - return can_select_name_and_alias - - def visitNormalImport(self, import_stmt, import_info): - new_pairs = [] - for name, alias in import_info.names_and_aliases: - if self.can_select(name, alias): - new_pairs.append((name, alias)) - return importinfo.NormalImport(new_pairs) - - def visitFromImport(self, import_stmt, import_info): - if _is_future(import_info): - return import_info - new_pairs = [] - if import_info.is_star_import(): - for name in import_info.get_imported_names(self.context): - if self.can_select(name, None): - new_pairs.append(import_info.names_and_aliases[0]) - break - else: - for name, alias in import_info.names_and_aliases: - if self.can_select(name, alias): - new_pairs.append((name, alias)) - return importinfo.FromImport( - import_info.module_name, import_info.level, new_pairs) - - -class RemovingVisitor(ImportInfoVisitor): - - def __init__(self, project, folder, can_select): - self.to_be_absolute = [] - self.project = project - self.filtering = FilteringVisitor(project, folder, can_select) - - def dispatch(self, import_): - result = self.filtering.dispatch(import_) - if result is not None: - import_.import_info = result - - -class AddingVisitor(ImportInfoVisitor): - """A class for adding imports - - Given a list of `ImportInfo`\s, it tries to add each import to the - module and returns `True` and gives up when an import can be added - to older ones. - - """ - - def __init__(self, project, import_list): - self.project = project - self.import_list = import_list - self.import_info = None - - def dispatch(self, import_): - for import_info in self.import_list: - self.import_info = import_info - if ImportInfoVisitor.dispatch(self, import_): - return True - - # TODO: Handle adding relative and absolute imports - def visitNormalImport(self, import_stmt, import_info): - if not isinstance(self.import_info, import_info.__class__): - return False - # Adding ``import x`` and ``import x.y`` that results ``import x.y`` - if len(import_info.names_and_aliases) == \ - len(self.import_info.names_and_aliases) == 1: - imported1 = import_info.names_and_aliases[0] - imported2 = self.import_info.names_and_aliases[0] - if imported1[1] == imported2[1] is None: - if imported1[0].startswith(imported2[0] + '.'): - return True - if imported2[0].startswith(imported1[0] + '.'): - import_stmt.import_info = self.import_info - return True - # Multiple imports using a single import statement is discouraged - # so we won't bother adding them. - if self.import_info._are_name_and_alias_lists_equal( - import_info.names_and_aliases, - self.import_info.names_and_aliases): - return True - - def visitFromImport(self, import_stmt, import_info): - if isinstance(self.import_info, import_info.__class__) and \ - import_info.module_name == self.import_info.module_name and \ - import_info.level == self.import_info.level: - if import_info.is_star_import(): - return True - if self.import_info.is_star_import(): - import_stmt.import_info = self.import_info - return True - if self.project.prefs.get("split_imports"): - return self.import_info.names_and_aliases == \ - import_info.names_and_aliases - new_pairs = list(import_info.names_and_aliases) - for pair in self.import_info.names_and_aliases: - if pair not in new_pairs: - new_pairs.append(pair) - import_stmt.import_info = importinfo.FromImport( - import_info.module_name, import_info.level, new_pairs) - return True - - -class ExpandStarsVisitor(ImportInfoVisitor): - - def __init__(self, project, folder, can_select): - self.project = project - self.filtering = FilteringVisitor(project, folder, can_select) - self.context = importinfo.ImportContext(project, folder) - - def visitNormalImport(self, import_stmt, import_info): - self.filtering.dispatch(import_stmt) - - def visitFromImport(self, import_stmt, import_info): - if import_info.is_star_import(): - new_pairs = [] - for name in import_info.get_imported_names(self.context): - new_pairs.append((name, None)) - new_import = importinfo.FromImport( - import_info.module_name, import_info.level, new_pairs) - import_stmt.import_info = \ - self.filtering.visitFromImport(None, new_import) - else: - self.filtering.dispatch(import_stmt) - - -class SelfImportVisitor(ImportInfoVisitor): - - def __init__(self, project, current_folder, resource): - self.project = project - self.folder = current_folder - self.resource = resource - self.to_be_fixed = set() - self.to_be_renamed = set() - self.context = importinfo.ImportContext(project, current_folder) - - def visitNormalImport(self, import_stmt, import_info): - new_pairs = [] - for name, alias in import_info.names_and_aliases: - resource = self.project.find_module(name, folder=self.folder) - if resource is not None and resource == self.resource: - imported = name - if alias is not None: - imported = alias - self.to_be_fixed.add(imported) - else: - new_pairs.append((name, alias)) - if not import_info._are_name_and_alias_lists_equal( - new_pairs, import_info.names_and_aliases): - import_stmt.import_info = importinfo.NormalImport(new_pairs) - - def visitFromImport(self, import_stmt, import_info): - resource = import_info.get_imported_resource(self.context) - if resource is None: - return - if resource == self.resource: - self._importing_names_from_self(import_info, import_stmt) - return - pymodule = self.project.get_pymodule(resource) - new_pairs = [] - for name, alias in import_info.names_and_aliases: - try: - result = pymodule[name].get_object() - if isinstance(result, pyobjects.PyModule) and \ - result.get_resource() == self.resource: - imported = name - if alias is not None: - imported = alias - self.to_be_fixed.add(imported) - else: - new_pairs.append((name, alias)) - except exceptions.AttributeNotFoundError: - new_pairs.append((name, alias)) - if not import_info._are_name_and_alias_lists_equal( - new_pairs, import_info.names_and_aliases): - import_stmt.import_info = importinfo.FromImport( - import_info.module_name, import_info.level, new_pairs) - - def _importing_names_from_self(self, import_info, import_stmt): - if not import_info.is_star_import(): - for name, alias in import_info.names_and_aliases: - if alias is not None: - self.to_be_renamed.add((alias, name)) - import_stmt.empty_import() - - -class SortingVisitor(ImportInfoVisitor): - - def __init__(self, project, current_folder): - self.project = project - self.folder = current_folder - self.standard = set() - self.third_party = set() - self.in_project = set() - self.future = set() - self.context = importinfo.ImportContext(project, current_folder) - - def visitNormalImport(self, import_stmt, import_info): - if import_info.names_and_aliases: - name, alias = import_info.names_and_aliases[0] - resource = self.project.find_module( - name, folder=self.folder) - self._check_imported_resource(import_stmt, resource, name) - - def visitFromImport(self, import_stmt, import_info): - resource = import_info.get_imported_resource(self.context) - self._check_imported_resource(import_stmt, resource, - import_info.module_name) - - def _check_imported_resource(self, import_stmt, resource, imported_name): - info = import_stmt.import_info - if resource is not None and resource.project == self.project: - self.in_project.add(import_stmt) - elif _is_future(info): - self.future.add(import_stmt) - elif imported_name.split('.')[0] in stdmods.standard_modules(): - self.standard.add(import_stmt) - else: - self.third_party.add(import_stmt) - - -class LongImportVisitor(ImportInfoVisitor): - - def __init__(self, current_folder, project, maxdots, maxlength): - self.maxdots = maxdots - self.maxlength = maxlength - self.to_be_renamed = set() - self.current_folder = current_folder - self.project = project - self.new_imports = [] - - def visitNormalImport(self, import_stmt, import_info): - for name, alias in import_info.names_and_aliases: - if alias is None and self._is_long(name): - self.to_be_renamed.add(name) - last_dot = name.rindex('.') - from_ = name[:last_dot] - imported = name[last_dot + 1:] - self.new_imports.append( - importinfo.FromImport(from_, 0, ((imported, None), ))) - - def _is_long(self, name): - return name.count('.') > self.maxdots or \ - ('.' in name and len(name) > self.maxlength) - - -class RemovePyNameVisitor(ImportInfoVisitor): - - def __init__(self, project, pymodule, pyname, folder): - self.pymodule = pymodule - self.pyname = pyname - self.context = importinfo.ImportContext(project, folder) - - def visitFromImport(self, import_stmt, import_info): - new_pairs = [] - if not import_info.is_star_import(): - for name, alias in import_info.names_and_aliases: - try: - pyname = self.pymodule[alias or name] - if occurrences.same_pyname(self.pyname, pyname): - continue - except exceptions.AttributeNotFoundError: - pass - new_pairs.append((name, alias)) - return importinfo.FromImport( - import_info.module_name, import_info.level, new_pairs) - - def dispatch(self, import_): - result = ImportInfoVisitor.dispatch(self, import_) - if result is not None: - import_.import_info = result - - -def _is_future(info): - return isinstance(info, importinfo.FromImport) and \ - info.module_name == '__future__' diff --git a/pythonFiles/rope/refactor/importutils/importinfo.py b/pythonFiles/rope/refactor/importutils/importinfo.py deleted file mode 100644 index 114080aac486..000000000000 --- a/pythonFiles/rope/refactor/importutils/importinfo.py +++ /dev/null @@ -1,201 +0,0 @@ -class ImportStatement(object): - """Represent an import in a module - - `readonly` attribute controls whether this import can be changed - by import actions or not. - - """ - - def __init__(self, import_info, start_line, end_line, - main_statement=None, blank_lines=0): - self.start_line = start_line - self.end_line = end_line - self.readonly = False - self.main_statement = main_statement - self._import_info = None - self.import_info = import_info - self._is_changed = False - self.new_start = None - self.blank_lines = blank_lines - - def _get_import_info(self): - return self._import_info - - def _set_import_info(self, new_import): - if not self.readonly and \ - new_import is not None and not new_import == self._import_info: - self._is_changed = True - self._import_info = new_import - - import_info = property(_get_import_info, _set_import_info) - - def get_import_statement(self): - if self._is_changed or self.main_statement is None: - return self.import_info.get_import_statement() - else: - return self.main_statement - - def empty_import(self): - self.import_info = ImportInfo.get_empty_import() - - def move(self, lineno, blank_lines=0): - self.new_start = lineno - self.blank_lines = blank_lines - - def get_old_location(self): - return self.start_line, self.end_line - - def get_new_start(self): - return self.new_start - - def is_changed(self): - return self._is_changed or (self.new_start is not None or - self.new_start != self.start_line) - - def accept(self, visitor): - return visitor.dispatch(self) - - -class ImportInfo(object): - - def get_imported_primaries(self, context): - pass - - def get_imported_names(self, context): - return [primary.split('.')[0] - for primary in self.get_imported_primaries(context)] - - def get_import_statement(self): - pass - - def is_empty(self): - pass - - def __hash__(self): - return hash(self.get_import_statement()) - - def _are_name_and_alias_lists_equal(self, list1, list2): - if len(list1) != len(list2): - return False - for pair1, pair2 in zip(list1, list2): - if pair1 != pair2: - return False - return True - - def __eq__(self, obj): - return isinstance(obj, self.__class__) and \ - self.get_import_statement() == obj.get_import_statement() - - def __ne__(self, obj): - return not self.__eq__(obj) - - @staticmethod - def get_empty_import(): - return EmptyImport() - - -class NormalImport(ImportInfo): - - def __init__(self, names_and_aliases): - self.names_and_aliases = names_and_aliases - - def get_imported_primaries(self, context): - result = [] - for name, alias in self.names_and_aliases: - if alias: - result.append(alias) - else: - result.append(name) - return result - - def get_import_statement(self): - result = 'import ' - for name, alias in self.names_and_aliases: - result += name - if alias: - result += ' as ' + alias - result += ', ' - return result[:-2] - - def is_empty(self): - return len(self.names_and_aliases) == 0 - - -class FromImport(ImportInfo): - - def __init__(self, module_name, level, names_and_aliases): - self.module_name = module_name - self.level = level - self.names_and_aliases = names_and_aliases - - def get_imported_primaries(self, context): - if self.names_and_aliases[0][0] == '*': - module = self.get_imported_module(context) - return [name for name in module - if not name.startswith('_')] - result = [] - for name, alias in self.names_and_aliases: - if alias: - result.append(alias) - else: - result.append(name) - return result - - def get_imported_resource(self, context): - """Get the imported resource - - Returns `None` if module was not found. - """ - if self.level == 0: - return context.project.find_module( - self.module_name, folder=context.folder) - else: - return context.project.find_relative_module( - self.module_name, context.folder, self.level) - - def get_imported_module(self, context): - """Get the imported `PyModule` - - Raises `rope.base.exceptions.ModuleNotFoundError` if module - could not be found. - """ - if self.level == 0: - return context.project.get_module( - self.module_name, context.folder) - else: - return context.project.get_relative_module( - self.module_name, context.folder, self.level) - - def get_import_statement(self): - result = 'from ' + '.' * self.level + self.module_name + ' import ' - for name, alias in self.names_and_aliases: - result += name - if alias: - result += ' as ' + alias - result += ', ' - return result[:-2] - - def is_empty(self): - return len(self.names_and_aliases) == 0 - - def is_star_import(self): - return len(self.names_and_aliases) > 0 and \ - self.names_and_aliases[0][0] == '*' - - -class EmptyImport(ImportInfo): - - names_and_aliases = [] - - def is_empty(self): - return True - - def get_imported_primaries(self, context): - return [] - - -class ImportContext(object): - - def __init__(self, project, folder): - self.project = project - self.folder = folder diff --git a/pythonFiles/rope/refactor/importutils/module_imports.py b/pythonFiles/rope/refactor/importutils/module_imports.py deleted file mode 100644 index 26059a49af50..000000000000 --- a/pythonFiles/rope/refactor/importutils/module_imports.py +++ /dev/null @@ -1,487 +0,0 @@ -from rope.base import ast -from rope.base import pynames -from rope.base import utils -from rope.refactor.importutils import actions -from rope.refactor.importutils import importinfo - - -class ModuleImports(object): - - def __init__(self, project, pymodule, import_filter=None): - self.project = project - self.pymodule = pymodule - self.separating_lines = 0 - self.filter = import_filter - - @property - @utils.saveit - def imports(self): - finder = _GlobalImportFinder(self.pymodule) - result = finder.find_import_statements() - self.separating_lines = finder.get_separating_line_count() - if self.filter is not None: - for import_stmt in result: - if not self.filter(import_stmt): - import_stmt.readonly = True - return result - - def _get_unbound_names(self, defined_pyobject): - visitor = _GlobalUnboundNameFinder(self.pymodule, defined_pyobject) - ast.walk(self.pymodule.get_ast(), visitor) - return visitor.unbound - - def remove_unused_imports(self): - can_select = _OneTimeSelector(self._get_unbound_names(self.pymodule)) - visitor = actions.RemovingVisitor( - self.project, self._current_folder(), can_select) - for import_statement in self.imports: - import_statement.accept(visitor) - - def get_used_imports(self, defined_pyobject): - result = [] - can_select = _OneTimeSelector( - self._get_unbound_names(defined_pyobject)) - visitor = actions.FilteringVisitor( - self.project, self._current_folder(), can_select) - for import_statement in self.imports: - new_import = import_statement.accept(visitor) - if new_import is not None and not new_import.is_empty(): - result.append(new_import) - return result - - def get_changed_source(self): - # Make sure we forward a removed import's preceding blank - # lines count to the following import statement. - prev_stmt = None - for stmt in self.imports: - if prev_stmt is not None and prev_stmt.import_info.is_empty(): - stmt.blank_lines = max(prev_stmt.blank_lines, stmt.blank_lines) - prev_stmt = stmt - # The new list of imports. - imports = [stmt for stmt in self.imports - if not stmt.import_info.is_empty()] - - after_removing = self._remove_imports(self.imports) - first_non_blank = self._first_non_blank_line(after_removing, 0) - first_import = self._first_import_line() - 1 - result = [] - # Writing module docs - result.extend(after_removing[first_non_blank:first_import]) - # Writing imports - sorted_imports = sorted(imports, key=self._get_location) - for stmt in sorted_imports: - if stmt != sorted_imports[0]: - result.append('\n' * stmt.blank_lines) - result.append(stmt.get_import_statement() + '\n') - if sorted_imports and first_non_blank < len(after_removing): - result.append('\n' * self.separating_lines) - - # Writing the body - first_after_imports = self._first_non_blank_line(after_removing, - first_import) - result.extend(after_removing[first_after_imports:]) - return ''.join(result) - - def _get_import_location(self, stmt): - start = stmt.get_new_start() - if start is None: - start = stmt.get_old_location()[0] - return start - - def _get_location(self, stmt): - if stmt.get_new_start() is not None: - return stmt.get_new_start() - else: - return stmt.get_old_location()[0] - - def _remove_imports(self, imports): - lines = self.pymodule.source_code.splitlines(True) - after_removing = [] - first_import_line = self._first_import_line() - last_index = 0 - for stmt in imports: - start, end = stmt.get_old_location() - blank_lines = 0 - if start != first_import_line: - blank_lines = _count_blank_lines(lines.__getitem__, start - 2, - last_index - 1, -1) - after_removing.extend(lines[last_index:start - 1 - blank_lines]) - last_index = end - 1 - after_removing.extend(lines[last_index:]) - return after_removing - - def _first_non_blank_line(self, lines, lineno): - return lineno + _count_blank_lines(lines.__getitem__, lineno, - len(lines)) - - def add_import(self, import_info): - visitor = actions.AddingVisitor(self.project, [import_info]) - for import_statement in self.imports: - if import_statement.accept(visitor): - break - else: - lineno = self._get_new_import_lineno() - blanks = self._get_new_import_blanks() - self.imports.append(importinfo.ImportStatement( - import_info, lineno, lineno, - blank_lines=blanks)) - - def _get_new_import_blanks(self): - return 0 - - def _get_new_import_lineno(self): - if self.imports: - return self.imports[-1].end_line - return 1 - - def filter_names(self, can_select): - visitor = actions.RemovingVisitor( - self.project, self._current_folder(), can_select) - for import_statement in self.imports: - import_statement.accept(visitor) - - def expand_stars(self): - can_select = _OneTimeSelector(self._get_unbound_names(self.pymodule)) - visitor = actions.ExpandStarsVisitor( - self.project, self._current_folder(), can_select) - for import_statement in self.imports: - import_statement.accept(visitor) - - def remove_duplicates(self): - added_imports = [] - for import_stmt in self.imports: - visitor = actions.AddingVisitor(self.project, - [import_stmt.import_info]) - for added_import in added_imports: - if added_import.accept(visitor): - import_stmt.empty_import() - else: - added_imports.append(import_stmt) - - def force_single_imports(self): - """force a single import per statement""" - for import_stmt in self.imports[:]: - import_info = import_stmt.import_info - if import_info.is_empty() or import_stmt.readonly: - continue - if len(import_info.names_and_aliases) > 1: - for name_and_alias in import_info.names_and_aliases: - if hasattr(import_info, "module_name"): - new_import = importinfo.FromImport( - import_info.module_name, import_info.level, - [name_and_alias]) - else: - new_import = importinfo.NormalImport([name_and_alias]) - self.add_import(new_import) - import_stmt.empty_import() - - def get_relative_to_absolute_list(self): - visitor = actions.RelativeToAbsoluteVisitor( - self.project, self._current_folder()) - for import_stmt in self.imports: - if not import_stmt.readonly: - import_stmt.accept(visitor) - return visitor.to_be_absolute - - def get_self_import_fix_and_rename_list(self): - visitor = actions.SelfImportVisitor( - self.project, self._current_folder(), self.pymodule.get_resource()) - for import_stmt in self.imports: - if not import_stmt.readonly: - import_stmt.accept(visitor) - return visitor.to_be_fixed, visitor.to_be_renamed - - def _current_folder(self): - return self.pymodule.get_resource().parent - - def sort_imports(self): - if self.project.prefs.get("sort_imports_alphabetically"): - sort_kwargs = dict(key=self._get_import_name) - else: - sort_kwargs = dict(key=self._key_imports) - - # IDEA: Sort from import list - visitor = actions.SortingVisitor(self.project, self._current_folder()) - for import_statement in self.imports: - import_statement.accept(visitor) - in_projects = sorted(visitor.in_project, **sort_kwargs) - third_party = sorted(visitor.third_party, **sort_kwargs) - standards = sorted(visitor.standard, **sort_kwargs) - future = sorted(visitor.future, **sort_kwargs) - last_index = self._first_import_line() - last_index = self._move_imports(future, last_index, 0) - last_index = self._move_imports(standards, last_index, 1) - last_index = self._move_imports(third_party, last_index, 1) - last_index = self._move_imports(in_projects, last_index, 1) - self.separating_lines = 2 - - def _first_import_line(self): - nodes = self.pymodule.get_ast().body - lineno = 0 - if self.pymodule.get_doc() is not None: - lineno = 1 - if len(nodes) > lineno: - if (isinstance(nodes[lineno], ast.Import) or - isinstance(nodes[lineno], ast.ImportFrom)): - return nodes[lineno].lineno - lineno = self.pymodule.logical_lines.logical_line_in( - nodes[lineno].lineno)[0] - else: - lineno = self.pymodule.lines.length() - - return lineno - _count_blank_lines(self.pymodule.lines.get_line, - lineno - 1, 1, -1) - - def _get_import_name(self, import_stmt): - import_info = import_stmt.import_info - if hasattr(import_info, "module_name"): - return "%s.%s" % (import_info.module_name, - import_info.names_and_aliases[0][0]) - else: - return import_info.names_and_aliases[0][0] - - def _key_imports(self, stm1): - str1 = stm1.get_import_statement() - return str1.startswith("from "), str1 - - #str1 = stmt1.get_import_statement() - #str2 = stmt2.get_import_statement() - #if str1.startswith('from ') and not str2.startswith('from '): - # return 1 - #if not str1.startswith('from ') and str2.startswith('from '): - # return -1 - #return cmp(str1, str2) - - def _move_imports(self, imports, index, blank_lines): - if imports: - imports[0].move(index, blank_lines) - index += 1 - if len(imports) > 1: - for stmt in imports[1:]: - stmt.move(index) - index += 1 - return index - - def handle_long_imports(self, maxdots, maxlength): - visitor = actions.LongImportVisitor( - self._current_folder(), self.project, maxdots, maxlength) - for import_statement in self.imports: - if not import_statement.readonly: - import_statement.accept(visitor) - for import_info in visitor.new_imports: - self.add_import(import_info) - return visitor.to_be_renamed - - def remove_pyname(self, pyname): - """Removes pyname when imported in ``from mod import x``""" - visitor = actions.RemovePyNameVisitor(self.project, self.pymodule, - pyname, self._current_folder()) - for import_stmt in self.imports: - import_stmt.accept(visitor) - - -def _count_blank_lines(get_line, start, end, step=1): - count = 0 - for idx in range(start, end, step): - if get_line(idx).strip() == '': - count += 1 - else: - break - return count - - -class _OneTimeSelector(object): - - def __init__(self, names): - self.names = names - self.selected_names = set() - - def __call__(self, imported_primary): - if self._can_name_be_added(imported_primary): - for name in self._get_dotted_tokens(imported_primary): - self.selected_names.add(name) - return True - return False - - def _get_dotted_tokens(self, imported_primary): - tokens = imported_primary.split('.') - for i in range(len(tokens)): - yield '.'.join(tokens[:i + 1]) - - def _can_name_be_added(self, imported_primary): - for name in self._get_dotted_tokens(imported_primary): - if name in self.names and name not in self.selected_names: - return True - return False - - -class _UnboundNameFinder(object): - - def __init__(self, pyobject): - self.pyobject = pyobject - - def _visit_child_scope(self, node): - pyobject = self.pyobject.get_module().get_scope().\ - get_inner_scope_for_line(node.lineno).pyobject - visitor = _LocalUnboundNameFinder(pyobject, self) - for child in ast.get_child_nodes(node): - ast.walk(child, visitor) - - def _FunctionDef(self, node): - self._visit_child_scope(node) - - def _ClassDef(self, node): - self._visit_child_scope(node) - - def _Name(self, node): - if self._get_root()._is_node_interesting(node) and \ - not self.is_bound(node.id): - self.add_unbound(node.id) - - def _Attribute(self, node): - result = [] - while isinstance(node, ast.Attribute): - result.append(node.attr) - node = node.value - if isinstance(node, ast.Name): - result.append(node.id) - primary = '.'.join(reversed(result)) - if self._get_root()._is_node_interesting(node) and \ - not self.is_bound(primary): - self.add_unbound(primary) - else: - ast.walk(node, self) - - def _get_root(self): - pass - - def is_bound(self, name, propagated=False): - pass - - def add_unbound(self, name): - pass - - -class _GlobalUnboundNameFinder(_UnboundNameFinder): - - def __init__(self, pymodule, wanted_pyobject): - super(_GlobalUnboundNameFinder, self).__init__(pymodule) - self.unbound = set() - self.names = set() - for name, pyname in pymodule._get_structural_attributes().items(): - if not isinstance(pyname, (pynames.ImportedName, - pynames.ImportedModule)): - self.names.add(name) - wanted_scope = wanted_pyobject.get_scope() - self.start = wanted_scope.get_start() - self.end = wanted_scope.get_end() + 1 - - def _get_root(self): - return self - - def is_bound(self, primary, propagated=False): - name = primary.split('.')[0] - if name in self.names: - return True - return False - - def add_unbound(self, name): - names = name.split('.') - for i in range(len(names)): - self.unbound.add('.'.join(names[:i + 1])) - - def _is_node_interesting(self, node): - return self.start <= node.lineno < self.end - - -class _LocalUnboundNameFinder(_UnboundNameFinder): - - def __init__(self, pyobject, parent): - super(_LocalUnboundNameFinder, self).__init__(pyobject) - self.parent = parent - - def _get_root(self): - return self.parent._get_root() - - def is_bound(self, primary, propagated=False): - name = primary.split('.')[0] - if propagated: - names = self.pyobject.get_scope().get_propagated_names() - else: - names = self.pyobject.get_scope().get_names() - if name in names or self.parent.is_bound(name, propagated=True): - return True - return False - - def add_unbound(self, name): - self.parent.add_unbound(name) - - -class _GlobalImportFinder(object): - - def __init__(self, pymodule): - self.current_folder = None - if pymodule.get_resource(): - self.current_folder = pymodule.get_resource().parent - self.pymodule = pymodule - self.imports = [] - self.pymodule = pymodule - self.lines = self.pymodule.lines - - def visit_import(self, node, end_line): - start_line = node.lineno - import_statement = importinfo.ImportStatement( - importinfo.NormalImport(self._get_names(node.names)), - start_line, end_line, self._get_text(start_line, end_line), - blank_lines=self._count_empty_lines_before(start_line)) - self.imports.append(import_statement) - - def _count_empty_lines_before(self, lineno): - return _count_blank_lines(self.lines.get_line, lineno - 1, 0, -1) - - def _count_empty_lines_after(self, lineno): - return _count_blank_lines(self.lines.get_line, lineno + 1, - self.lines.length()) - - def get_separating_line_count(self): - if not self.imports: - return 0 - return self._count_empty_lines_after(self.imports[-1].end_line - 1) - - def _get_text(self, start_line, end_line): - result = [] - for index in range(start_line, end_line): - result.append(self.lines.get_line(index)) - return '\n'.join(result) - - def visit_from(self, node, end_line): - level = 0 - if node.level: - level = node.level - import_info = importinfo.FromImport( - node.module or '', # see comment at rope.base.ast.walk - level, self._get_names(node.names)) - start_line = node.lineno - self.imports.append(importinfo.ImportStatement( - import_info, node.lineno, end_line, - self._get_text(start_line, end_line), - blank_lines= - self._count_empty_lines_before(start_line))) - - def _get_names(self, alias_names): - result = [] - for alias in alias_names: - result.append((alias.name, alias.asname)) - return result - - def find_import_statements(self): - nodes = self.pymodule.get_ast().body - for index, node in enumerate(nodes): - if isinstance(node, (ast.Import, ast.ImportFrom)): - lines = self.pymodule.logical_lines - end_line = lines.logical_line_in(node.lineno)[1] + 1 - if isinstance(node, ast.Import): - self.visit_import(node, end_line) - if isinstance(node, ast.ImportFrom): - self.visit_from(node, end_line) - return self.imports diff --git a/pythonFiles/rope/refactor/inline.py b/pythonFiles/rope/refactor/inline.py deleted file mode 100644 index 467edefaa16f..000000000000 --- a/pythonFiles/rope/refactor/inline.py +++ /dev/null @@ -1,625 +0,0 @@ -# Known Bugs when inlining a function/method -# The values passed to function are inlined using _inlined_variable. -# This may cause two problems, illustrated in the examples below -# -# def foo(var1): -# var1 = var1*10 -# return var1 -# -# If a call to foo(20) is inlined, the result of inlined function is 20, -# but it should be 200. -# -# def foo(var1): -# var2 = var1*10 -# return var2 -# -# 2- If a call to foo(10+10) is inlined the result of inlined function is 110 -# but it should be 200. - -import re - -import rope.base.exceptions -import rope.refactor.functionutils -from rope.base import (pynames, pyobjects, codeanalyze, - taskhandle, evaluate, worder, utils, libutils) -from rope.base.change import ChangeSet, ChangeContents -from rope.refactor import (occurrences, rename, sourceutils, - importutils, move, change_signature) - - -def unique_prefix(): - n = 0 - while True: - yield "__" + str(n) + "__" - n += 1 - - -def create_inline(project, resource, offset): - """Create a refactoring object for inlining - - Based on `resource` and `offset` it returns an instance of - `InlineMethod`, `InlineVariable` or `InlineParameter`. - - """ - pyname = _get_pyname(project, resource, offset) - message = 'Inline refactoring should be performed on ' \ - 'a method, local variable or parameter.' - if pyname is None: - raise rope.base.exceptions.RefactoringError(message) - if isinstance(pyname, pynames.ImportedName): - pyname = pyname._get_imported_pyname() - if isinstance(pyname, pynames.AssignedName): - return InlineVariable(project, resource, offset) - if isinstance(pyname, pynames.ParameterName): - return InlineParameter(project, resource, offset) - if isinstance(pyname.get_object(), pyobjects.PyFunction): - return InlineMethod(project, resource, offset) - else: - raise rope.base.exceptions.RefactoringError(message) - - -class _Inliner(object): - - def __init__(self, project, resource, offset): - self.project = project - self.pyname = _get_pyname(project, resource, offset) - range_finder = worder.Worder(resource.read(), True) - self.region = range_finder.get_primary_range(offset) - self.name = range_finder.get_word_at(offset) - self.offset = offset - self.original = resource - - def get_changes(self, *args, **kwds): - pass - - def get_kind(self): - """Return either 'variable', 'method' or 'parameter'""" - - -class InlineMethod(_Inliner): - - def __init__(self, *args, **kwds): - super(InlineMethod, self).__init__(*args, **kwds) - self.pyfunction = self.pyname.get_object() - self.pymodule = self.pyfunction.get_module() - self.resource = self.pyfunction.get_module().get_resource() - self.occurrence_finder = occurrences.create_finder( - self.project, self.name, self.pyname) - self.normal_generator = _DefinitionGenerator(self.project, - self.pyfunction) - self._init_imports() - - def _init_imports(self): - body = sourceutils.get_body(self.pyfunction) - body, imports = move.moving_code_with_imports( - self.project, self.resource, body) - self.imports = imports - self.others_generator = _DefinitionGenerator( - self.project, self.pyfunction, body=body) - - def _get_scope_range(self): - scope = self.pyfunction.get_scope() - lines = self.pymodule.lines - start_line = scope.get_start() - if self.pyfunction.decorators: - decorators = self.pyfunction.decorators - if hasattr(decorators[0], 'lineno'): - start_line = decorators[0].lineno - start_offset = lines.get_line_start(start_line) - end_offset = min(lines.get_line_end(scope.end) + 1, - len(self.pymodule.source_code)) - return (start_offset, end_offset) - - def get_changes(self, remove=True, only_current=False, resources=None, - task_handle=taskhandle.NullTaskHandle()): - """Get the changes this refactoring makes - - If `remove` is `False` the definition will not be removed. If - `only_current` is `True`, the the current occurrence will be - inlined, only. - """ - changes = ChangeSet('Inline method <%s>' % self.name) - if resources is None: - resources = self.project.get_python_files() - if only_current: - resources = [self.original] - if remove: - resources.append(self.resource) - job_set = task_handle.create_jobset('Collecting Changes', - len(resources)) - for file in resources: - job_set.started_job(file.path) - if file == self.resource: - changes.add_change(self._defining_file_changes( - changes, remove=remove, only_current=only_current)) - else: - aim = None - if only_current and self.original == file: - aim = self.offset - handle = _InlineFunctionCallsForModuleHandle( - self.project, file, self.others_generator, aim) - result = move.ModuleSkipRenamer( - self.occurrence_finder, file, handle).get_changed_module() - if result is not None: - result = _add_imports(self.project, result, - file, self.imports) - if remove: - result = _remove_from(self.project, self.pyname, - result, file) - changes.add_change(ChangeContents(file, result)) - job_set.finished_job() - return changes - - def _get_removed_range(self): - scope = self.pyfunction.get_scope() - lines = self.pymodule.lines - start, end = self._get_scope_range() - end_line = scope.get_end() - for i in range(end_line + 1, lines.length()): - if lines.get_line(i).strip() == '': - end_line = i - else: - break - end = min(lines.get_line_end(end_line) + 1, - len(self.pymodule.source_code)) - return (start, end) - - def _defining_file_changes(self, changes, remove, only_current): - start_offset, end_offset = self._get_removed_range() - aim = None - if only_current: - if self.resource == self.original: - aim = self.offset - else: - # we don't want to change any of them - aim = len(self.resource.read()) + 100 - handle = _InlineFunctionCallsForModuleHandle( - self.project, self.resource, - self.normal_generator, aim_offset=aim) - replacement = None - if remove: - replacement = self._get_method_replacement() - result = move.ModuleSkipRenamer( - self.occurrence_finder, self.resource, handle, start_offset, - end_offset, replacement).get_changed_module() - return ChangeContents(self.resource, result) - - def _get_method_replacement(self): - if self._is_the_last_method_of_a_class(): - indents = sourceutils.get_indents( - self.pymodule.lines, self.pyfunction.get_scope().get_start()) - return ' ' * indents + 'pass\n' - return '' - - def _is_the_last_method_of_a_class(self): - pyclass = self.pyfunction.parent - if not isinstance(pyclass, pyobjects.PyClass): - return False - class_start, class_end = sourceutils.get_body_region(pyclass) - source = self.pymodule.source_code - func_start, func_end = self._get_scope_range() - if source[class_start:func_start].strip() == '' and \ - source[func_end:class_end].strip() == '': - return True - return False - - def get_kind(self): - return 'method' - - -class InlineVariable(_Inliner): - - def __init__(self, *args, **kwds): - super(InlineVariable, self).__init__(*args, **kwds) - self.pymodule = self.pyname.get_definition_location()[0] - self.resource = self.pymodule.get_resource() - self._check_exceptional_conditions() - self._init_imports() - - def _check_exceptional_conditions(self): - if len(self.pyname.assignments) != 1: - raise rope.base.exceptions.RefactoringError( - 'Local variable should be assigned once for inlining.') - - def get_changes(self, remove=True, only_current=False, resources=None, - docs=False, task_handle=taskhandle.NullTaskHandle()): - if resources is None: - if rename._is_local(self.pyname): - resources = [self.resource] - else: - resources = self.project.get_python_files() - if only_current: - resources = [self.original] - if remove and self.original != self.resource: - resources.append(self.resource) - changes = ChangeSet('Inline variable <%s>' % self.name) - jobset = task_handle.create_jobset('Calculating changes', - len(resources)) - - for resource in resources: - jobset.started_job(resource.path) - if resource == self.resource: - source = self._change_main_module(remove, only_current, docs) - changes.add_change(ChangeContents(self.resource, source)) - else: - result = self._change_module(resource, remove, only_current) - if result is not None: - result = _add_imports(self.project, result, - resource, self.imports) - changes.add_change(ChangeContents(resource, result)) - jobset.finished_job() - return changes - - def _change_main_module(self, remove, only_current, docs): - region = None - if only_current and self.original == self.resource: - region = self.region - return _inline_variable(self.project, self.pymodule, self.pyname, - self.name, remove=remove, region=region, - docs=docs) - - def _init_imports(self): - vardef = _getvardef(self.pymodule, self.pyname) - self.imported, self.imports = move.moving_code_with_imports( - self.project, self.resource, vardef) - - def _change_module(self, resource, remove, only_current): - filters = [occurrences.NoImportsFilter(), - occurrences.PyNameFilter(self.pyname)] - if only_current and resource == self.original: - def check_aim(occurrence): - start, end = occurrence.get_primary_range() - if self.offset < start or end < self.offset: - return False - filters.insert(0, check_aim) - finder = occurrences.Finder(self.project, self.name, filters=filters) - changed = rename.rename_in_module( - finder, self.imported, resource=resource, replace_primary=True) - if changed and remove: - changed = _remove_from(self.project, self.pyname, - changed, resource) - return changed - - def get_kind(self): - return 'variable' - - -class InlineParameter(_Inliner): - - def __init__(self, *args, **kwds): - super(InlineParameter, self).__init__(*args, **kwds) - resource, offset = self._function_location() - index = self.pyname.index - self.changers = [change_signature.ArgumentDefaultInliner(index)] - self.signature = change_signature.ChangeSignature(self.project, - resource, offset) - - def _function_location(self): - pymodule, lineno = self.pyname.get_definition_location() - resource = pymodule.get_resource() - start = pymodule.lines.get_line_start(lineno) - word_finder = worder.Worder(pymodule.source_code) - offset = word_finder.find_function_offset(start) - return resource, offset - - def get_changes(self, **kwds): - """Get the changes needed by this refactoring - - See `rope.refactor.change_signature.ChangeSignature.get_changes()` - for arguments. - """ - return self.signature.get_changes(self.changers, **kwds) - - def get_kind(self): - return 'parameter' - - -def _join_lines(lines): - definition_lines = [] - for unchanged_line in lines: - line = unchanged_line.strip() - if line.endswith('\\'): - line = line[:-1].strip() - definition_lines.append(line) - joined = ' '.join(definition_lines) - return joined - - -class _DefinitionGenerator(object): - unique_prefix = unique_prefix() - - def __init__(self, project, pyfunction, body=None): - self.project = project - self.pyfunction = pyfunction - self.pymodule = pyfunction.get_module() - self.resource = self.pymodule.get_resource() - self.definition_info = self._get_definition_info() - self.definition_params = self._get_definition_params() - self._calculated_definitions = {} - if body is not None: - self.body = body - else: - self.body = sourceutils.get_body(self.pyfunction) - - def _get_definition_info(self): - return rope.refactor.functionutils.DefinitionInfo.read(self.pyfunction) - - def _get_definition_params(self): - definition_info = self.definition_info - paramdict = dict([pair for pair in definition_info.args_with_defaults]) - if definition_info.args_arg is not None or \ - definition_info.keywords_arg is not None: - raise rope.base.exceptions.RefactoringError( - 'Cannot inline functions with list and keyword arguements.') - if self.pyfunction.get_kind() == 'classmethod': - paramdict[definition_info.args_with_defaults[0][0]] = \ - self.pyfunction.parent.get_name() - return paramdict - - def get_function_name(self): - return self.pyfunction.get_name() - - def get_definition(self, primary, pyname, call, host_vars=[], - returns=False): - # caching already calculated definitions - return self._calculate_definition(primary, pyname, call, - host_vars, returns) - - def _calculate_header(self, primary, pyname, call): - # A header is created which initializes parameters - # to the values passed to the function. - call_info = rope.refactor.functionutils.CallInfo.read( - primary, pyname, self.definition_info, call) - paramdict = self.definition_params - mapping = rope.refactor.functionutils.ArgumentMapping( - self.definition_info, call_info) - for param_name, value in mapping.param_dict.items(): - paramdict[param_name] = value - header = '' - to_be_inlined = [] - for name, value in paramdict.items(): - if name != value and value is not None: - header += name + ' = ' + value.replace('\n', ' ') + '\n' - to_be_inlined.append(name) - return header, to_be_inlined - - def _calculate_definition(self, primary, pyname, call, host_vars, returns): - - header, to_be_inlined = self._calculate_header(primary, pyname, call) - - source = header + self.body - mod = libutils.get_string_module(self.project, source) - name_dict = mod.get_scope().get_names() - all_names = [x for x in name_dict if - not isinstance(name_dict[x], - rope.base.builtins.BuiltinName)] - - # If there is a name conflict, all variable names - # inside the inlined function are renamed - if len(set(all_names).intersection(set(host_vars))) > 0: - - prefix = next(_DefinitionGenerator.unique_prefix) - guest = libutils.get_string_module(self.project, source, - self.resource) - - to_be_inlined = [prefix + item for item in to_be_inlined] - for item in all_names: - pyname = guest[item] - occurrence_finder = occurrences.create_finder(self.project, - item, pyname) - source = rename.rename_in_module(occurrence_finder, - prefix + item, pymodule=guest) - guest = libutils.get_string_module( - self.project, source, self.resource) - - #parameters not reassigned inside the functions are now inlined. - for name in to_be_inlined: - pymodule = libutils.get_string_module( - self.project, source, self.resource) - pyname = pymodule[name] - source = _inline_variable(self.project, pymodule, pyname, name) - - return self._replace_returns_with(source, returns) - - def _replace_returns_with(self, source, returns): - result = [] - returned = None - last_changed = 0 - for match in _DefinitionGenerator._get_return_pattern().finditer( - source): - for key, value in match.groupdict().items(): - if value and key == 'return': - result.append(source[last_changed:match.start('return')]) - if returns: - self._check_nothing_after_return(source, - match.end('return')) - beg_idx = match.end('return') - returned = _join_lines( - source[beg_idx:len(source)].splitlines()) - last_changed = len(source) - else: - current = match.end('return') - while current < len(source) and \ - source[current] in ' \t': - current += 1 - last_changed = current - if current == len(source) or source[current] == '\n': - result.append('pass') - result.append(source[last_changed:]) - return ''.join(result), returned - - def _check_nothing_after_return(self, source, offset): - lines = codeanalyze.SourceLinesAdapter(source) - lineno = lines.get_line_number(offset) - logical_lines = codeanalyze.LogicalLineFinder(lines) - lineno = logical_lines.logical_line_in(lineno)[1] - if source[lines.get_line_end(lineno):len(source)].strip() != '': - raise rope.base.exceptions.RefactoringError( - 'Cannot inline functions with statements ' + - 'after return statement.') - - @classmethod - def _get_return_pattern(cls): - if not hasattr(cls, '_return_pattern'): - def named_pattern(name, list_): - return "(?P<%s>" % name + "|".join(list_) + ")" - comment_pattern = named_pattern('comment', [r'#[^\n]*']) - string_pattern = named_pattern('string', - [codeanalyze.get_string_pattern()]) - return_pattern = r'\b(?Preturn)\b' - cls._return_pattern = re.compile(comment_pattern + "|" + - string_pattern + "|" + - return_pattern) - return cls._return_pattern - - -class _InlineFunctionCallsForModuleHandle(object): - - def __init__(self, project, resource, - definition_generator, aim_offset=None): - """Inlines occurrences - - If `aim` is not `None` only the occurrences that intersect - `aim` offset will be inlined. - - """ - self.project = project - self.generator = definition_generator - self.resource = resource - self.aim = aim_offset - - def occurred_inside_skip(self, change_collector, occurrence): - if not occurrence.is_defined(): - raise rope.base.exceptions.RefactoringError( - 'Cannot inline functions that reference themselves') - - def occurred_outside_skip(self, change_collector, occurrence): - start, end = occurrence.get_primary_range() - # we remove out of date imports later - if occurrence.is_in_import_statement(): - return - # the function is referenced outside an import statement - if not occurrence.is_called(): - raise rope.base.exceptions.RefactoringError( - 'Reference to inlining function other than function call' - ' in ' % (self.resource.path, start)) - if self.aim is not None and (self.aim < start or self.aim > end): - return - end_parens = self._find_end_parens(self.source, end - 1) - lineno = self.lines.get_line_number(start) - start_line, end_line = self.pymodule.logical_lines.\ - logical_line_in(lineno) - line_start = self.lines.get_line_start(start_line) - line_end = self.lines.get_line_end(end_line) - - returns = self.source[line_start:start].strip() != '' or \ - self.source[end_parens:line_end].strip() != '' - indents = sourceutils.get_indents(self.lines, start_line) - primary, pyname = occurrence.get_primary_and_pyname() - - host = self.pymodule - scope = host.scope.get_inner_scope_for_line(lineno) - definition, returned = self.generator.get_definition( - primary, pyname, self.source[start:end_parens], scope.get_names(), - returns=returns) - - end = min(line_end + 1, len(self.source)) - change_collector.add_change( - line_start, end, sourceutils.fix_indentation(definition, indents)) - if returns: - name = returned - if name is None: - name = 'None' - change_collector.add_change( - line_end, end, self.source[line_start:start] + name + - self.source[end_parens:end]) - - def _find_end_parens(self, source, offset): - finder = worder.Worder(source) - return finder.get_word_parens_range(offset)[1] - - @property - @utils.saveit - def pymodule(self): - return self.project.get_pymodule(self.resource) - - @property - @utils.saveit - def source(self): - if self.resource is not None: - return self.resource.read() - else: - return self.pymodule.source_code - - @property - @utils.saveit - def lines(self): - return self.pymodule.lines - - -def _inline_variable(project, pymodule, pyname, name, - remove=True, region=None, docs=False): - definition = _getvardef(pymodule, pyname) - start, end = _assigned_lineno(pymodule, pyname) - - occurrence_finder = occurrences.create_finder(project, name, pyname, - docs=docs) - changed_source = rename.rename_in_module( - occurrence_finder, definition, pymodule=pymodule, - replace_primary=True, writes=False, region=region) - if changed_source is None: - changed_source = pymodule.source_code - if remove: - lines = codeanalyze.SourceLinesAdapter(changed_source) - source = changed_source[:lines.get_line_start(start)] + \ - changed_source[lines.get_line_end(end) + 1:] - else: - source = changed_source - return source - - -def _getvardef(pymodule, pyname): - assignment = pyname.assignments[0] - lines = pymodule.lines - start, end = _assigned_lineno(pymodule, pyname) - definition_with_assignment = _join_lines( - [lines.get_line(n) for n in range(start, end + 1)]) - if assignment.levels: - raise rope.base.exceptions.RefactoringError( - 'Cannot inline tuple assignments.') - definition = definition_with_assignment[definition_with_assignment. - index('=') + 1:].strip() - return definition - - -def _assigned_lineno(pymodule, pyname): - definition_line = pyname.assignments[0].ast_node.lineno - return pymodule.logical_lines.logical_line_in(definition_line) - - -def _add_imports(project, source, resource, imports): - if not imports: - return source - pymodule = libutils.get_string_module(project, source, resource) - module_import = importutils.get_module_imports(project, pymodule) - for import_info in imports: - module_import.add_import(import_info) - source = module_import.get_changed_source() - pymodule = libutils.get_string_module(project, source, resource) - import_tools = importutils.ImportTools(project) - return import_tools.organize_imports(pymodule, unused=False, sort=False) - - -def _get_pyname(project, resource, offset): - pymodule = project.get_pymodule(resource) - pyname = evaluate.eval_location(pymodule, offset) - if isinstance(pyname, pynames.ImportedName): - pyname = pyname._get_imported_pyname() - return pyname - - -def _remove_from(project, pyname, source, resource): - pymodule = libutils.get_string_module(project, source, resource) - module_import = importutils.get_module_imports(project, pymodule) - module_import.remove_pyname(pyname) - return module_import.get_changed_source() diff --git a/pythonFiles/rope/refactor/introduce_factory.py b/pythonFiles/rope/refactor/introduce_factory.py deleted file mode 100644 index 7532e361a808..000000000000 --- a/pythonFiles/rope/refactor/introduce_factory.py +++ /dev/null @@ -1,135 +0,0 @@ -import rope.base.exceptions -import rope.base.pyobjects -from rope.base import libutils -from rope.base import taskhandle, evaluate -from rope.base.change import (ChangeSet, ChangeContents) -from rope.refactor import rename, occurrences, sourceutils, importutils - - -class IntroduceFactory(object): - - def __init__(self, project, resource, offset): - self.project = project - self.offset = offset - - this_pymodule = self.project.get_pymodule(resource) - self.old_pyname = evaluate.eval_location(this_pymodule, offset) - if self.old_pyname is None or \ - not isinstance(self.old_pyname.get_object(), - rope.base.pyobjects.PyClass): - raise rope.base.exceptions.RefactoringError( - 'Introduce factory should be performed on a class.') - self.old_name = self.old_pyname.get_object().get_name() - self.pymodule = self.old_pyname.get_object().get_module() - self.resource = self.pymodule.get_resource() - - def get_changes(self, factory_name, global_factory=False, resources=None, - task_handle=taskhandle.NullTaskHandle()): - """Get the changes this refactoring makes - - `factory_name` indicates the name of the factory function to - be added. If `global_factory` is `True` the factory will be - global otherwise a static method is added to the class. - - `resources` can be a list of `rope.base.resource.File`\s that - this refactoring should be applied on; if `None` all python - files in the project are searched. - - """ - if resources is None: - resources = self.project.get_python_files() - changes = ChangeSet('Introduce factory method <%s>' % factory_name) - job_set = task_handle.create_jobset('Collecting Changes', - len(resources)) - self._change_module(resources, changes, factory_name, - global_factory, job_set) - return changes - - def get_name(self): - """Return the name of the class""" - return self.old_name - - def _change_module(self, resources, changes, - factory_name, global_, job_set): - if global_: - replacement = '__rope_factory_%s_' % factory_name - else: - replacement = self._new_function_name(factory_name, global_) - - for file_ in resources: - job_set.started_job(file_.path) - if file_ == self.resource: - self._change_resource(changes, factory_name, global_) - job_set.finished_job() - continue - changed_code = self._rename_occurrences(file_, replacement, - global_) - if changed_code is not None: - if global_: - new_pymodule = libutils.get_string_module( - self.project, changed_code, self.resource) - modname = libutils.modname(self.resource) - changed_code, imported = importutils.add_import( - self.project, new_pymodule, modname, factory_name) - changed_code = changed_code.replace(replacement, imported) - changes.add_change(ChangeContents(file_, changed_code)) - job_set.finished_job() - - def _change_resource(self, changes, factory_name, global_): - class_scope = self.old_pyname.get_object().get_scope() - source_code = self._rename_occurrences( - self.resource, self._new_function_name(factory_name, - global_), global_) - if source_code is None: - source_code = self.pymodule.source_code - else: - self.pymodule = libutils.get_string_module( - self.project, source_code, resource=self.resource) - lines = self.pymodule.lines - start = self._get_insertion_offset(class_scope, lines) - result = source_code[:start] - result += self._get_factory_method(lines, class_scope, - factory_name, global_) - result += source_code[start:] - changes.add_change(ChangeContents(self.resource, result)) - - def _get_insertion_offset(self, class_scope, lines): - start_line = class_scope.get_end() - if class_scope.get_scopes(): - start_line = class_scope.get_scopes()[-1].get_end() - start = lines.get_line_end(start_line) + 1 - return start - - def _get_factory_method(self, lines, class_scope, - factory_name, global_): - unit_indents = ' ' * sourceutils.get_indent(self.project) - if global_: - if self._get_scope_indents(lines, class_scope) > 0: - raise rope.base.exceptions.RefactoringError( - 'Cannot make global factory method for nested classes.') - return ('\ndef %s(*args, **kwds):\n%sreturn %s(*args, **kwds)\n' % - (factory_name, unit_indents, self.old_name)) - unindented_factory = \ - ('@staticmethod\ndef %s(*args, **kwds):\n' % factory_name + - '%sreturn %s(*args, **kwds)\n' % (unit_indents, self.old_name)) - indents = self._get_scope_indents(lines, class_scope) + \ - sourceutils.get_indent(self.project) - return '\n' + sourceutils.indent_lines(unindented_factory, indents) - - def _get_scope_indents(self, lines, scope): - return sourceutils.get_indents(lines, scope.get_start()) - - def _new_function_name(self, factory_name, global_): - if global_: - return factory_name - else: - return self.old_name + '.' + factory_name - - def _rename_occurrences(self, file_, changed_name, global_factory): - finder = occurrences.create_finder(self.project, self.old_name, - self.old_pyname, only_calls=True) - result = rename.rename_in_module(finder, changed_name, resource=file_, - replace_primary=global_factory) - return result - -IntroduceFactoryRefactoring = IntroduceFactory diff --git a/pythonFiles/rope/refactor/introduce_parameter.py b/pythonFiles/rope/refactor/introduce_parameter.py deleted file mode 100644 index 43d6f755b117..000000000000 --- a/pythonFiles/rope/refactor/introduce_parameter.py +++ /dev/null @@ -1,96 +0,0 @@ -import rope.base.change -from rope.base import exceptions, evaluate, worder, codeanalyze -from rope.refactor import functionutils, sourceutils, occurrences - - -class IntroduceParameter(object): - """Introduce parameter refactoring - - This refactoring adds a new parameter to a function and replaces - references to an expression in it with the new parameter. - - The parameter finding part is different from finding similar - pieces in extract refactorings. In this refactoring parameters - are found based on the object they reference to. For instance - in:: - - class A(object): - var = None - - class B(object): - a = A() - - b = B() - a = b.a - - def f(a): - x = b.a.var + a.var - - using this refactoring on ``a.var`` with ``p`` as the new - parameter name, will result in:: - - def f(p=a.var): - x = p + p - - """ - - def __init__(self, project, resource, offset): - self.project = project - self.resource = resource - self.offset = offset - self.pymodule = self.project.get_pymodule(self.resource) - scope = self.pymodule.get_scope().get_inner_scope_for_offset(offset) - if scope.get_kind() != 'Function': - raise exceptions.RefactoringError( - 'Introduce parameter should be performed inside functions') - self.pyfunction = scope.pyobject - self.name, self.pyname = self._get_name_and_pyname() - if self.pyname is None: - raise exceptions.RefactoringError( - 'Cannot find the definition of <%s>' % self.name) - - def _get_primary(self): - word_finder = worder.Worder(self.resource.read()) - return word_finder.get_primary_at(self.offset) - - def _get_name_and_pyname(self): - return (worder.get_name_at(self.resource, self.offset), - evaluate.eval_location(self.pymodule, self.offset)) - - def get_changes(self, new_parameter): - definition_info = functionutils.DefinitionInfo.read(self.pyfunction) - definition_info.args_with_defaults.append((new_parameter, - self._get_primary())) - collector = codeanalyze.ChangeCollector(self.resource.read()) - header_start, header_end = self._get_header_offsets() - body_start, body_end = sourceutils.get_body_region(self.pyfunction) - collector.add_change(header_start, header_end, - definition_info.to_string()) - self._change_function_occurances(collector, body_start, - body_end, new_parameter) - changes = rope.base.change.ChangeSet('Introduce parameter <%s>' % - new_parameter) - change = rope.base.change.ChangeContents(self.resource, - collector.get_changed()) - changes.add_change(change) - return changes - - def _get_header_offsets(self): - lines = self.pymodule.lines - start_line = self.pyfunction.get_scope().get_start() - end_line = self.pymodule.logical_lines.\ - logical_line_in(start_line)[1] - start = lines.get_line_start(start_line) - end = lines.get_line_end(end_line) - start = self.pymodule.source_code.find('def', start) + 4 - end = self.pymodule.source_code.rfind(':', start, end) - return start, end - - def _change_function_occurances(self, collector, function_start, - function_end, new_name): - finder = occurrences.create_finder(self.project, self.name, - self.pyname) - for occurrence in finder.find_occurrences(resource=self.resource): - start, end = occurrence.get_primary_range() - if function_start <= start < function_end: - collector.add_change(start, end, new_name) diff --git a/pythonFiles/rope/refactor/localtofield.py b/pythonFiles/rope/refactor/localtofield.py deleted file mode 100644 index f276070f71fa..000000000000 --- a/pythonFiles/rope/refactor/localtofield.py +++ /dev/null @@ -1,49 +0,0 @@ -from rope.base import pynames, evaluate, exceptions, worder -from rope.refactor.rename import Rename - - -class LocalToField(object): - - def __init__(self, project, resource, offset): - self.project = project - self.resource = resource - self.offset = offset - - def get_changes(self): - name = worder.get_name_at(self.resource, self.offset) - this_pymodule = self.project.get_pymodule(self.resource) - pyname = evaluate.eval_location(this_pymodule, self.offset) - if not self._is_a_method_local(pyname): - raise exceptions.RefactoringError( - 'Convert local variable to field should be performed on \n' - 'a local variable of a method.') - - pymodule, lineno = pyname.get_definition_location() - function_scope = pymodule.get_scope().get_inner_scope_for_line(lineno) - # Not checking redefinition - #self._check_redefinition(name, function_scope) - - new_name = self._get_field_name(function_scope.pyobject, name) - changes = Rename(self.project, self.resource, self.offset).\ - get_changes(new_name, resources=[self.resource]) - return changes - - def _check_redefinition(self, name, function_scope): - class_scope = function_scope.parent - if name in class_scope.pyobject: - raise exceptions.RefactoringError( - 'The field %s already exists' % name) - - def _get_field_name(self, pyfunction, name): - self_name = pyfunction.get_param_names()[0] - new_name = self_name + '.' + name - return new_name - - def _is_a_method_local(self, pyname): - pymodule, lineno = pyname.get_definition_location() - holding_scope = pymodule.get_scope().get_inner_scope_for_line(lineno) - parent = holding_scope.parent - return isinstance(pyname, pynames.AssignedName) and \ - pyname in holding_scope.get_names().values() and \ - holding_scope.get_kind() == 'Function' and \ - parent is not None and parent.get_kind() == 'Class' diff --git a/pythonFiles/rope/refactor/method_object.py b/pythonFiles/rope/refactor/method_object.py deleted file mode 100644 index 29ce429db611..000000000000 --- a/pythonFiles/rope/refactor/method_object.py +++ /dev/null @@ -1,90 +0,0 @@ -import warnings - -from rope.base import libutils -from rope.base import pyobjects, exceptions, change, evaluate, codeanalyze -from rope.refactor import sourceutils, occurrences, rename - - -class MethodObject(object): - - def __init__(self, project, resource, offset): - self.project = project - this_pymodule = self.project.get_pymodule(resource) - pyname = evaluate.eval_location(this_pymodule, offset) - if pyname is None or not isinstance(pyname.get_object(), - pyobjects.PyFunction): - raise exceptions.RefactoringError( - 'Replace method with method object refactoring should be ' - 'performed on a function.') - self.pyfunction = pyname.get_object() - self.pymodule = self.pyfunction.get_module() - self.resource = self.pymodule.get_resource() - - def get_new_class(self, name): - body = sourceutils.fix_indentation( - self._get_body(), sourceutils.get_indent(self.project) * 2) - return 'class %s(object):\n\n%s%sdef __call__(self):\n%s' % \ - (name, self._get_init(), - ' ' * sourceutils.get_indent(self.project), body) - - def get_changes(self, classname=None, new_class_name=None): - if new_class_name is not None: - warnings.warn( - 'new_class_name parameter is deprecated; use classname', - DeprecationWarning, stacklevel=2) - classname = new_class_name - collector = codeanalyze.ChangeCollector(self.pymodule.source_code) - start, end = sourceutils.get_body_region(self.pyfunction) - indents = sourceutils.get_indents( - self.pymodule.lines, self.pyfunction.get_scope().get_start()) + \ - sourceutils.get_indent(self.project) - new_contents = ' ' * indents + 'return %s(%s)()\n' % \ - (classname, ', '.join(self._get_parameter_names())) - collector.add_change(start, end, new_contents) - insertion = self._get_class_insertion_point() - collector.add_change(insertion, insertion, - '\n\n' + self.get_new_class(classname)) - changes = change.ChangeSet( - 'Replace method with method object refactoring') - changes.add_change(change.ChangeContents(self.resource, - collector.get_changed())) - return changes - - def _get_class_insertion_point(self): - current = self.pyfunction - while current.parent != self.pymodule: - current = current.parent - end = self.pymodule.lines.get_line_end(current.get_scope().get_end()) - return min(end + 1, len(self.pymodule.source_code)) - - def _get_body(self): - body = sourceutils.get_body(self.pyfunction) - for param in self._get_parameter_names(): - body = param + ' = None\n' + body - pymod = libutils.get_string_module( - self.project, body, self.resource) - pyname = pymod[param] - finder = occurrences.create_finder(self.project, param, pyname) - result = rename.rename_in_module(finder, 'self.' + param, - pymodule=pymod) - body = result[result.index('\n') + 1:] - return body - - def _get_init(self): - params = self._get_parameter_names() - indents = ' ' * sourceutils.get_indent(self.project) - if not params: - return '' - header = indents + 'def __init__(self' - body = '' - for arg in params: - new_name = arg - if arg == 'self': - new_name = 'host' - header += ', %s' % new_name - body += indents * 2 + 'self.%s = %s\n' % (arg, new_name) - header += '):' - return '%s\n%s\n' % (header, body) - - def _get_parameter_names(self): - return self.pyfunction.get_param_names() diff --git a/pythonFiles/rope/refactor/move.py b/pythonFiles/rope/refactor/move.py deleted file mode 100644 index ce618277e52c..000000000000 --- a/pythonFiles/rope/refactor/move.py +++ /dev/null @@ -1,784 +0,0 @@ -"""A module containing classes for move refactoring - -`create_move()` is a factory for creating move refactoring objects -based on inputs. - -""" -from rope.base import (pyobjects, codeanalyze, exceptions, pynames, - taskhandle, evaluate, worder, libutils) -from rope.base.change import ChangeSet, ChangeContents, MoveResource -from rope.refactor import importutils, rename, occurrences, sourceutils, \ - functionutils - - -def create_move(project, resource, offset=None): - """A factory for creating Move objects - - Based on `resource` and `offset`, return one of `MoveModule`, - `MoveGlobal` or `MoveMethod` for performing move refactoring. - - """ - if offset is None: - return MoveModule(project, resource) - this_pymodule = project.get_pymodule(resource) - pyname = evaluate.eval_location(this_pymodule, offset) - if pyname is not None: - pyobject = pyname.get_object() - if isinstance(pyobject, pyobjects.PyModule) or \ - isinstance(pyobject, pyobjects.PyPackage): - return MoveModule(project, pyobject.get_resource()) - if isinstance(pyobject, pyobjects.PyFunction) and \ - isinstance(pyobject.parent, pyobjects.PyClass): - return MoveMethod(project, resource, offset) - if isinstance(pyobject, pyobjects.PyDefinedObject) and \ - isinstance(pyobject.parent, pyobjects.PyModule) or \ - isinstance(pyname, pynames.AssignedName): - return MoveGlobal(project, resource, offset) - raise exceptions.RefactoringError( - 'Move only works on global classes/functions/variables, modules and ' - 'methods.') - - -class MoveMethod(object): - """For moving methods - - It makes a new method in the destination class and changes - the body of the old method to call the new method. You can - inline the old method to change all of its occurrences. - - """ - - def __init__(self, project, resource, offset): - self.project = project - this_pymodule = self.project.get_pymodule(resource) - pyname = evaluate.eval_location(this_pymodule, offset) - self.method_name = worder.get_name_at(resource, offset) - self.pyfunction = pyname.get_object() - if self.pyfunction.get_kind() != 'method': - raise exceptions.RefactoringError('Only normal methods' - ' can be moved.') - - def get_changes(self, dest_attr, new_name=None, resources=None, - task_handle=taskhandle.NullTaskHandle()): - """Return the changes needed for this refactoring - - Parameters: - - - `dest_attr`: the name of the destination attribute - - `new_name`: the name of the new method; if `None` uses - the old name - - `resources` can be a list of `rope.base.resources.File`\s to - apply this refactoring on. If `None`, the restructuring - will be applied to all python files. - - """ - changes = ChangeSet('Moving method <%s>' % self.method_name) - if resources is None: - resources = self.project.get_python_files() - if new_name is None: - new_name = self.get_method_name() - resource1, start1, end1, new_content1 = \ - self._get_changes_made_by_old_class(dest_attr, new_name) - collector1 = codeanalyze.ChangeCollector(resource1.read()) - collector1.add_change(start1, end1, new_content1) - - resource2, start2, end2, new_content2 = \ - self._get_changes_made_by_new_class(dest_attr, new_name) - if resource1 == resource2: - collector1.add_change(start2, end2, new_content2) - else: - collector2 = codeanalyze.ChangeCollector(resource2.read()) - collector2.add_change(start2, end2, new_content2) - result = collector2.get_changed() - import_tools = importutils.ImportTools(self.project) - new_imports = self._get_used_imports(import_tools) - if new_imports: - goal_pymodule = libutils.get_string_module( - self.project, result, resource2) - result = _add_imports_to_module( - import_tools, goal_pymodule, new_imports) - if resource2 in resources: - changes.add_change(ChangeContents(resource2, result)) - - if resource1 in resources: - changes.add_change(ChangeContents(resource1, - collector1.get_changed())) - return changes - - def get_method_name(self): - return self.method_name - - def _get_used_imports(self, import_tools): - return importutils.get_imports(self.project, self.pyfunction) - - def _get_changes_made_by_old_class(self, dest_attr, new_name): - pymodule = self.pyfunction.get_module() - indents = self._get_scope_indents(self.pyfunction) - body = 'return self.%s.%s(%s)\n' % ( - dest_attr, new_name, self._get_passed_arguments_string()) - region = sourceutils.get_body_region(self.pyfunction) - return (pymodule.get_resource(), region[0], region[1], - sourceutils.fix_indentation(body, indents)) - - def _get_scope_indents(self, pyobject): - pymodule = pyobject.get_module() - return sourceutils.get_indents( - pymodule.lines, pyobject.get_scope().get_start()) + \ - sourceutils.get_indent(self.project) - - def _get_changes_made_by_new_class(self, dest_attr, new_name): - old_pyclass = self.pyfunction.parent - if dest_attr not in old_pyclass: - raise exceptions.RefactoringError( - 'Destination attribute <%s> not found' % dest_attr) - pyclass = old_pyclass[dest_attr].get_object().get_type() - if not isinstance(pyclass, pyobjects.PyClass): - raise exceptions.RefactoringError( - 'Unknown class type for attribute <%s>' % dest_attr) - pymodule = pyclass.get_module() - resource = pyclass.get_module().get_resource() - start, end = sourceutils.get_body_region(pyclass) - pre_blanks = '\n' - if pymodule.source_code[start:end].strip() != 'pass': - pre_blanks = '\n\n' - start = end - indents = self._get_scope_indents(pyclass) - body = pre_blanks + sourceutils.fix_indentation( - self.get_new_method(new_name), indents) - return resource, start, end, body - - def get_new_method(self, name): - return '%s\n%s' % ( - self._get_new_header(name), - sourceutils.fix_indentation(self._get_body(), - sourceutils.get_indent(self.project))) - - def _get_unchanged_body(self): - return sourceutils.get_body(self.pyfunction) - - def _get_body(self, host='host'): - self_name = self._get_self_name() - body = self_name + ' = None\n' + self._get_unchanged_body() - pymodule = libutils.get_string_module(self.project, body) - finder = occurrences.create_finder( - self.project, self_name, pymodule[self_name]) - result = rename.rename_in_module(finder, host, pymodule=pymodule) - if result is None: - result = body - return result[result.index('\n') + 1:] - - def _get_self_name(self): - return self.pyfunction.get_param_names()[0] - - def _get_new_header(self, name): - header = 'def %s(self' % name - if self._is_host_used(): - header += ', host' - definition_info = functionutils.DefinitionInfo.read(self.pyfunction) - others = definition_info.arguments_to_string(1) - if others: - header += ', ' + others - return header + '):' - - def _get_passed_arguments_string(self): - result = '' - if self._is_host_used(): - result = 'self' - definition_info = functionutils.DefinitionInfo.read(self.pyfunction) - others = definition_info.arguments_to_string(1) - if others: - if result: - result += ', ' - result += others - return result - - def _is_host_used(self): - return self._get_body('__old_self') != self._get_unchanged_body() - - -class MoveGlobal(object): - """For moving global function and classes""" - - def __init__(self, project, resource, offset): - self.project = project - this_pymodule = self.project.get_pymodule(resource) - self.old_pyname = evaluate.eval_location(this_pymodule, offset) - if self.old_pyname is None: - raise exceptions.RefactoringError( - 'Move refactoring should be performed on a ' - 'class/function/variable.') - if self._is_variable(self.old_pyname): - self.old_name = worder.get_name_at(resource, offset) - pymodule = this_pymodule - else: - self.old_name = self.old_pyname.get_object().get_name() - pymodule = self.old_pyname.get_object().get_module() - self._check_exceptional_conditions() - self.source = pymodule.get_resource() - self.tools = _MoveTools(self.project, self.source, - self.old_pyname, self.old_name) - self.import_tools = self.tools.import_tools - - def _import_filter(self, stmt): - module_name = libutils.modname(self.source) - - if isinstance(stmt.import_info, importutils.NormalImport): - # Affect any statement that imports the source module - return any(module_name == name - for name, alias in stmt.import_info.names_and_aliases) - elif isinstance(stmt.import_info, importutils.FromImport): - # Affect statements importing from the source package - if '.' in module_name: - package_name, basename = module_name.rsplit('.', 1) - if (stmt.import_info.module_name == package_name and - any(basename == name - for name, alias in stmt.import_info.names_and_aliases)): - return True - return stmt.import_info.module_name == module_name - return False - - def _check_exceptional_conditions(self): - if self._is_variable(self.old_pyname): - pymodule = self.old_pyname.get_definition_location()[0] - try: - pymodule.get_scope().get_name(self.old_name) - except exceptions.NameNotFoundError: - self._raise_refactoring_error() - elif not (isinstance(self.old_pyname.get_object(), - pyobjects.PyDefinedObject) and - self._is_global(self.old_pyname.get_object())): - self._raise_refactoring_error() - - def _raise_refactoring_error(self): - raise exceptions.RefactoringError( - 'Move refactoring should be performed on a global class, function ' - 'or variable.') - - def _is_global(self, pyobject): - return pyobject.get_scope().parent == pyobject.get_module().get_scope() - - def _is_variable(self, pyname): - return isinstance(pyname, pynames.AssignedName) - - def get_changes(self, dest, resources=None, - task_handle=taskhandle.NullTaskHandle()): - if resources is None: - resources = self.project.get_python_files() - if dest is None or not dest.exists(): - raise exceptions.RefactoringError( - 'Move destination does not exist.') - if dest.is_folder() and dest.has_child('__init__.py'): - dest = dest.get_child('__init__.py') - if dest.is_folder(): - raise exceptions.RefactoringError( - 'Move destination for non-modules should not be folders.') - if self.source == dest: - raise exceptions.RefactoringError( - 'Moving global elements to the same module.') - return self._calculate_changes(dest, resources, task_handle) - - def _calculate_changes(self, dest, resources, task_handle): - changes = ChangeSet('Moving global <%s>' % self.old_name) - job_set = task_handle.create_jobset('Collecting Changes', - len(resources)) - for file_ in resources: - job_set.started_job(file_.path) - if file_ == self.source: - changes.add_change(self._source_module_changes(dest)) - elif file_ == dest: - changes.add_change(self._dest_module_changes(dest)) - elif self.tools.occurs_in_module(resource=file_): - pymodule = self.project.get_pymodule(file_) - # Changing occurrences - placeholder = '__rope_renaming_%s_' % self.old_name - source = self.tools.rename_in_module(placeholder, - resource=file_) - should_import = source is not None - # Removing out of date imports - pymodule = self.tools.new_pymodule(pymodule, source) - source = self.import_tools.organize_imports( - pymodule, sort=False, import_filter=self._import_filter) - # Adding new import - if should_import: - pymodule = self.tools.new_pymodule(pymodule, source) - source, imported = importutils.add_import( - self.project, pymodule, self._new_modname(dest), - self.old_name) - source = source.replace(placeholder, imported) - source = self.tools.new_source(pymodule, source) - if source != file_.read(): - changes.add_change(ChangeContents(file_, source)) - job_set.finished_job() - return changes - - def _source_module_changes(self, dest): - placeholder = '__rope_moving_%s_' % self.old_name - handle = _ChangeMoveOccurrencesHandle(placeholder) - occurrence_finder = occurrences.create_finder( - self.project, self.old_name, self.old_pyname) - start, end = self._get_moving_region() - renamer = ModuleSkipRenamer(occurrence_finder, self.source, - handle, start, end) - source = renamer.get_changed_module() - pymodule = libutils.get_string_module(self.project, source, self.source) - source = self.import_tools.organize_imports(pymodule, sort=False) - if handle.occurred: - pymodule = libutils.get_string_module( - self.project, source, self.source) - # Adding new import - source, imported = importutils.add_import( - self.project, pymodule, self._new_modname(dest), self.old_name) - source = source.replace(placeholder, imported) - return ChangeContents(self.source, source) - - def _new_modname(self, dest): - return libutils.modname(dest) - - def _dest_module_changes(self, dest): - # Changing occurrences - pymodule = self.project.get_pymodule(dest) - source = self.tools.rename_in_module(self.old_name, pymodule) - pymodule = self.tools.new_pymodule(pymodule, source) - - moving, imports = self._get_moving_element_with_imports() - pymodule, has_changed = self._add_imports2(pymodule, imports) - - module_with_imports = self.import_tools.module_imports(pymodule) - source = pymodule.source_code - lineno = 0 - if module_with_imports.imports: - lineno = module_with_imports.imports[-1].end_line - 1 - else: - while lineno < pymodule.lines.length() and \ - pymodule.lines.get_line(lineno + 1).\ - lstrip().startswith('#'): - lineno += 1 - if lineno > 0: - cut = pymodule.lines.get_line_end(lineno) + 1 - result = source[:cut] + '\n\n' + moving + source[cut:] - else: - result = moving + source - - # Organizing imports - source = result - pymodule = libutils.get_string_module(self.project, source, dest) - source = self.import_tools.organize_imports(pymodule, sort=False, - unused=False) - # Remove unused imports of the old module - pymodule = libutils.get_string_module(self.project, source, dest) - source = self.import_tools.organize_imports( - pymodule, sort=False, selfs=False, unused=True, - import_filter=self._import_filter) - return ChangeContents(dest, source) - - def _get_moving_element_with_imports(self): - return moving_code_with_imports( - self.project, self.source, self._get_moving_element()) - - def _get_module_with_imports(self, source_code, resource): - pymodule = libutils.get_string_module( - self.project, source_code, resource) - return self.import_tools.module_imports(pymodule) - - def _get_moving_element(self): - start, end = self._get_moving_region() - moving = self.source.read()[start:end] - return moving.rstrip() + '\n' - - def _get_moving_region(self): - pymodule = self.project.get_pymodule(self.source) - lines = pymodule.lines - if self._is_variable(self.old_pyname): - logical_lines = pymodule.logical_lines - lineno = logical_lines.logical_line_in( - self.old_pyname.get_definition_location()[1])[0] - start = lines.get_line_start(lineno) - end_line = logical_lines.logical_line_in(lineno)[1] - else: - scope = self.old_pyname.get_object().get_scope() - start = lines.get_line_start(scope.get_start()) - end_line = scope.get_end() - - # Include comment lines before the definition - start_line = lines.get_line_number(start) - while start_line > 1 and lines.get_line(start_line - 1).startswith('#'): - start_line -= 1 - start = lines.get_line_start(start_line) - - while end_line < lines.length() and \ - lines.get_line(end_line + 1).strip() == '': - end_line += 1 - end = min(lines.get_line_end(end_line) + 1, len(pymodule.source_code)) - return start, end - - def _add_imports2(self, pymodule, new_imports): - source = self.tools.add_imports(pymodule, new_imports) - if source is None: - return pymodule, False - else: - resource = pymodule.get_resource() - pymodule = libutils.get_string_module( - self.project, source, resource) - return pymodule, True - - -class MoveModule(object): - """For moving modules and packages""" - - def __init__(self, project, resource): - self.project = project - if not resource.is_folder() and resource.name == '__init__.py': - resource = resource.parent - if resource.is_folder() and not resource.has_child('__init__.py'): - raise exceptions.RefactoringError( - 'Cannot move non-package folder.') - dummy_pymodule = libutils.get_string_module(self.project, '') - self.old_pyname = pynames.ImportedModule(dummy_pymodule, - resource=resource) - self.source = self.old_pyname.get_object().get_resource() - if self.source.is_folder(): - self.old_name = self.source.name - else: - self.old_name = self.source.name[:-3] - self.tools = _MoveTools(self.project, self.source, - self.old_pyname, self.old_name) - self.import_tools = self.tools.import_tools - - def get_changes(self, dest, resources=None, - task_handle=taskhandle.NullTaskHandle()): - if resources is None: - resources = self.project.get_python_files() - if dest is None or not dest.is_folder(): - raise exceptions.RefactoringError( - 'Move destination for modules should be packages.') - return self._calculate_changes(dest, resources, task_handle) - - def _calculate_changes(self, dest, resources, task_handle): - changes = ChangeSet('Moving module <%s>' % self.old_name) - job_set = task_handle.create_jobset('Collecting changes', - len(resources)) - for module in resources: - job_set.started_job(module.path) - if module == self.source: - self._change_moving_module(changes, dest) - else: - source = self._change_occurrences_in_module(dest, - resource=module) - if source is not None: - changes.add_change(ChangeContents(module, source)) - job_set.finished_job() - if self.project == self.source.project: - changes.add_change(MoveResource(self.source, dest.path)) - return changes - - def _new_modname(self, dest): - destname = libutils.modname(dest) - if destname: - return destname + '.' + self.old_name - return self.old_name - - def _new_import(self, dest): - return importutils.NormalImport([(self._new_modname(dest), None)]) - - def _change_moving_module(self, changes, dest): - if not self.source.is_folder(): - pymodule = self.project.get_pymodule(self.source) - source = self.import_tools.relatives_to_absolutes(pymodule) - pymodule = self.tools.new_pymodule(pymodule, source) - source = self._change_occurrences_in_module(dest, pymodule) - source = self.tools.new_source(pymodule, source) - if source != self.source.read(): - changes.add_change(ChangeContents(self.source, source)) - - def _change_occurrences_in_module(self, dest, pymodule=None, - resource=None): - if not self.tools.occurs_in_module(pymodule=pymodule, - resource=resource): - return - if pymodule is None: - pymodule = self.project.get_pymodule(resource) - new_name = self._new_modname(dest) - module_imports = importutils.get_module_imports(self.project, pymodule) - changed = False - source = None - if libutils.modname(dest): - changed = self._change_import_statements(dest, new_name, - module_imports) - if changed: - source = module_imports.get_changed_source() - source = self.tools.new_source(pymodule, source) - pymodule = self.tools.new_pymodule(pymodule, source) - - new_import = self._new_import(dest) - source = self.tools.rename_in_module( - new_name, imports=True, pymodule=pymodule, - resource=resource if not changed else None) - should_import = self.tools.occurs_in_module( - pymodule=pymodule, resource=resource, imports=False) - pymodule = self.tools.new_pymodule(pymodule, source) - source = self.tools.remove_old_imports(pymodule) - if should_import: - pymodule = self.tools.new_pymodule(pymodule, source) - source = self.tools.add_imports(pymodule, [new_import]) - source = self.tools.new_source(pymodule, source) - if source is not None and source != pymodule.resource.read(): - return source - return None - - def _change_import_statements(self, dest, new_name, module_imports): - moving_module = self.source - parent_module = moving_module.parent - - changed = False - for import_stmt in module_imports.imports: - if not any(name_and_alias[0] == self.old_name - for name_and_alias in - import_stmt.import_info.names_and_aliases) and \ - not any(name_and_alias[0] == libutils.modname(self.source) - for name_and_alias in - import_stmt.import_info.names_and_aliases): - continue - - # Case 1: Look for normal imports of the moving module. - if isinstance(import_stmt.import_info, importutils.NormalImport): - continue - - # Case 2: The moving module is from-imported. - changed = self._handle_moving_in_from_import_stmt( - dest, import_stmt, module_imports, parent_module) or changed - - # Case 3: Names are imported from the moving module. - context = importutils.importinfo.ImportContext(self.project, None) - if not import_stmt.import_info.is_empty() and \ - import_stmt.import_info.get_imported_resource(context) == \ - moving_module: - import_stmt.import_info = importutils.FromImport( - new_name, import_stmt.import_info.level, - import_stmt.import_info.names_and_aliases) - changed = True - - return changed - - def _handle_moving_in_from_import_stmt(self, dest, import_stmt, - module_imports, parent_module): - changed = False - context = importutils.importinfo.ImportContext(self.project, None) - if import_stmt.import_info.get_imported_resource(context) == \ - parent_module: - imports = import_stmt.import_info.names_and_aliases - new_imports = [] - for name, alias in imports: - # The moving module was imported. - if name == self.old_name: - changed = True - new_import = importutils.FromImport( - libutils.modname(dest), 0, - [(self.old_name, alias)]) - module_imports.add_import(new_import) - else: - new_imports.append((name, alias)) - - # Update the imports if the imported names were changed. - if new_imports != imports: - changed = True - if new_imports: - import_stmt.import_info = importutils.FromImport( - import_stmt.import_info.module_name, - import_stmt.import_info.level, - new_imports) - else: - import_stmt.empty_import() - return changed - - -class _ChangeMoveOccurrencesHandle(object): - - def __init__(self, new_name): - self.new_name = new_name - self.occurred = False - - def occurred_inside_skip(self, change_collector, occurrence): - pass - - def occurred_outside_skip(self, change_collector, occurrence): - start, end = occurrence.get_primary_range() - change_collector.add_change(start, end, self.new_name) - self.occurred = True - - -class _MoveTools(object): - - def __init__(self, project, source, pyname, old_name): - self.project = project - self.source = source - self.old_pyname = pyname - self.old_name = old_name - self.import_tools = importutils.ImportTools(self.project) - - def remove_old_imports(self, pymodule): - old_source = pymodule.source_code - module_with_imports = self.import_tools.module_imports(pymodule) - - class CanSelect(object): - changed = False - old_name = self.old_name - old_pyname = self.old_pyname - - def __call__(self, name): - try: - if name == self.old_name and \ - pymodule[name].get_object() == \ - self.old_pyname.get_object(): - self.changed = True - return False - except exceptions.AttributeNotFoundError: - pass - return True - can_select = CanSelect() - module_with_imports.filter_names(can_select) - new_source = module_with_imports.get_changed_source() - if old_source != new_source: - return new_source - - def rename_in_module(self, new_name, pymodule=None, - imports=False, resource=None): - occurrence_finder = self._create_finder(imports) - source = rename.rename_in_module( - occurrence_finder, new_name, replace_primary=True, - pymodule=pymodule, resource=resource) - return source - - def occurs_in_module(self, pymodule=None, resource=None, imports=True): - finder = self._create_finder(imports) - for occurrence in finder.find_occurrences(pymodule=pymodule, - resource=resource): - return True - return False - - def _create_finder(self, imports): - return occurrences.create_finder(self.project, self.old_name, - self.old_pyname, imports=imports, - keywords=False) - - def new_pymodule(self, pymodule, source): - if source is not None: - return libutils.get_string_module( - self.project, source, pymodule.get_resource()) - return pymodule - - def new_source(self, pymodule, source): - if source is None: - return pymodule.source_code - return source - - def add_imports(self, pymodule, new_imports): - return _add_imports_to_module(self.import_tools, pymodule, new_imports) - - -def _add_imports_to_module(import_tools, pymodule, new_imports): - module_with_imports = import_tools.module_imports(pymodule) - for new_import in new_imports: - module_with_imports.add_import(new_import) - return module_with_imports.get_changed_source() - - -def moving_code_with_imports(project, resource, source): - import_tools = importutils.ImportTools(project) - pymodule = libutils.get_string_module(project, source, resource) - - # Strip comment prefix, if any. These need to stay before the moving - # section, but imports would be added between them. - lines = codeanalyze.SourceLinesAdapter(source) - start = 1 - while start < lines.length() and lines.get_line(start).startswith('#'): - start += 1 - moving_prefix = source[:lines.get_line_start(start)] - pymodule = libutils.get_string_module( - project, source[lines.get_line_start(start):], resource) - - origin = project.get_pymodule(resource) - - imports = [] - for stmt in import_tools.module_imports(origin).imports: - imports.append(stmt.import_info) - - back_names = [] - for name in origin: - if name not in pymodule: - back_names.append(name) - imports.append(import_tools.get_from_import(resource, back_names)) - - source = _add_imports_to_module(import_tools, pymodule, imports) - pymodule = libutils.get_string_module(project, source, resource) - - source = import_tools.relatives_to_absolutes(pymodule) - pymodule = libutils.get_string_module(project, source, resource) - source = import_tools.organize_imports(pymodule, selfs=False) - pymodule = libutils.get_string_module(project, source, resource) - - # extracting imports after changes - module_imports = import_tools.module_imports(pymodule) - imports = [import_stmt.import_info - for import_stmt in module_imports.imports] - start = 1 - if module_imports.imports: - start = module_imports.imports[-1].end_line - lines = codeanalyze.SourceLinesAdapter(source) - while start < lines.length() and not lines.get_line(start).strip(): - start += 1 - - # Reinsert the prefix which was removed at the beginning - moving = moving_prefix + source[lines.get_line_start(start):] - return moving, imports - - -class ModuleSkipRenamerHandle(object): - - def occurred_outside_skip(self, change_collector, occurrence): - pass - - def occurred_inside_skip(self, change_collector, occurrence): - pass - - -class ModuleSkipRenamer(object): - """Rename occurrences in a module - - This class can be used when you want to treat a region in a file - separately from other parts when renaming. - - """ - - def __init__(self, occurrence_finder, resource, handle=None, - skip_start=0, skip_end=0, replacement=''): - """Constructor - - if replacement is `None` the region is not changed. Otherwise - it is replaced with `replacement`. - - """ - self.occurrence_finder = occurrence_finder - self.resource = resource - self.skip_start = skip_start - self.skip_end = skip_end - self.replacement = replacement - self.handle = handle - if self.handle is None: - self.handle = ModuleSkipRenamerHandle() - - def get_changed_module(self): - source = self.resource.read() - change_collector = codeanalyze.ChangeCollector(source) - if self.replacement is not None: - change_collector.add_change(self.skip_start, self.skip_end, - self.replacement) - for occurrence in self.occurrence_finder.find_occurrences( - self.resource): - start, end = occurrence.get_primary_range() - if self.skip_start <= start < self.skip_end: - self.handle.occurred_inside_skip(change_collector, occurrence) - else: - self.handle.occurred_outside_skip(change_collector, occurrence) - result = change_collector.get_changed() - if result is not None and result != source: - return result diff --git a/pythonFiles/rope/refactor/multiproject.py b/pythonFiles/rope/refactor/multiproject.py deleted file mode 100644 index ac243bdafcea..000000000000 --- a/pythonFiles/rope/refactor/multiproject.py +++ /dev/null @@ -1,78 +0,0 @@ -"""This module can be used for performing cross-project refactorings - -See the "cross-project refactorings" section of ``docs/library.rst`` -file. - -""" - -from rope.base import resources, libutils - - -class MultiProjectRefactoring(object): - - def __init__(self, refactoring, projects, addpath=True): - """Create a multiproject proxy for the main refactoring - - `projects` are other project. - - """ - self.refactoring = refactoring - self.projects = projects - self.addpath = addpath - - def __call__(self, project, *args, **kwds): - """Create the refactoring""" - return _MultiRefactoring(self.refactoring, self.projects, - self.addpath, project, *args, **kwds) - - -class _MultiRefactoring(object): - - def __init__(self, refactoring, other_projects, addpath, - project, *args, **kwds): - self.refactoring = refactoring - self.projects = [project] + other_projects - for other_project in other_projects: - for folder in self.project.get_source_folders(): - other_project.get_prefs().add('python_path', folder.real_path) - self.refactorings = [] - for other in self.projects: - args, kwds = self._resources_for_args(other, args, kwds) - self.refactorings.append( - self.refactoring(other, *args, **kwds)) - - def get_all_changes(self, *args, **kwds): - """Get a project to changes dict""" - result = [] - for project, refactoring in zip(self.projects, self.refactorings): - args, kwds = self._resources_for_args(project, args, kwds) - result.append((project, refactoring.get_changes(*args, **kwds))) - return result - - def __getattr__(self, name): - return getattr(self.main_refactoring, name) - - def _resources_for_args(self, project, args, kwds): - newargs = [self._change_project_resource(project, arg) for arg in args] - newkwds = dict((name, self._change_project_resource(project, value)) - for name, value in kwds.items()) - return newargs, newkwds - - def _change_project_resource(self, project, obj): - if isinstance(obj, resources.Resource) and \ - obj.project != project: - return libutils.path_to_resource(project, obj.real_path) - return obj - - @property - def project(self): - return self.projects[0] - - @property - def main_refactoring(self): - return self.refactorings[0] - - -def perform(project_changes): - for project, changes in project_changes: - project.do(changes) diff --git a/pythonFiles/rope/refactor/occurrences.py b/pythonFiles/rope/refactor/occurrences.py deleted file mode 100644 index dfc2d685d7d8..000000000000 --- a/pythonFiles/rope/refactor/occurrences.py +++ /dev/null @@ -1,402 +0,0 @@ -"""Find occurrences of a name in a project. - -This module consists of a `Finder` that finds all occurrences of a name -in a project. The `Finder.find_occurrences()` method is a generator that -yields `Occurrence` instances for each occurrence of the name. To create -a `Finder` object, use the `create_finder()` function: - - finder = occurrences.create_finder(project, 'foo', pyname) - for occurrence in finder.find_occurrences(): - pass - -It's possible to filter the occurrences. They can be specified when -calling the `create_finder()` function. - - * `only_calls`: If True, return only those instances where the name is - a function that's being called. - - * `imports`: If False, don't return instances that are in import - statements. - - * `unsure`: If a prediate function, return instances where we don't - know what the name references. It also filters based on the - predicate function. - - * `docs`: If True, it will search for occurrences in regions normally - ignored. E.g., strings and comments. - - * `in_hierarchy`: If True, it will find occurrences if the name is in - the class's hierarchy. - - * `instance`: Used only when you want implicit interfaces to be - considered. - - * `keywords`: If False, don't return instances that are the names of keyword - arguments -""" - -import re - -from rope.base import codeanalyze -from rope.base import evaluate -from rope.base import exceptions -from rope.base import pynames -from rope.base import pyobjects -from rope.base import utils -from rope.base import worder - - -class Finder(object): - """For finding occurrences of a name - - The constructor takes a `filters` argument. It should be a list - of functions that take a single argument. For each possible - occurrence, these functions are called in order with the an - instance of `Occurrence`: - - * If it returns `None` other filters are tried. - * If it returns `True`, the occurrence will be a match. - * If it returns `False`, the occurrence will be skipped. - * If all of the filters return `None`, it is skipped also. - - """ - - def __init__(self, project, name, filters=[lambda o: True], docs=False): - self.project = project - self.name = name - self.docs = docs - self.filters = filters - self._textual_finder = _TextualFinder(name, docs=docs) - - def find_occurrences(self, resource=None, pymodule=None): - """Generate `Occurrence` instances""" - tools = _OccurrenceToolsCreator(self.project, resource=resource, - pymodule=pymodule, docs=self.docs) - for offset in self._textual_finder.find_offsets(tools.source_code): - occurrence = Occurrence(tools, offset) - for filter in self.filters: - result = filter(occurrence) - if result is None: - continue - if result: - yield occurrence - break - - -def create_finder(project, name, pyname, only_calls=False, imports=True, - unsure=None, docs=False, instance=None, in_hierarchy=False, - keywords=True): - """A factory for `Finder` - - Based on the arguments it creates a list of filters. `instance` - argument is needed only when you want implicit interfaces to be - considered. - - """ - pynames_ = set([pyname]) - filters = [] - if only_calls: - filters.append(CallsFilter()) - if not imports: - filters.append(NoImportsFilter()) - if not keywords: - filters.append(NoKeywordsFilter()) - if isinstance(instance, pynames.ParameterName): - for pyobject in instance.get_objects(): - try: - pynames_.add(pyobject[name]) - except exceptions.AttributeNotFoundError: - pass - for pyname in pynames_: - filters.append(PyNameFilter(pyname)) - if in_hierarchy: - filters.append(InHierarchyFilter(pyname)) - if unsure: - filters.append(UnsureFilter(unsure)) - return Finder(project, name, filters=filters, docs=docs) - - -class Occurrence(object): - - def __init__(self, tools, offset): - self.tools = tools - self.offset = offset - self.resource = tools.resource - - @utils.saveit - def get_word_range(self): - return self.tools.word_finder.get_word_range(self.offset) - - @utils.saveit - def get_primary_range(self): - return self.tools.word_finder.get_primary_range(self.offset) - - @utils.saveit - def get_pyname(self): - try: - return self.tools.name_finder.get_pyname_at(self.offset) - except exceptions.BadIdentifierError: - pass - - @utils.saveit - def get_primary_and_pyname(self): - try: - return self.tools.name_finder.get_primary_and_pyname_at( - self.offset) - except exceptions.BadIdentifierError: - pass - - @utils.saveit - def is_in_import_statement(self): - return (self.tools.word_finder.is_from_statement(self.offset) or - self.tools.word_finder.is_import_statement(self.offset)) - - def is_called(self): - return self.tools.word_finder.is_a_function_being_called(self.offset) - - def is_defined(self): - return self.tools.word_finder.is_a_class_or_function_name_in_header( - self.offset) - - def is_a_fixed_primary(self): - return self.tools.word_finder.is_a_class_or_function_name_in_header( - self.offset) or \ - self.tools.word_finder.is_a_name_after_from_import(self.offset) - - def is_written(self): - return self.tools.word_finder.is_assigned_here(self.offset) - - def is_unsure(self): - return unsure_pyname(self.get_pyname()) - - def is_function_keyword_parameter(self): - return self.tools.word_finder.is_function_keyword_parameter( - self.offset) - - @property - @utils.saveit - def lineno(self): - offset = self.get_word_range()[0] - return self.tools.pymodule.lines.get_line_number(offset) - - -def same_pyname(expected, pyname): - """Check whether `expected` and `pyname` are the same""" - if expected is None or pyname is None: - return False - if expected == pyname: - return True - if type(expected) not in (pynames.ImportedModule, pynames.ImportedName) \ - and type(pyname) not in \ - (pynames.ImportedModule, pynames.ImportedName): - return False - return expected.get_definition_location() == \ - pyname.get_definition_location() and \ - expected.get_object() == pyname.get_object() - - -def unsure_pyname(pyname, unbound=True): - """Return `True` if we don't know what this name references""" - if pyname is None: - return True - if unbound and not isinstance(pyname, pynames.UnboundName): - return False - if pyname.get_object() == pyobjects.get_unknown(): - return True - - -class PyNameFilter(object): - """For finding occurrences of a name.""" - - def __init__(self, pyname): - self.pyname = pyname - - def __call__(self, occurrence): - if same_pyname(self.pyname, occurrence.get_pyname()): - return True - - -class InHierarchyFilter(object): - """Finds the occurrence if the name is in the class's hierarchy.""" - - def __init__(self, pyname, implementations_only=False): - self.pyname = pyname - self.impl_only = implementations_only - self.pyclass = self._get_containing_class(pyname) - if self.pyclass is not None: - self.name = pyname.get_object().get_name() - self.roots = self._get_root_classes(self.pyclass, self.name) - else: - self.roots = None - - def __call__(self, occurrence): - if self.roots is None: - return - pyclass = self._get_containing_class(occurrence.get_pyname()) - if pyclass is not None: - roots = self._get_root_classes(pyclass, self.name) - if self.roots.intersection(roots): - return True - - def _get_containing_class(self, pyname): - if isinstance(pyname, pynames.DefinedName): - scope = pyname.get_object().get_scope() - parent = scope.parent - if parent is not None and parent.get_kind() == 'Class': - return parent.pyobject - - def _get_root_classes(self, pyclass, name): - if self.impl_only and pyclass == self.pyclass: - return set([pyclass]) - result = set() - for superclass in pyclass.get_superclasses(): - if name in superclass: - result.update(self._get_root_classes(superclass, name)) - if not result: - return set([pyclass]) - return result - - -class UnsureFilter(object): - """Occurrences where we don't knoow what the name references.""" - - def __init__(self, unsure): - self.unsure = unsure - - def __call__(self, occurrence): - if occurrence.is_unsure() and self.unsure(occurrence): - return True - - -class NoImportsFilter(object): - """Don't include import statements as occurrences.""" - - def __call__(self, occurrence): - if occurrence.is_in_import_statement(): - return False - - -class CallsFilter(object): - """Filter out non-call occurrences.""" - - def __call__(self, occurrence): - if not occurrence.is_called(): - return False - - -class NoKeywordsFilter(object): - """Filter out keyword parameters.""" - - def __call__(self, occurrence): - if occurrence.is_function_keyword_parameter(): - return False - - -class _TextualFinder(object): - - def __init__(self, name, docs=False): - self.name = name - self.docs = docs - self.comment_pattern = _TextualFinder.any('comment', [r'#[^\n]*']) - self.string_pattern = _TextualFinder.any( - 'string', [codeanalyze.get_string_pattern()]) - self.pattern = self._get_occurrence_pattern(self.name) - - def find_offsets(self, source): - if not self._fast_file_query(source): - return - if self.docs: - searcher = self._normal_search - else: - searcher = self._re_search - for matched in searcher(source): - yield matched - - def _re_search(self, source): - for match in self.pattern.finditer(source): - for key, value in match.groupdict().items(): - if value and key == 'occurrence': - yield match.start(key) - - def _normal_search(self, source): - current = 0 - while True: - try: - found = source.index(self.name, current) - current = found + len(self.name) - if (found == 0 or - not self._is_id_char(source[found - 1])) and \ - (current == len(source) or - not self._is_id_char(source[current])): - yield found - except ValueError: - break - - def _is_id_char(self, c): - return c.isalnum() or c == '_' - - def _fast_file_query(self, source): - try: - source.index(self.name) - return True - except ValueError: - return False - - def _get_source(self, resource, pymodule): - if resource is not None: - return resource.read() - else: - return pymodule.source_code - - def _get_occurrence_pattern(self, name): - occurrence_pattern = _TextualFinder.any('occurrence', - ['\\b' + name + '\\b']) - pattern = re.compile(occurrence_pattern + '|' + self.comment_pattern + - '|' + self.string_pattern) - return pattern - - @staticmethod - def any(name, list_): - return '(?P<%s>' % name + '|'.join(list_) + ')' - - -class _OccurrenceToolsCreator(object): - - def __init__(self, project, resource=None, pymodule=None, docs=False): - self.project = project - self.__resource = resource - self.__pymodule = pymodule - self.docs = docs - - @property - @utils.saveit - def name_finder(self): - return evaluate.ScopeNameFinder(self.pymodule) - - @property - @utils.saveit - def source_code(self): - if self.__resource is not None: - return self.resource.read() - else: - return self.pymodule.source_code - - @property - @utils.saveit - def word_finder(self): - return worder.Worder(self.source_code, self.docs) - - @property - @utils.saveit - def resource(self): - if self.__resource is not None: - return self.__resource - if self.__pymodule is not None: - return self.__pymodule.resource - - @property - @utils.saveit - def pymodule(self): - if self.__pymodule is not None: - return self.__pymodule - return self.project.get_pymodule(self.resource) diff --git a/pythonFiles/rope/refactor/patchedast.py b/pythonFiles/rope/refactor/patchedast.py deleted file mode 100644 index 10f0a05cb77c..000000000000 --- a/pythonFiles/rope/refactor/patchedast.py +++ /dev/null @@ -1,829 +0,0 @@ -import collections -import re -import warnings - -from rope.base import ast, codeanalyze, exceptions -from rope.base.utils import pycompat - -try: - basestring -except NameError: - basestring = (str, bytes) - - -def get_patched_ast(source, sorted_children=False): - """Adds ``region`` and ``sorted_children`` fields to nodes - - Adds ``sorted_children`` field only if `sorted_children` is True. - - """ - return patch_ast(ast.parse(source), source, sorted_children) - - -def patch_ast(node, source, sorted_children=False): - """Patches the given node - - After calling, each node in `node` will have a new field named - `region` that is a tuple containing the start and end offsets - of the code that generated it. - - If `sorted_children` is true, a `sorted_children` field will - be created for each node, too. It is a list containing child - nodes as well as whitespaces and comments that occur between - them. - - """ - if hasattr(node, 'region'): - return node - walker = _PatchingASTWalker(source, children=sorted_children) - ast.call_for_nodes(node, walker) - return node - - -def node_region(patched_ast_node): - """Get the region of a patched ast node""" - return patched_ast_node.region - - -def write_ast(patched_ast_node): - """Extract source form a patched AST node with `sorted_children` field - - If the node is patched with sorted_children turned off you can use - `node_region` function for obtaining code using module source code. - """ - result = [] - for child in patched_ast_node.sorted_children: - if isinstance(child, ast.AST): - result.append(write_ast(child)) - else: - result.append(child) - return ''.join(result) - - -class MismatchedTokenError(exceptions.RopeError): - pass - - -class _PatchingASTWalker(object): - - def __init__(self, source, children=False): - self.source = _Source(source) - self.children = children - self.lines = codeanalyze.SourceLinesAdapter(source) - self.children_stack = [] - - Number = object() - String = object() - semicolon_or_as_in_except = object() - - def __call__(self, node): - method = getattr(self, '_' + node.__class__.__name__, None) - if method is not None: - return method(node) - # ???: Unknown node; what should we do here? - warnings.warn('Unknown node type <%s>; please report!' - % node.__class__.__name__, RuntimeWarning) - node.region = (self.source.offset, self.source.offset) - if self.children: - node.sorted_children = ast.get_children(node) - - def _handle(self, node, base_children, eat_parens=False, eat_spaces=False): - if hasattr(node, 'region'): - # ???: The same node was seen twice; what should we do? - warnings.warn( - 'Node <%s> has been already patched; please report!' % - node.__class__.__name__, RuntimeWarning) - return - base_children = collections.deque(base_children) - self.children_stack.append(base_children) - children = collections.deque() - formats = [] - suspected_start = self.source.offset - start = suspected_start - first_token = True - while base_children: - child = base_children.popleft() - if child is None: - continue - offset = self.source.offset - if isinstance(child, ast.AST): - ast.call_for_nodes(child, self) - token_start = child.region[0] - else: - if child is self.String: - region = self.source.consume_string( - end=self._find_next_statement_start()) - elif child is self.Number: - region = self.source.consume_number() - elif child == '!=': - # INFO: This has been added to handle deprecated ``<>`` - region = self.source.consume_not_equal() - elif child == self.semicolon_or_as_in_except: - # INFO: This has been added to handle deprecated - # semicolon in except - region = self.source.consume_except_as_or_semicolon() - else: - region = self.source.consume(child) - child = self.source[region[0]:region[1]] - token_start = region[0] - if not first_token: - formats.append(self.source[offset:token_start]) - if self.children: - children.append(self.source[offset:token_start]) - else: - first_token = False - start = token_start - if self.children: - children.append(child) - start = self._handle_parens(children, start, formats) - if eat_parens: - start = self._eat_surrounding_parens( - children, suspected_start, start) - if eat_spaces: - if self.children: - children.appendleft(self.source[0:start]) - end_spaces = self.source[self.source.offset:] - self.source.consume(end_spaces) - if self.children: - children.append(end_spaces) - start = 0 - if self.children: - node.sorted_children = children - node.region = (start, self.source.offset) - self.children_stack.pop() - - def _handle_parens(self, children, start, formats): - """Changes `children` and returns new start""" - opens, closes = self._count_needed_parens(formats) - old_end = self.source.offset - new_end = None - for i in range(closes): - new_end = self.source.consume(')')[1] - if new_end is not None: - if self.children: - children.append(self.source[old_end:new_end]) - new_start = start - for i in range(opens): - new_start = self.source.rfind_token('(', 0, new_start) - if new_start != start: - if self.children: - children.appendleft(self.source[new_start:start]) - start = new_start - return start - - def _eat_surrounding_parens(self, children, suspected_start, start): - index = self.source.rfind_token('(', suspected_start, start) - if index is not None: - old_start = start - old_offset = self.source.offset - start = index - if self.children: - children.appendleft(self.source[start + 1:old_start]) - children.appendleft('(') - token_start, token_end = self.source.consume(')') - if self.children: - children.append(self.source[old_offset:token_start]) - children.append(')') - return start - - def _count_needed_parens(self, children): - start = 0 - opens = 0 - for child in children: - if not isinstance(child, basestring): - continue - if child == '' or child[0] in '\'"': - continue - index = 0 - while index < len(child): - if child[index] == ')': - if opens > 0: - opens -= 1 - else: - start += 1 - if child[index] == '(': - opens += 1 - if child[index] == '#': - try: - index = child.index('\n', index) - except ValueError: - break - index += 1 - return start, opens - - def _find_next_statement_start(self): - for children in reversed(self.children_stack): - for child in children: - if isinstance(child, ast.stmt): - return child.col_offset \ - + self.lines.get_line_start(child.lineno) - return len(self.source.source) - - _operators = {'And': 'and', 'Or': 'or', 'Add': '+', 'Sub': '-', - 'Mult': '*', 'Div': '/', 'Mod': '%', 'Pow': '**', - 'LShift': '<<', 'RShift': '>>', 'BitOr': '|', 'BitAnd': '&', - 'BitXor': '^', 'FloorDiv': '//', 'Invert': '~', - 'Not': 'not', 'UAdd': '+', 'USub': '-', 'Eq': '==', - 'NotEq': '!=', 'Lt': '<', 'LtE': '<=', 'Gt': '>', - 'GtE': '>=', 'Is': 'is', 'IsNot': 'is not', 'In': 'in', - 'NotIn': 'not in'} - - def _get_op(self, node): - return self._operators[node.__class__.__name__].split(' ') - - def _Attribute(self, node): - self._handle(node, [node.value, '.', node.attr]) - - def _Assert(self, node): - children = ['assert', node.test] - if node.msg: - children.append(',') - children.append(node.msg) - self._handle(node, children) - - def _Assign(self, node): - children = self._child_nodes(node.targets, '=') - children.append('=') - children.append(node.value) - self._handle(node, children) - - def _AugAssign(self, node): - children = [node.target] - children.extend(self._get_op(node.op)) - children.extend(['=', node.value]) - self._handle(node, children) - - def _Repr(self, node): - self._handle(node, ['`', node.value, '`']) - - def _BinOp(self, node): - children = [node.left] + self._get_op(node.op) + [node.right] - self._handle(node, children) - - def _BoolOp(self, node): - self._handle(node, self._child_nodes(node.values, - self._get_op(node.op)[0])) - - def _Break(self, node): - self._handle(node, ['break']) - - def _Call(self, node): - children = [node.func, '('] - args = list(node.args) + node.keywords - children.extend(self._child_nodes(args, ',')) - if getattr(node, 'starargs', None): - if args: - children.append(',') - children.extend(['*', node.starargs]) - if getattr(node, 'kwargs', None): - if args or node.starargs is not None: - children.append(',') - children.extend(['**', node.kwargs]) - children.append(')') - self._handle(node, children) - - def _ClassDef(self, node): - children = [] - if getattr(node, 'decorator_list', None): - for decorator in node.decorator_list: - children.append('@') - children.append(decorator) - children.extend(['class', node.name]) - if node.bases: - children.append('(') - children.extend(self._child_nodes(node.bases, ',')) - children.append(')') - children.append(':') - children.extend(node.body) - self._handle(node, children) - - def _Compare(self, node): - children = [] - children.append(node.left) - for op, expr in zip(node.ops, node.comparators): - children.extend(self._get_op(op)) - children.append(expr) - self._handle(node, children) - - def _Delete(self, node): - self._handle(node, ['del'] + self._child_nodes(node.targets, ',')) - - def _Num(self, node): - self._handle(node, [self.Number]) - - def _Str(self, node): - self._handle(node, [self.String]) - - def _Continue(self, node): - self._handle(node, ['continue']) - - def _Dict(self, node): - children = [] - children.append('{') - if node.keys: - for index, (key, value) in enumerate(zip(node.keys, node.values)): - children.extend([key, ':', value]) - if index < len(node.keys) - 1: - children.append(',') - children.append('}') - self._handle(node, children) - - def _Ellipsis(self, node): - self._handle(node, ['...']) - - def _Expr(self, node): - self._handle(node, [node.value]) - - def _Exec(self, node): - children = [] - children.extend(['exec', node.body]) - if node.globals: - children.extend(['in', node.globals]) - if node.locals: - children.extend([',', node.locals]) - self._handle(node, children) - - def _ExtSlice(self, node): - children = [] - for index, dim in enumerate(node.dims): - if index > 0: - children.append(',') - children.append(dim) - self._handle(node, children) - - def _For(self, node): - children = ['for', node.target, 'in', node.iter, ':'] - children.extend(node.body) - if node.orelse: - children.extend(['else', ':']) - children.extend(node.orelse) - self._handle(node, children) - - def _ImportFrom(self, node): - children = ['from'] - if node.level: - children.append('.' * node.level) - # see comment at rope.base.ast.walk - children.extend([node.module or '', - 'import']) - children.extend(self._child_nodes(node.names, ',')) - self._handle(node, children) - - def _alias(self, node): - children = [node.name] - if node.asname: - children.extend(['as', node.asname]) - self._handle(node, children) - - def _FunctionDef(self, node): - children = [] - try: - decorators = getattr(node, 'decorator_list') - except AttributeError: - decorators = getattr(node, 'decorators', None) - if decorators: - for decorator in decorators: - children.append('@') - children.append(decorator) - children.extend(['def', node.name, '(', node.args]) - children.extend([')', ':']) - children.extend(node.body) - self._handle(node, children) - - def _arguments(self, node): - children = [] - args = list(node.args) - defaults = [None] * (len(args) - len(node.defaults)) + \ - list(node.defaults) - for index, (arg, default) in enumerate(zip(args, defaults)): - if index > 0: - children.append(',') - self._add_args_to_children(children, arg, default) - if node.vararg is not None: - if args: - children.append(',') - children.extend(['*', pycompat.get_ast_arg_arg(node.vararg)]) - if node.kwarg is not None: - if args or node.vararg is not None: - children.append(',') - children.extend(['**', pycompat.get_ast_arg_arg(node.kwarg)]) - self._handle(node, children) - - def _add_args_to_children(self, children, arg, default): - if isinstance(arg, (list, tuple)): - self._add_tuple_parameter(children, arg) - else: - children.append(arg) - if default is not None: - children.append('=') - children.append(default) - - def _add_tuple_parameter(self, children, arg): - children.append('(') - for index, token in enumerate(arg): - if index > 0: - children.append(',') - if isinstance(token, (list, tuple)): - self._add_tuple_parameter(children, token) - else: - children.append(token) - children.append(')') - - def _GeneratorExp(self, node): - children = [node.elt] - children.extend(node.generators) - self._handle(node, children, eat_parens=True) - - def _comprehension(self, node): - children = ['for', node.target, 'in', node.iter] - if node.ifs: - for if_ in node.ifs: - children.append('if') - children.append(if_) - self._handle(node, children) - - def _Global(self, node): - children = self._child_nodes(node.names, ',') - children.insert(0, 'global') - self._handle(node, children) - - def _If(self, node): - if self._is_elif(node): - children = ['elif'] - else: - children = ['if'] - children.extend([node.test, ':']) - children.extend(node.body) - if node.orelse: - if len(node.orelse) == 1 and self._is_elif(node.orelse[0]): - pass - else: - children.extend(['else', ':']) - children.extend(node.orelse) - self._handle(node, children) - - def _is_elif(self, node): - if not isinstance(node, ast.If): - return False - offset = self.lines.get_line_start(node.lineno) + node.col_offset - word = self.source[offset:offset + 4] - # XXX: This is a bug; the offset does not point to the first - alt_word = self.source[offset - 5:offset - 1] - return 'elif' in (word, alt_word) - - def _IfExp(self, node): - return self._handle(node, [node.body, 'if', node.test, - 'else', node.orelse]) - - def _Import(self, node): - children = ['import'] - children.extend(self._child_nodes(node.names, ',')) - self._handle(node, children) - - def _keyword(self, node): - children = [] - if node.arg is None: - children.append(node.value) - else: - children.extend([node.arg, '=', node.value]) - self._handle(node, children) - - def _Lambda(self, node): - self._handle(node, ['lambda', node.args, ':', node.body]) - - def _List(self, node): - self._handle(node, ['['] + self._child_nodes(node.elts, ',') + [']']) - - def _ListComp(self, node): - children = ['[', node.elt] - children.extend(node.generators) - children.append(']') - self._handle(node, children) - - def _Set(self, node): - if node.elts: - self._handle(node, - ['{'] + self._child_nodes(node.elts, ',') + ['}']) - return - # Python doesn't have empty set literals - warnings.warn('Tried to handle empty literal; please report!', - RuntimeWarning) - self._handle(node, ['set(', ')']) - - def _SetComp(self, node): - children = ['{', node.elt] - children.extend(node.generators) - children.append('}') - self._handle(node, children) - - def _DictComp(self, node): - children = ['{'] - children.extend([node.key, ':', node.value]) - children.extend(node.generators) - children.append('}') - self._handle(node, children) - - def _Module(self, node): - self._handle(node, list(node.body), eat_spaces=True) - - def _Name(self, node): - self._handle(node, [node.id]) - - def _NameConstant(self, node): - self._handle(node, [str(node.value)]) - - def _arg(self, node): - self._handle(node, [node.arg]) - - def _Pass(self, node): - self._handle(node, ['pass']) - - def _Print(self, node): - children = ['print'] - if node.dest: - children.extend(['>>', node.dest]) - if node.values: - children.append(',') - children.extend(self._child_nodes(node.values, ',')) - if not node.nl: - children.append(',') - self._handle(node, children) - - def _Raise(self, node): - - def get_python3_raise_children(node): - children = ['raise'] - if node.exc: - children.append(node.exc) - if node.cause: - children.append(node.cause) - return children - - def get_python2_raise_children(node): - children = ['raise'] - if node.type: - children.append(node.type) - if node.inst: - children.append(',') - children.append(node.inst) - if node.tback: - children.append(',') - children.append(node.tback) - return children - if pycompat.PY2: - children = get_python2_raise_children(node) - else: - children = get_python3_raise_children(node) - self._handle(node, children) - - def _Return(self, node): - children = ['return'] - if node.value: - children.append(node.value) - self._handle(node, children) - - def _Sliceobj(self, node): - children = [] - for index, slice in enumerate(node.nodes): - if index > 0: - children.append(':') - if slice: - children.append(slice) - self._handle(node, children) - - def _Index(self, node): - self._handle(node, [node.value]) - - def _Subscript(self, node): - self._handle(node, [node.value, '[', node.slice, ']']) - - def _Slice(self, node): - children = [] - if node.lower: - children.append(node.lower) - children.append(':') - if node.upper: - children.append(node.upper) - if node.step: - children.append(':') - children.append(node.step) - self._handle(node, children) - - def _TryFinally(self, node): - # @todo fixme - is_there_except_handler = False - not_empty_body = True - if len(node.finalbody) == 1: - if pycompat.PY2: - is_there_except_handler = isinstance(node.body[0], ast.TryExcept) - not_empty_body = not bool(len(node.body)) - elif pycompat.PY3: - try: - is_there_except_handler = isinstance(node.handlers[0], ast.ExceptHandler) - not_empty_body = True - except IndexError: - pass - children = [] - if not_empty_body or not is_there_except_handler: - children.extend(['try', ':']) - children.extend(node.body) - if pycompat.PY3: - children.extend(node.handlers) - children.extend(['finally', ':']) - children.extend(node.finalbody) - self._handle(node, children) - - def _TryExcept(self, node): - children = ['try', ':'] - children.extend(node.body) - children.extend(node.handlers) - if node.orelse: - children.extend(['else', ':']) - children.extend(node.orelse) - self._handle(node, children) - - def _Try(self, node): - if len(node.finalbody): - self._TryFinally(node) - else: - self._TryExcept(node) - - def _ExceptHandler(self, node): - self._excepthandler(node) - - def _excepthandler(self, node): - # self._handle(node, [self.semicolon_or_as_in_except]) - children = ['except'] - if node.type: - children.append(node.type) - if node.name: - children.append(self.semicolon_or_as_in_except) - children.append(node.name) - children.append(':') - children.extend(node.body) - - self._handle(node, children) - - def _Tuple(self, node): - if node.elts: - self._handle(node, self._child_nodes(node.elts, ','), - eat_parens=True) - else: - self._handle(node, ['(', ')']) - - def _UnaryOp(self, node): - children = self._get_op(node.op) - children.append(node.operand) - self._handle(node, children) - - def _Yield(self, node): - children = ['yield'] - if node.value: - children.append(node.value) - self._handle(node, children) - - def _While(self, node): - children = ['while', node.test, ':'] - children.extend(node.body) - if node.orelse: - children.extend(['else', ':']) - children.extend(node.orelse) - self._handle(node, children) - - def _With(self, node): - children = [] - for item in pycompat.get_ast_with_items(node): - children.extend(['with', item.context_expr]) - if item.optional_vars: - children.extend(['as', item.optional_vars]) - children.append(':') - children.extend(node.body) - self._handle(node, children) - - def _child_nodes(self, nodes, separator): - children = [] - for index, child in enumerate(nodes): - children.append(child) - if index < len(nodes) - 1: - children.append(separator) - return children - - def _Starred(self, node): - self._handle(node, [node.value]) - -class _Source(object): - - def __init__(self, source): - self.source = source - self.offset = 0 - - def consume(self, token): - try: - while True: - new_offset = self.source.index(token, self.offset) - if self._good_token(token, new_offset): - break - else: - self._skip_comment() - except (ValueError, TypeError): - raise MismatchedTokenError( - 'Token <%s> at %s cannot be matched' % - (token, self._get_location())) - self.offset = new_offset + len(token) - return (new_offset, self.offset) - - def consume_string(self, end=None): - if _Source._string_pattern is None: - original = codeanalyze.get_string_pattern() - pattern = r'(%s)((\s|\\\n|#[^\n]*\n)*(%s))*' % \ - (original, original) - _Source._string_pattern = re.compile(pattern) - repattern = _Source._string_pattern - return self._consume_pattern(repattern, end) - - def consume_number(self): - if _Source._number_pattern is None: - _Source._number_pattern = re.compile( - self._get_number_pattern()) - repattern = _Source._number_pattern - return self._consume_pattern(repattern) - - def consume_not_equal(self): - if _Source._not_equals_pattern is None: - _Source._not_equals_pattern = re.compile(r'<>|!=') - repattern = _Source._not_equals_pattern - return self._consume_pattern(repattern) - - def consume_except_as_or_semicolon(self): - repattern = re.compile(r'as|,') - return self._consume_pattern(repattern) - - def _good_token(self, token, offset, start=None): - """Checks whether consumed token is in comments""" - if start is None: - start = self.offset - try: - comment_index = self.source.rindex('#', start, offset) - except ValueError: - return True - try: - new_line_index = self.source.rindex('\n', start, offset) - except ValueError: - return False - return comment_index < new_line_index - - def _skip_comment(self): - self.offset = self.source.index('\n', self.offset + 1) - - def _get_location(self): - lines = self.source[:self.offset].split('\n') - return (len(lines), len(lines[-1])) - - def _consume_pattern(self, repattern, end=None): - while True: - if end is None: - end = len(self.source) - match = repattern.search(self.source, self.offset, end) - if self._good_token(match.group(), match.start()): - break - else: - self._skip_comment() - self.offset = match.end() - return match.start(), match.end() - - def till_token(self, token): - new_offset = self.source.index(token, self.offset) - return self[self.offset:new_offset] - - def rfind_token(self, token, start, end): - index = start - while True: - try: - index = self.source.rindex(token, start, end) - if self._good_token(token, index, start=start): - return index - else: - end = index - except ValueError: - return None - - def from_offset(self, offset): - return self[offset:self.offset] - - def find_backwards(self, pattern, offset): - return self.source.rindex(pattern, 0, offset) - - def __getitem__(self, index): - return self.source[index] - - def __getslice__(self, i, j): - return self.source[i:j] - - def _get_number_pattern(self): - # HACK: It is merely an approaximation and does the job - integer = r'\-?(0x[\da-fA-F]+|\d+)[lL]?' - return r'(%s(\.\d*)?|(\.\d+))([eE][-+]?\d+)?[jJ]?' % integer - - _string_pattern = None - _number_pattern = None - _not_equals_pattern = None diff --git a/pythonFiles/rope/refactor/rename.py b/pythonFiles/rope/refactor/rename.py deleted file mode 100644 index 3f1f5b7e6d61..000000000000 --- a/pythonFiles/rope/refactor/rename.py +++ /dev/null @@ -1,220 +0,0 @@ -import warnings - -from rope.base import (exceptions, pyobjects, pynames, taskhandle, - evaluate, worder, codeanalyze, libutils) -from rope.base.change import ChangeSet, ChangeContents, MoveResource -from rope.refactor import occurrences - - -class Rename(object): - """A class for performing rename refactoring - - It can rename everything: classes, functions, modules, packages, - methods, variables and keyword arguments. - - """ - - def __init__(self, project, resource, offset=None): - """If `offset` is None, the `resource` itself will be renamed""" - self.project = project - self.resource = resource - if offset is not None: - self.old_name = worder.get_name_at(self.resource, offset) - this_pymodule = self.project.get_pymodule(self.resource) - self.old_instance, self.old_pyname = \ - evaluate.eval_location2(this_pymodule, offset) - if self.old_pyname is None: - raise exceptions.RefactoringError( - 'Rename refactoring should be performed' - ' on resolvable python identifiers.') - else: - if not resource.is_folder() and resource.name == '__init__.py': - resource = resource.parent - dummy_pymodule = libutils.get_string_module(self.project, '') - self.old_instance = None - self.old_pyname = pynames.ImportedModule(dummy_pymodule, - resource=resource) - if resource.is_folder(): - self.old_name = resource.name - else: - self.old_name = resource.name[:-3] - - def get_old_name(self): - return self.old_name - - def get_changes(self, new_name, in_file=None, in_hierarchy=False, - unsure=None, docs=False, resources=None, - task_handle=taskhandle.NullTaskHandle()): - """Get the changes needed for this refactoring - - Parameters: - - - `in_hierarchy`: when renaming a method this keyword forces - to rename all matching methods in the hierarchy - - `docs`: when `True` rename refactoring will rename - occurrences in comments and strings where the name is - visible. Setting it will make renames faster, too. - - `unsure`: decides what to do about unsure occurrences. - If `None`, they are ignored. Otherwise `unsure` is - called with an instance of `occurrence.Occurrence` as - parameter. If it returns `True`, the occurrence is - considered to be a match. - - `resources` can be a list of `rope.base.resources.File`\s to - apply this refactoring on. If `None`, the restructuring - will be applied to all python files. - - `in_file`: this argument has been deprecated; use - `resources` instead. - - """ - if unsure in (True, False): - warnings.warn( - 'unsure parameter should be a function that returns ' - 'True or False', DeprecationWarning, stacklevel=2) - - def unsure_func(value=unsure): - return value - unsure = unsure_func - if in_file is not None: - warnings.warn( - '`in_file` argument has been deprecated; use `resources` ' - 'instead. ', DeprecationWarning, stacklevel=2) - if in_file: - resources = [self.resource] - if _is_local(self.old_pyname): - resources = [self.resource] - if resources is None: - resources = self.project.get_python_files() - changes = ChangeSet('Renaming <%s> to <%s>' % - (self.old_name, new_name)) - finder = occurrences.create_finder( - self.project, self.old_name, self.old_pyname, unsure=unsure, - docs=docs, instance=self.old_instance, - in_hierarchy=in_hierarchy and self.is_method()) - job_set = task_handle.create_jobset('Collecting Changes', - len(resources)) - for file_ in resources: - job_set.started_job(file_.path) - new_content = rename_in_module(finder, new_name, resource=file_) - if new_content is not None: - changes.add_change(ChangeContents(file_, new_content)) - job_set.finished_job() - if self._is_renaming_a_module(): - resource = self.old_pyname.get_object().get_resource() - if self._is_allowed_to_move(resources, resource): - self._rename_module(resource, new_name, changes) - return changes - - def _is_allowed_to_move(self, resources, resource): - if resource.is_folder(): - try: - return resource.get_child('__init__.py') in resources - except exceptions.ResourceNotFoundError: - return False - else: - return resource in resources - - def _is_renaming_a_module(self): - if isinstance(self.old_pyname.get_object(), pyobjects.AbstractModule): - return True - return False - - def is_method(self): - pyname = self.old_pyname - return isinstance(pyname, pynames.DefinedName) and \ - isinstance(pyname.get_object(), pyobjects.PyFunction) and \ - isinstance(pyname.get_object().parent, pyobjects.PyClass) - - def _rename_module(self, resource, new_name, changes): - if not resource.is_folder(): - new_name = new_name + '.py' - parent_path = resource.parent.path - if parent_path == '': - new_location = new_name - else: - new_location = parent_path + '/' + new_name - changes.add_change(MoveResource(resource, new_location)) - - -class ChangeOccurrences(object): - """A class for changing the occurrences of a name in a scope - - This class replaces the occurrences of a name. Note that it only - changes the scope containing the offset passed to the constructor. - What's more it does not have any side-effects. That is for - example changing occurrences of a module does not rename the - module; it merely replaces the occurrences of that module in a - scope with the given expression. This class is useful for - performing many custom refactorings. - - """ - - def __init__(self, project, resource, offset): - self.project = project - self.resource = resource - self.offset = offset - self.old_name = worder.get_name_at(resource, offset) - self.pymodule = project.get_pymodule(self.resource) - self.old_pyname = evaluate.eval_location(self.pymodule, offset) - - def get_old_name(self): - word_finder = worder.Worder(self.resource.read()) - return word_finder.get_primary_at(self.offset) - - def _get_scope_offset(self): - lines = self.pymodule.lines - scope = self.pymodule.get_scope().\ - get_inner_scope_for_line(lines.get_line_number(self.offset)) - start = lines.get_line_start(scope.get_start()) - end = lines.get_line_end(scope.get_end()) - return start, end - - def get_changes(self, new_name, only_calls=False, reads=True, writes=True): - changes = ChangeSet('Changing <%s> occurrences to <%s>' % - (self.old_name, new_name)) - scope_start, scope_end = self._get_scope_offset() - finder = occurrences.create_finder( - self.project, self.old_name, self.old_pyname, - imports=False, only_calls=only_calls) - new_contents = rename_in_module( - finder, new_name, pymodule=self.pymodule, replace_primary=True, - region=(scope_start, scope_end), reads=reads, writes=writes) - if new_contents is not None: - changes.add_change(ChangeContents(self.resource, new_contents)) - return changes - - -def rename_in_module(occurrences_finder, new_name, resource=None, - pymodule=None, replace_primary=False, region=None, - reads=True, writes=True): - """Returns the changed source or `None` if there is no changes""" - if resource is not None: - source_code = resource.read() - else: - source_code = pymodule.source_code - change_collector = codeanalyze.ChangeCollector(source_code) - for occurrence in occurrences_finder.find_occurrences(resource, pymodule): - if replace_primary and occurrence.is_a_fixed_primary(): - continue - if replace_primary: - start, end = occurrence.get_primary_range() - else: - start, end = occurrence.get_word_range() - if (not reads and not occurrence.is_written()) or \ - (not writes and occurrence.is_written()): - continue - if region is None or region[0] <= start < region[1]: - change_collector.add_change(start, end, new_name) - return change_collector.get_changed() - - -def _is_local(pyname): - module, lineno = pyname.get_definition_location() - if lineno is None: - return False - scope = module.get_scope().get_inner_scope_for_line(lineno) - if isinstance(pyname, pynames.DefinedName) and \ - scope.get_kind() in ('Function', 'Class'): - scope = scope.parent - return scope.get_kind() == 'Function' and \ - pyname in scope.get_names().values() and \ - isinstance(pyname, pynames.AssignedName) diff --git a/pythonFiles/rope/refactor/restructure.py b/pythonFiles/rope/refactor/restructure.py deleted file mode 100644 index 98a11e3d7741..000000000000 --- a/pythonFiles/rope/refactor/restructure.py +++ /dev/null @@ -1,307 +0,0 @@ -import warnings - -from rope.base import change, taskhandle, builtins, ast, codeanalyze -from rope.base import libutils -from rope.refactor import patchedast, similarfinder, sourceutils -from rope.refactor.importutils import module_imports - - -class Restructure(object): - """A class to perform python restructurings - - A restructuring transforms pieces of code matching `pattern` to - `goal`. In the `pattern` wildcards can appear. Wildcards match - some piece of code based on their kind and arguments that are - passed to them through `args`. - - `args` is a dictionary of wildcard names to wildcard arguments. - If the argument is a tuple, the first item of the tuple is - considered to be the name of the wildcard to use; otherwise the - "default" wildcard is used. For getting the list arguments a - wildcard supports, see the pydoc of the wildcard. (see - `rope.refactor.wildcard.DefaultWildcard` for the default - wildcard.) - - `wildcards` is the list of wildcard types that can appear in - `pattern`. See `rope.refactor.wildcards`. If a wildcard does not - specify its kind (by using a tuple in args), the wildcard named - "default" is used. So there should be a wildcard with "default" - name in `wildcards`. - - `imports` is the list of imports that changed modules should - import. Note that rope handles duplicate imports and does not add - the import if it already appears. - - Example #1:: - - pattern ${pyobject}.get_attribute(${name}) - goal ${pyobject}[${name}] - args pyobject: instance=rope.base.pyobjects.PyObject - - Example #2:: - - pattern ${name} in ${pyobject}.get_attributes() - goal ${name} in {pyobject} - args pyobject: instance=rope.base.pyobjects.PyObject - - Example #3:: - - pattern ${pycore}.create_module(${project}.root, ${name}) - goal generate.create_module(${project}, ${name}) - - imports - from rope.contrib import generate - - args - project: type=rope.base.project.Project - - Example #4:: - - pattern ${pow}(${param1}, ${param2}) - goal ${param1} ** ${param2} - args pow: name=mod.pow, exact - - Example #5:: - - pattern ${inst}.longtask(${p1}, ${p2}) - goal - ${inst}.subtask1(${p1}) - ${inst}.subtask2(${p2}) - args - inst: type=mod.A,unsure - - """ - - def __init__(self, project, pattern, goal, args=None, - imports=None, wildcards=None): - """Construct a restructuring - - See class pydoc for more info about the arguments. - - """ - self.project = project - self.pattern = pattern - self.goal = goal - self.args = args - if self.args is None: - self.args = {} - self.imports = imports - if self.imports is None: - self.imports = [] - self.wildcards = wildcards - self.template = similarfinder.CodeTemplate(self.goal) - - def get_changes(self, checks=None, imports=None, resources=None, - task_handle=taskhandle.NullTaskHandle()): - """Get the changes needed by this restructuring - - `resources` can be a list of `rope.base.resources.File`\s to - apply the restructuring on. If `None`, the restructuring will - be applied to all python files. - - `checks` argument has been deprecated. Use the `args` argument - of the constructor. The usage of:: - - strchecks = {'obj1.type': 'mod.A', 'obj2': 'mod.B', - 'obj3.object': 'mod.C'} - checks = restructuring.make_checks(strchecks) - - can be replaced with:: - - args = {'obj1': 'type=mod.A', 'obj2': 'name=mod.B', - 'obj3': 'object=mod.C'} - - where obj1, obj2 and obj3 are wildcard names that appear - in restructuring pattern. - - """ - if checks is not None: - warnings.warn( - 'The use of checks parameter is deprecated; ' - 'use the args parameter of the constructor instead.', - DeprecationWarning, stacklevel=2) - for name, value in checks.items(): - self.args[name] = similarfinder._pydefined_to_str(value) - if imports is not None: - warnings.warn( - 'The use of imports parameter is deprecated; ' - 'use imports parameter of the constructor, instead.', - DeprecationWarning, stacklevel=2) - self.imports = imports - changes = change.ChangeSet('Restructuring <%s> to <%s>' % - (self.pattern, self.goal)) - if resources is not None: - files = [resource for resource in resources - if libutils.is_python_file(self.project, resource)] - else: - files = self.project.get_python_files() - job_set = task_handle.create_jobset('Collecting Changes', len(files)) - for resource in files: - job_set.started_job(resource.path) - pymodule = self.project.get_pymodule(resource) - finder = similarfinder.SimilarFinder(pymodule, - wildcards=self.wildcards) - matches = list(finder.get_matches(self.pattern, self.args)) - computer = self._compute_changes(matches, pymodule) - result = computer.get_changed() - if result is not None: - imported_source = self._add_imports(resource, result, - self.imports) - changes.add_change(change.ChangeContents(resource, - imported_source)) - job_set.finished_job() - return changes - - def _compute_changes(self, matches, pymodule): - return _ChangeComputer( - pymodule.source_code, pymodule.get_ast(), - pymodule.lines, self.template, matches) - - def _add_imports(self, resource, source, imports): - if not imports: - return source - import_infos = self._get_import_infos(resource, imports) - pymodule = libutils.get_string_module(self.project, source, resource) - imports = module_imports.ModuleImports(self.project, pymodule) - for import_info in import_infos: - imports.add_import(import_info) - return imports.get_changed_source() - - def _get_import_infos(self, resource, imports): - pymodule = libutils.get_string_module( - self.project, '\n'.join(imports), resource) - imports = module_imports.ModuleImports(self.project, pymodule) - return [imports.import_info - for imports in imports.imports] - - def make_checks(self, string_checks): - """Convert str to str dicts to str to PyObject dicts - - This function is here to ease writing a UI. - - """ - checks = {} - for key, value in string_checks.items(): - is_pyname = not key.endswith('.object') and \ - not key.endswith('.type') - evaluated = self._evaluate(value, is_pyname=is_pyname) - if evaluated is not None: - checks[key] = evaluated - return checks - - def _evaluate(self, code, is_pyname=True): - attributes = code.split('.') - pyname = None - if attributes[0] in ('__builtin__', '__builtins__'): - class _BuiltinsStub(object): - def get_attribute(self, name): - return builtins.builtins[name] - pyobject = _BuiltinsStub() - else: - pyobject = self.project.get_module(attributes[0]) - for attribute in attributes[1:]: - pyname = pyobject[attribute] - if pyname is None: - return None - pyobject = pyname.get_object() - return pyname if is_pyname else pyobject - - -def replace(code, pattern, goal): - """used by other refactorings""" - finder = similarfinder.RawSimilarFinder(code) - matches = list(finder.get_matches(pattern)) - ast = patchedast.get_patched_ast(code) - lines = codeanalyze.SourceLinesAdapter(code) - template = similarfinder.CodeTemplate(goal) - computer = _ChangeComputer(code, ast, lines, template, matches) - result = computer.get_changed() - if result is None: - return code - return result - - -class _ChangeComputer(object): - - def __init__(self, code, ast, lines, goal, matches): - self.source = code - self.goal = goal - self.matches = matches - self.ast = ast - self.lines = lines - self.matched_asts = {} - self._nearest_roots = {} - if self._is_expression(): - for match in self.matches: - self.matched_asts[match.ast] = match - - def get_changed(self): - if self._is_expression(): - result = self._get_node_text(self.ast) - if result == self.source: - return None - return result - else: - collector = codeanalyze.ChangeCollector(self.source) - last_end = -1 - for match in self.matches: - start, end = match.get_region() - if start < last_end: - if not self._is_expression(): - continue - last_end = end - replacement = self._get_matched_text(match) - collector.add_change(start, end, replacement) - return collector.get_changed() - - def _is_expression(self): - return self.matches and isinstance(self.matches[0], - similarfinder.ExpressionMatch) - - def _get_matched_text(self, match): - mapping = {} - for name in self.goal.get_names(): - node = match.get_ast(name) - if node is None: - raise similarfinder.BadNameInCheckError( - 'Unknown name <%s>' % name) - force = self._is_expression() and match.ast == node - mapping[name] = self._get_node_text(node, force) - unindented = self.goal.substitute(mapping) - return self._auto_indent(match.get_region()[0], unindented) - - def _get_node_text(self, node, force=False): - if not force and node in self.matched_asts: - return self._get_matched_text(self.matched_asts[node]) - start, end = patchedast.node_region(node) - main_text = self.source[start:end] - collector = codeanalyze.ChangeCollector(main_text) - for node in self._get_nearest_roots(node): - sub_start, sub_end = patchedast.node_region(node) - collector.add_change(sub_start - start, sub_end - start, - self._get_node_text(node)) - result = collector.get_changed() - if result is None: - return main_text - return result - - def _auto_indent(self, offset, text): - lineno = self.lines.get_line_number(offset) - indents = sourceutils.get_indents(self.lines, lineno) - result = [] - for index, line in enumerate(text.splitlines(True)): - if index != 0 and line.strip(): - result.append(' ' * indents) - result.append(line) - return ''.join(result) - - def _get_nearest_roots(self, node): - if node not in self._nearest_roots: - result = [] - for child in ast.get_child_nodes(node): - if child in self.matched_asts: - result.append(child) - else: - result.extend(self._get_nearest_roots(child)) - self._nearest_roots[node] = result - return self._nearest_roots[node] diff --git a/pythonFiles/rope/refactor/similarfinder.py b/pythonFiles/rope/refactor/similarfinder.py deleted file mode 100644 index 425f9ed955c2..000000000000 --- a/pythonFiles/rope/refactor/similarfinder.py +++ /dev/null @@ -1,370 +0,0 @@ -"""This module can be used for finding similar code""" -import re - -import rope.refactor.wildcards -from rope.base import libutils -from rope.base import codeanalyze, exceptions, ast, builtins -from rope.refactor import (patchedast, wildcards) - -from rope.refactor.patchedast import MismatchedTokenError - - -class BadNameInCheckError(exceptions.RefactoringError): - pass - - -class SimilarFinder(object): - """`SimilarFinder` can be used to find similar pieces of code - - See the notes in the `rope.refactor.restructure` module for more - info. - - """ - - def __init__(self, pymodule, wildcards=None): - """Construct a SimilarFinder""" - self.source = pymodule.source_code - try: - self.raw_finder = RawSimilarFinder( - pymodule.source_code, pymodule.get_ast(), self._does_match) - except MismatchedTokenError: - print("in file %s" % pymodule.resource.path) - raise - self.pymodule = pymodule - if wildcards is None: - self.wildcards = {} - for wildcard in [rope.refactor.wildcards. - DefaultWildcard(pymodule.pycore.project)]: - self.wildcards[wildcard.get_name()] = wildcard - else: - self.wildcards = wildcards - - def get_matches(self, code, args={}, start=0, end=None): - self.args = args - if end is None: - end = len(self.source) - skip_region = None - if 'skip' in args.get('', {}): - resource, region = args['']['skip'] - if resource == self.pymodule.get_resource(): - skip_region = region - return self.raw_finder.get_matches(code, start=start, end=end, - skip=skip_region) - - def get_match_regions(self, *args, **kwds): - for match in self.get_matches(*args, **kwds): - yield match.get_region() - - def _does_match(self, node, name): - arg = self.args.get(name, '') - kind = 'default' - if isinstance(arg, (tuple, list)): - kind = arg[0] - arg = arg[1] - suspect = wildcards.Suspect(self.pymodule, node, name) - return self.wildcards[kind].matches(suspect, arg) - - -class RawSimilarFinder(object): - """A class for finding similar expressions and statements""" - - def __init__(self, source, node=None, does_match=None): - if node is None: - node = ast.parse(source) - if does_match is None: - self.does_match = self._simple_does_match - else: - self.does_match = does_match - self._init_using_ast(node, source) - - def _simple_does_match(self, node, name): - return isinstance(node, (ast.expr, ast.Name)) - - def _init_using_ast(self, node, source): - self.source = source - self._matched_asts = {} - if not hasattr(node, 'region'): - patchedast.patch_ast(node, source) - self.ast = node - - def get_matches(self, code, start=0, end=None, skip=None): - """Search for `code` in source and return a list of `Match`\es - - `code` can contain wildcards. ``${name}`` matches normal - names and ``${?name} can match any expression. You can use - `Match.get_ast()` for getting the node that has matched a - given pattern. - - """ - if end is None: - end = len(self.source) - for match in self._get_matched_asts(code): - match_start, match_end = match.get_region() - if start <= match_start and match_end <= end: - if skip is not None and (skip[0] < match_end and - skip[1] > match_start): - continue - yield match - - def _get_matched_asts(self, code): - if code not in self._matched_asts: - wanted = self._create_pattern(code) - matches = _ASTMatcher(self.ast, wanted, - self.does_match).find_matches() - self._matched_asts[code] = matches - return self._matched_asts[code] - - def _create_pattern(self, expression): - expression = self._replace_wildcards(expression) - node = ast.parse(expression) - # Getting Module.Stmt.nodes - nodes = node.body - if len(nodes) == 1 and isinstance(nodes[0], ast.Expr): - # Getting Discard.expr - wanted = nodes[0].value - else: - wanted = nodes - return wanted - - def _replace_wildcards(self, expression): - ropevar = _RopeVariable() - template = CodeTemplate(expression) - mapping = {} - for name in template.get_names(): - mapping[name] = ropevar.get_var(name) - return template.substitute(mapping) - - -class _ASTMatcher(object): - - def __init__(self, body, pattern, does_match): - """Searches the given pattern in the body AST. - - body is an AST node and pattern can be either an AST node or - a list of ASTs nodes - """ - self.body = body - self.pattern = pattern - self.matches = None - self.ropevar = _RopeVariable() - self.matches_callback = does_match - - def find_matches(self): - if self.matches is None: - self.matches = [] - ast.call_for_nodes(self.body, self._check_node, recursive=True) - return self.matches - - def _check_node(self, node): - if isinstance(self.pattern, list): - self._check_statements(node) - else: - self._check_expression(node) - - def _check_expression(self, node): - mapping = {} - if self._match_nodes(self.pattern, node, mapping): - self.matches.append(ExpressionMatch(node, mapping)) - - def _check_statements(self, node): - for child in ast.get_children(node): - if isinstance(child, (list, tuple)): - self.__check_stmt_list(child) - - def __check_stmt_list(self, nodes): - for index in range(len(nodes)): - if len(nodes) - index >= len(self.pattern): - current_stmts = nodes[index:index + len(self.pattern)] - mapping = {} - if self._match_stmts(current_stmts, mapping): - self.matches.append(StatementMatch(current_stmts, mapping)) - - def _match_nodes(self, expected, node, mapping): - if isinstance(expected, ast.Name): - if self.ropevar.is_var(expected.id): - return self._match_wildcard(expected, node, mapping) - if not isinstance(expected, ast.AST): - return expected == node - if expected.__class__ != node.__class__: - return False - - children1 = self._get_children(expected) - children2 = self._get_children(node) - if len(children1) != len(children2): - return False - for child1, child2 in zip(children1, children2): - if isinstance(child1, ast.AST): - if not self._match_nodes(child1, child2, mapping): - return False - elif isinstance(child1, (list, tuple)): - if not isinstance(child2, (list, tuple)) or \ - len(child1) != len(child2): - return False - for c1, c2 in zip(child1, child2): - if not self._match_nodes(c1, c2, mapping): - return False - else: - if child1 != child2: - return False - return True - - def _get_children(self, node): - """Return not `ast.expr_context` children of `node`""" - children = ast.get_children(node) - return [child for child in children - if not isinstance(child, ast.expr_context)] - - def _match_stmts(self, current_stmts, mapping): - if len(current_stmts) != len(self.pattern): - return False - for stmt, expected in zip(current_stmts, self.pattern): - if not self._match_nodes(expected, stmt, mapping): - return False - return True - - def _match_wildcard(self, node1, node2, mapping): - name = self.ropevar.get_base(node1.id) - if name not in mapping: - if self.matches_callback(node2, name): - mapping[name] = node2 - return True - return False - else: - return self._match_nodes(mapping[name], node2, {}) - - -class Match(object): - - def __init__(self, mapping): - self.mapping = mapping - - def get_region(self): - """Returns match region""" - - def get_ast(self, name): - """Return the ast node that has matched rope variables""" - return self.mapping.get(name, None) - - -class ExpressionMatch(Match): - - def __init__(self, ast, mapping): - super(ExpressionMatch, self).__init__(mapping) - self.ast = ast - - def get_region(self): - return self.ast.region - - -class StatementMatch(Match): - - def __init__(self, ast_list, mapping): - super(StatementMatch, self).__init__(mapping) - self.ast_list = ast_list - - def get_region(self): - return self.ast_list[0].region[0], self.ast_list[-1].region[1] - - -class CodeTemplate(object): - - def __init__(self, template): - self.template = template - self._find_names() - - def _find_names(self): - self.names = {} - for match in CodeTemplate._get_pattern().finditer(self.template): - if 'name' in match.groupdict() and \ - match.group('name') is not None: - start, end = match.span('name') - name = self.template[start + 2:end - 1] - if name not in self.names: - self.names[name] = [] - self.names[name].append((start, end)) - - def get_names(self): - return self.names.keys() - - def substitute(self, mapping): - collector = codeanalyze.ChangeCollector(self.template) - for name, occurrences in self.names.items(): - for region in occurrences: - collector.add_change(region[0], region[1], mapping[name]) - result = collector.get_changed() - if result is None: - return self.template - return result - - _match_pattern = None - - @classmethod - def _get_pattern(cls): - if cls._match_pattern is None: - pattern = codeanalyze.get_comment_pattern() + '|' + \ - codeanalyze.get_string_pattern() + '|' + \ - r'(?P\$\{[^\s\$\}]*\})' - cls._match_pattern = re.compile(pattern) - return cls._match_pattern - - -class _RopeVariable(object): - """Transform and identify rope inserted wildcards""" - - _normal_prefix = '__rope__variable_normal_' - _any_prefix = '__rope__variable_any_' - - def get_var(self, name): - if name.startswith('?'): - return self._get_any(name) - else: - return self._get_normal(name) - - def is_var(self, name): - return self._is_normal(name) or self._is_var(name) - - def get_base(self, name): - if self._is_normal(name): - return name[len(self._normal_prefix):] - if self._is_var(name): - return '?' + name[len(self._any_prefix):] - - def _get_normal(self, name): - return self._normal_prefix + name - - def _get_any(self, name): - return self._any_prefix + name[1:] - - def _is_normal(self, name): - return name.startswith(self._normal_prefix) - - def _is_var(self, name): - return name.startswith(self._any_prefix) - - -def make_pattern(code, variables): - variables = set(variables) - collector = codeanalyze.ChangeCollector(code) - - def does_match(node, name): - return isinstance(node, ast.Name) and node.id == name - finder = RawSimilarFinder(code, does_match=does_match) - for variable in variables: - for match in finder.get_matches('${%s}' % variable): - start, end = match.get_region() - collector.add_change(start, end, '${%s}' % variable) - result = collector.get_changed() - return result if result is not None else code - - -def _pydefined_to_str(pydefined): - address = [] - if isinstance(pydefined, - (builtins.BuiltinClass, builtins.BuiltinFunction)): - return '__builtins__.' + pydefined.get_name() - else: - while pydefined.parent is not None: - address.insert(0, pydefined.get_name()) - pydefined = pydefined.parent - module_name = libutils.modname(pydefined.resource) - return '.'.join(module_name.split('.') + address) diff --git a/pythonFiles/rope/refactor/sourceutils.py b/pythonFiles/rope/refactor/sourceutils.py deleted file mode 100644 index 9b842906636f..000000000000 --- a/pythonFiles/rope/refactor/sourceutils.py +++ /dev/null @@ -1,91 +0,0 @@ -from rope.base import codeanalyze - - -def get_indents(lines, lineno): - return codeanalyze.count_line_indents(lines.get_line(lineno)) - - -def find_minimum_indents(source_code): - result = 80 - lines = source_code.split('\n') - for line in lines: - if line.strip() == '': - continue - result = min(result, codeanalyze.count_line_indents(line)) - return result - - -def indent_lines(source_code, amount): - if amount == 0: - return source_code - lines = source_code.splitlines(True) - result = [] - for l in lines: - if l.strip() == '': - result.append('\n') - continue - if amount < 0: - indents = codeanalyze.count_line_indents(l) - result.append(max(0, indents + amount) * ' ' + l.lstrip()) - else: - result.append(' ' * amount + l) - return ''.join(result) - - -def fix_indentation(code, new_indents): - """Change the indentation of `code` to `new_indents`""" - min_indents = find_minimum_indents(code) - return indent_lines(code, new_indents - min_indents) - - -def add_methods(pymodule, class_scope, methods_sources): - source_code = pymodule.source_code - lines = pymodule.lines - insertion_line = class_scope.get_end() - if class_scope.get_scopes(): - insertion_line = class_scope.get_scopes()[-1].get_end() - insertion_offset = lines.get_line_end(insertion_line) - methods = '\n\n' + '\n\n'.join(methods_sources) - indented_methods = fix_indentation( - methods, get_indents(lines, class_scope.get_start()) + - get_indent(pymodule.pycore.project)) - result = [] - result.append(source_code[:insertion_offset]) - result.append(indented_methods) - result.append(source_code[insertion_offset:]) - return ''.join(result) - - -def get_body(pyfunction): - """Return unindented function body""" - # FIXME scope = pyfunction.get_scope() - pymodule = pyfunction.get_module() - start, end = get_body_region(pyfunction) - return fix_indentation(pymodule.source_code[start:end], 0) - - -def get_body_region(defined): - """Return the start and end offsets of function body""" - scope = defined.get_scope() - pymodule = defined.get_module() - lines = pymodule.lines - node = defined.get_ast() - start_line = node.lineno - if defined.get_doc() is None: - start_line = node.body[0].lineno - elif len(node.body) > 1: - start_line = node.body[1].lineno - start = lines.get_line_start(start_line) - scope_start = pymodule.logical_lines.logical_line_in(scope.start) - if scope_start[1] >= start_line: - # a one-liner! - # XXX: what if colon appears in a string - start = pymodule.source_code.index(':', start) + 1 - while pymodule.source_code[start].isspace(): - start += 1 - end = min(lines.get_line_end(scope.end) + 1, len(pymodule.source_code)) - return start, end - - -def get_indent(project): - return project.prefs.get('indent_size', 4) diff --git a/pythonFiles/rope/refactor/suites.py b/pythonFiles/rope/refactor/suites.py deleted file mode 100644 index 6878508088a7..000000000000 --- a/pythonFiles/rope/refactor/suites.py +++ /dev/null @@ -1,158 +0,0 @@ -from rope.base import ast -from rope.base.utils import pycompat - - -def find_visible(node, lines): - """Return the line which is visible from all `lines`""" - root = ast_suite_tree(node) - return find_visible_for_suite(root, lines) - - -def find_visible_for_suite(root, lines): - if len(lines) == 1: - return lines[0] - line1 = lines[0] - line2 = find_visible_for_suite(root, lines[1:]) - suite1 = root.find_suite(line1) - suite2 = root.find_suite(line2) - - def valid(suite): - return suite is not None and not suite.ignored - if valid(suite1) and not valid(suite2): - return line1 - if not valid(suite1) and valid(suite2): - return line2 - if not valid(suite1) and not valid(suite2): - return None - while suite1 != suite2 and suite1.parent != suite2.parent: - if suite1._get_level() < suite2._get_level(): - line2 = suite2.get_start() - suite2 = suite2.parent - elif suite1._get_level() > suite2._get_level(): - line1 = suite1.get_start() - suite1 = suite1.parent - else: - line1 = suite1.get_start() - line2 = suite2.get_start() - suite1 = suite1.parent - suite2 = suite2.parent - if suite1 == suite2: - return min(line1, line2) - return min(suite1.get_start(), suite2.get_start()) - - -def ast_suite_tree(node): - if hasattr(node, 'lineno'): - lineno = node.lineno - else: - lineno = 1 - return Suite(node.body, lineno) - - -class Suite(object): - - def __init__(self, child_nodes, lineno, parent=None, ignored=False): - self.parent = parent - self.lineno = lineno - self.child_nodes = child_nodes - self._children = None - self.ignored = ignored - - def get_start(self): - if self.parent is None: - if self.child_nodes: - return self.local_start() - else: - return 1 - return self.lineno - - def get_children(self): - if self._children is None: - walker = _SuiteWalker(self) - for child in self.child_nodes: - ast.walk(child, walker) - self._children = walker.suites - return self._children - - def local_start(self): - return self.child_nodes[0].lineno - - def local_end(self): - end = self.child_nodes[-1].lineno - if self.get_children(): - end = max(end, self.get_children()[-1].local_end()) - return end - - def find_suite(self, line): - if line is None: - return None - for child in self.get_children(): - if child.local_start() <= line <= child.local_end(): - return child.find_suite(line) - return self - - def _get_level(self): - if self.parent is None: - return 0 - return self.parent._get_level() + 1 - - -class _SuiteWalker(object): - - def __init__(self, suite): - self.suite = suite - self.suites = [] - - def _If(self, node): - self._add_if_like_node(node) - - def _For(self, node): - self._add_if_like_node(node) - - def _While(self, node): - self._add_if_like_node(node) - - def _With(self, node): - self.suites.append(Suite(node.body, node.lineno, self.suite)) - - def _TryFinally(self, node): - proceed_to_except_handler = False - if len(node.finalbody) == 1: - if pycompat.PY2: - proceed_to_except_handler = isinstance(node.body[0], ast.TryExcept) - elif pycompat.PY3: - try: - proceed_to_except_handler = isinstance(node.handlers[0], ast.ExceptHandler) - except IndexError: - pass - if proceed_to_except_handler: - self._TryExcept(node if pycompat.PY3 else node.body[0]) - else: - self.suites.append(Suite(node.body, node.lineno, self.suite)) - self.suites.append(Suite(node.finalbody, node.lineno, self.suite)) - - def _Try(self, node): - if len(node.finalbody) == 1: - self._TryFinally(node) - else: - self._TryExcept(node) - - def _TryExcept(self, node): - self.suites.append(Suite(node.body, node.lineno, self.suite)) - for handler in node.handlers: - self.suites.append(Suite(handler.body, node.lineno, self.suite)) - if node.orelse: - self.suites.append(Suite(node.orelse, node.lineno, self.suite)) - - def _add_if_like_node(self, node): - self.suites.append(Suite(node.body, node.lineno, self.suite)) - if node.orelse: - self.suites.append(Suite(node.orelse, node.lineno, self.suite)) - - def _FunctionDef(self, node): - self.suites.append(Suite(node.body, node.lineno, - self.suite, ignored=True)) - - def _ClassDef(self, node): - self.suites.append(Suite(node.body, node.lineno, - self.suite, ignored=True)) diff --git a/pythonFiles/rope/refactor/topackage.py b/pythonFiles/rope/refactor/topackage.py deleted file mode 100644 index f36a6d528865..000000000000 --- a/pythonFiles/rope/refactor/topackage.py +++ /dev/null @@ -1,32 +0,0 @@ -import rope.refactor.importutils -from rope.base.change import ChangeSet, ChangeContents, MoveResource, \ - CreateFolder - - -class ModuleToPackage(object): - - def __init__(self, project, resource): - self.project = project - self.resource = resource - - def get_changes(self): - changes = ChangeSet('Transform <%s> module to package' % - self.resource.path) - new_content = self._transform_relatives_to_absolute(self.resource) - if new_content is not None: - changes.add_change(ChangeContents(self.resource, new_content)) - parent = self.resource.parent - name = self.resource.name[:-3] - changes.add_change(CreateFolder(parent, name)) - parent_path = parent.path + '/' - if not parent.path: - parent_path = '' - new_path = parent_path + '%s/__init__.py' % name - if self.resource.project == self.project: - changes.add_change(MoveResource(self.resource, new_path)) - return changes - - def _transform_relatives_to_absolute(self, resource): - pymodule = self.project.get_pymodule(resource) - import_tools = rope.refactor.importutils.ImportTools(self.project) - return import_tools.relatives_to_absolutes(pymodule) diff --git a/pythonFiles/rope/refactor/usefunction.py b/pythonFiles/rope/refactor/usefunction.py deleted file mode 100644 index 85896a98f775..000000000000 --- a/pythonFiles/rope/refactor/usefunction.py +++ /dev/null @@ -1,174 +0,0 @@ -from rope.base import (change, taskhandle, evaluate, - exceptions, pyobjects, pynames, ast) -from rope.base import libutils -from rope.refactor import restructure, sourceutils, similarfinder - - -class UseFunction(object): - """Try to use a function wherever possible""" - - def __init__(self, project, resource, offset): - self.project = project - self.offset = offset - this_pymodule = project.get_pymodule(resource) - pyname = evaluate.eval_location(this_pymodule, offset) - if pyname is None: - raise exceptions.RefactoringError('Unresolvable name selected') - self.pyfunction = pyname.get_object() - if not isinstance(self.pyfunction, pyobjects.PyFunction) or \ - not isinstance(self.pyfunction.parent, pyobjects.PyModule): - raise exceptions.RefactoringError( - 'Use function works for global functions, only.') - self.resource = self.pyfunction.get_module().get_resource() - self._check_returns() - - def _check_returns(self): - node = self.pyfunction.get_ast() - if _yield_count(node): - raise exceptions.RefactoringError('Use function should not ' - 'be used on generators.') - returns = _return_count(node) - if returns > 1: - raise exceptions.RefactoringError('usefunction: Function has more ' - 'than one return statement.') - if returns == 1 and not _returns_last(node): - raise exceptions.RefactoringError('usefunction: return should ' - 'be the last statement.') - - def get_changes(self, resources=None, - task_handle=taskhandle.NullTaskHandle()): - if resources is None: - resources = self.project.get_python_files() - changes = change.ChangeSet('Using function <%s>' % - self.pyfunction.get_name()) - if self.resource in resources: - newresources = list(resources) - newresources.remove(self.resource) - for c in self._restructure(newresources, task_handle).changes: - changes.add_change(c) - if self.resource in resources: - for c in self._restructure([self.resource], task_handle, - others=False).changes: - changes.add_change(c) - return changes - - def get_function_name(self): - return self.pyfunction.get_name() - - def _restructure(self, resources, task_handle, others=True): - pattern = self._make_pattern() - goal = self._make_goal(import_=others) - imports = None - if others: - imports = ['import %s' % self._module_name()] - - body_region = sourceutils.get_body_region(self.pyfunction) - args_value = {'skip': (self.resource, body_region)} - args = {'': args_value} - - restructuring = restructure.Restructure( - self.project, pattern, goal, args=args, imports=imports) - return restructuring.get_changes(resources=resources, - task_handle=task_handle) - - def _find_temps(self): - return find_temps(self.project, self._get_body()) - - def _module_name(self): - return libutils.modname(self.resource) - - def _make_pattern(self): - params = self.pyfunction.get_param_names() - body = self._get_body() - body = restructure.replace(body, 'return', 'pass') - wildcards = list(params) - wildcards.extend(self._find_temps()) - if self._does_return(): - if self._is_expression(): - replacement = '${%s}' % self._rope_returned - else: - replacement = '%s = ${%s}' % (self._rope_result, - self._rope_returned) - body = restructure.replace( - body, 'return ${%s}' % self._rope_returned, - replacement) - wildcards.append(self._rope_result) - return similarfinder.make_pattern(body, wildcards) - - def _get_body(self): - return sourceutils.get_body(self.pyfunction) - - def _make_goal(self, import_=False): - params = self.pyfunction.get_param_names() - function_name = self.pyfunction.get_name() - if import_: - function_name = self._module_name() + '.' + function_name - goal = '%s(%s)' % (function_name, - ', ' .join(('${%s}' % p) for p in params)) - if self._does_return() and not self._is_expression(): - goal = '${%s} = %s' % (self._rope_result, goal) - return goal - - def _does_return(self): - body = self._get_body() - removed_return = restructure.replace(body, 'return ${result}', '') - return removed_return != body - - def _is_expression(self): - return len(self.pyfunction.get_ast().body) == 1 - - _rope_result = '_rope__result' - _rope_returned = '_rope__returned' - - -def find_temps(project, code): - code = 'def f():\n' + sourceutils.indent_lines(code, 4) - pymodule = libutils.get_string_module(project, code) - result = [] - function_scope = pymodule.get_scope().get_scopes()[0] - for name, pyname in function_scope.get_names().items(): - if isinstance(pyname, pynames.AssignedName): - result.append(name) - return result - - -def _returns_last(node): - return node.body and isinstance(node.body[-1], ast.Return) - - -def _yield_count(node): - visitor = _ReturnOrYieldFinder() - visitor.start_walking(node) - return visitor.yields - - -def _return_count(node): - visitor = _ReturnOrYieldFinder() - visitor.start_walking(node) - return visitor.returns - - -class _ReturnOrYieldFinder(object): - - def __init__(self): - self.returns = 0 - self.yields = 0 - - def _Return(self, node): - self.returns += 1 - - def _Yield(self, node): - self.yields += 1 - - def _FunctionDef(self, node): - pass - - def _ClassDef(self, node): - pass - - def start_walking(self, node): - nodes = [node] - if isinstance(node, ast.FunctionDef): - nodes = ast.get_child_nodes(node) - for child in nodes: - ast.walk(child, self) diff --git a/pythonFiles/rope/refactor/wildcards.py b/pythonFiles/rope/refactor/wildcards.py deleted file mode 100644 index 90040c794e26..000000000000 --- a/pythonFiles/rope/refactor/wildcards.py +++ /dev/null @@ -1,178 +0,0 @@ -from rope.base import ast, evaluate, builtins, pyobjects -from rope.refactor import patchedast, occurrences - - -class Wildcard(object): - - def get_name(self): - """Return the name of this wildcard""" - - def matches(self, suspect, arg): - """Return `True` if `suspect` matches this wildcard""" - - -class Suspect(object): - - def __init__(self, pymodule, node, name): - self.name = name - self.pymodule = pymodule - self.node = node - - -class DefaultWildcard(object): - """The default restructuring wildcard - - The argument passed to this wildcard is in the - ``key1=value1,key2=value2,...`` format. Possible keys are: - - * name - for checking the reference - * type - for checking the type - * object - for checking the object - * instance - for checking types but similar to builtin isinstance - * exact - matching only occurrences with the same name as the wildcard - * unsure - matching unsure occurrences - - """ - - def __init__(self, project): - self.project = project - - def get_name(self): - return 'default' - - def matches(self, suspect, arg=''): - args = parse_arg(arg) - - if not self._check_exact(args, suspect): - return False - if not self._check_object(args, suspect): - return False - return True - - def _check_object(self, args, suspect): - kind = None - expected = None - unsure = args.get('unsure', False) - for check in ['name', 'object', 'type', 'instance']: - if check in args: - kind = check - expected = args[check] - if expected is not None: - checker = _CheckObject(self.project, expected, - kind, unsure=unsure) - return checker(suspect.pymodule, suspect.node) - return True - - def _check_exact(self, args, suspect): - node = suspect.node - if args.get('exact'): - if not isinstance(node, ast.Name) or not node.id == suspect.name: - return False - else: - if not isinstance(node, ast.expr): - return False - return True - - -def parse_arg(arg): - if isinstance(arg, dict): - return arg - result = {} - tokens = arg.split(',') - for token in tokens: - if '=' in token: - parts = token.split('=', 1) - result[parts[0].strip()] = parts[1].strip() - else: - result[token.strip()] = True - return result - - -class _CheckObject(object): - - def __init__(self, project, expected, kind='object', unsure=False): - self.project = project - self.kind = kind - self.unsure = unsure - self.expected = self._evaluate(expected) - - def __call__(self, pymodule, node): - pyname = self._evaluate_node(pymodule, node) - if pyname is None or self.expected is None: - return self.unsure - if self._unsure_pyname(pyname, unbound=self.kind == 'name'): - return True - if self.kind == 'name': - return self._same_pyname(self.expected, pyname) - else: - pyobject = pyname.get_object() - if self.kind == 'object': - objects = [pyobject] - if self.kind == 'type': - objects = [pyobject.get_type()] - if self.kind == 'instance': - objects = [pyobject] - objects.extend(self._get_super_classes(pyobject)) - objects.extend(self._get_super_classes(pyobject.get_type())) - for pyobject in objects: - if self._same_pyobject(self.expected.get_object(), pyobject): - return True - return False - - def _get_super_classes(self, pyobject): - result = [] - if isinstance(pyobject, pyobjects.AbstractClass): - for superclass in pyobject.get_superclasses(): - result.append(superclass) - result.extend(self._get_super_classes(superclass)) - return result - - def _same_pyobject(self, expected, pyobject): - return expected == pyobject - - def _same_pyname(self, expected, pyname): - return occurrences.same_pyname(expected, pyname) - - def _unsure_pyname(self, pyname, unbound=True): - return self.unsure and occurrences.unsure_pyname(pyname, unbound) - - def _split_name(self, name): - parts = name.split('.') - expression, kind = parts[0], parts[-1] - if len(parts) == 1: - kind = 'name' - return expression, kind - - def _evaluate_node(self, pymodule, node): - scope = pymodule.get_scope().get_inner_scope_for_line(node.lineno) - expression = node - if isinstance(expression, ast.Name) and \ - isinstance(expression.ctx, ast.Store): - start, end = patchedast.node_region(expression) - text = pymodule.source_code[start:end] - return evaluate.eval_str(scope, text) - else: - return evaluate.eval_node(scope, expression) - - def _evaluate(self, code): - attributes = code.split('.') - pyname = None - if attributes[0] in ('__builtin__', '__builtins__'): - class _BuiltinsStub(object): - def get_attribute(self, name): - return builtins.builtins[name] - - def __getitem__(self, name): - return builtins.builtins[name] - - def __contains__(self, name): - return name in builtins.builtins - pyobject = _BuiltinsStub() - else: - pyobject = self.project.get_module(attributes[0]) - for attribute in attributes[1:]: - pyname = pyobject[attribute] - if pyname is None: - return None - pyobject = pyname.get_object() - return pyname diff --git a/requirements.txt b/requirements.txt index b91b2a191669..6a4ea3463d4a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,6 @@ pydocstyle==1.0.0 jupyter ipython nose -pytest \ No newline at end of file +pytest +fabric +numba diff --git a/resources/ctagOptions b/resources/ctagOptions index b850134e0de6..3b656ac370fe 100644 --- a/resources/ctagOptions +++ b/resources/ctagOptions @@ -15,7 +15,6 @@ --exclude=**/*.jar --exclude=**/*.class --exclude=**/.idea/ ---exclude=**/site-packages/** --exclude=build --exclude=Builds --exclude=doc diff --git a/snippets/python.json b/snippets/python.json index f015069bb417..a2ba028d931d 100644 --- a/snippets/python.json +++ b/snippets/python.json @@ -2,26 +2,26 @@ "if": { "prefix": "if", "body": [ - "if ${expression}:", - "\t${pass}" + "if ${1:expression}:", + "\t${2:pass}" ], "description": "Code snippet for an if statement" }, "if/else": { "prefix": "if/else", "body": [ - "if ${condition}:", - "\t${1:pass}", + "if ${1:condition}:", + "\t${2:pass}", "else:", - "\t${2:pass}" + "\t${3:pass}" ], "description": "Code snippet for an if statement with else" }, "elif": { "prefix": "elif", "body": [ - "elif ${expression}:", - "\t${pass}" + "elif ${1:expression}:", + "\t${2:pass}" ], "description": "Code snippet for an elif" }, @@ -29,43 +29,43 @@ "prefix": "else", "body": [ "else:", - "\t${pass}" + "\t${1:pass}" ], "description": "Code snippet for an else" }, "while": { "prefix": "while", "body": [ - "while ${expression}:", - "\t${pass}" + "while ${1:expression}:", + "\t${2:pass}" ], "description": "Code snippet for a while loop" }, "while/else": { "prefix": "while/else", "body": [ - "while ${expression}:", - "\t${1:pass}", + "while ${1:expression}:", + "\t${2:pass}", "else:", - "\t${2:pass}" + "\t${3:pass}" ], "description": "Code snippet for a while loop with else" }, "for": { "prefix": "for", "body": [ - "for ${target_list} in ${expression_list}:", - "\t${pass}" + "for ${1:target_list} in ${2:expression_list}:", + "\t${3:pass}" ], "description": "Code snippet for a for loop" }, "for/else": { "prefix": "for/else", "body": [ - "for ${target_list} in ${expression_list}:", - "\t${1:pass}", + "for ${1:target_list} in ${2:expression_list}:", + "\t${3:pass}", "else:", - "\t${2:pass}" + "\t${4:pass}" ], "description": "Code snippet for a for loop with else" }, @@ -74,8 +74,8 @@ "body": [ "try:", "\t${1:pass}", - "except ${expression} as ${identifier}:", - "\t${2:pass}" + "except ${2:expression} as ${3:identifier}:", + "\t${4:pass}" ], "description": "Code snippet for a try/except statement" }, @@ -94,10 +94,10 @@ "body": [ "try:", "\t${1:pass}", - "except ${expression} as ${identifier}:", - "\t${2:pass}", + "except ${2:expression} as ${3:identifier}:", + "\t${4:pass}", "else:", - "\t${3:pass}" + "\t${5:pass}" ], "description": "Code snippet for a try/except/else statement" }, @@ -106,10 +106,10 @@ "body": [ "try:", "\t${1:pass}", - "except ${expression} as ${identifier}:", - "\t${2:pass}", + "except ${2:expression} as ${3:identifier}:", + "\t${4:pass}", "finally:", - "\t${3:pass}" + "\t${5:pass}" ], "description": "Code snippet for a try/except/finally statement" }, @@ -118,36 +118,36 @@ "body": [ "try:", "\t${1:pass}", - "except ${expression} as ${identifier}:", - "\t${2:pass}", + "except ${2:expression} as ${3:identifier}:", + "\t${4:pass}", "else:", - "\t${3:pass}", + "\t${5:pass}", "finally:", - "\t${4:pass}" + "\t${6:pass}" ], "description": "Code snippet for a try/except/else/finally statement" }, "with": { "prefix": "with", "body": [ - "with ${expression} as ${target}:", - "\t${pass}" + "with ${1:expression} as ${2:target}:", + "\t${3:pass}" ], "description": "Code snippet for a with statement" }, "def": { "prefix": "def", "body": [ - "def ${funcname}(${parameter_list}):", - "\t${pass}" + "def ${1:funcname}(${2:parameter_list}):", + "\t${3:pass}" ], "description": "Code snippet for a function definition" }, "def(class method)": { "prefix": "def(class method)", "body": [ - "def ${funcname}(self, ${parameter_list}):", - "\t${pass}" + "def ${1:funcname}(self, ${2:parameter_list}):", + "\t${3:pass}" ], "description": "Code snippet for a class method" }, @@ -155,15 +155,15 @@ "prefix": "def(static class method)", "body": [ "@staticmethod", - "def ${funcname}(${parameter_list}):", - "\t${pass}" + "def ${1:funcname}(${2:parameter_list}):", + "\t${3:pass}" ], "description": "Code snippet for a static class method" }, "def(abstract class method)": { "prefix": "def(abstract class method)", "body": [ - "def ${funcname}(self, ${parameter_list}):", + "def ${1:funcname}(self, ${2:parameter_list}):", "\traise NotImplementedError" ], "description": "Code snippet for an abstract class method" @@ -171,15 +171,15 @@ "class": { "prefix": "class", "body": [ - "class ${classname}(${object}):", - "\t${pass}" + "class ${1:classname}(${2:object}):", + "\t${3:pass}" ], "description": "Code snippet for a class definition" }, "lambda": { "prefix": "lambda", "body": [ - "lambda ${parameter_list}: ${expression}" + "lambda ${1:parameter_list}: ${2:expression}" ], "description": "Code snippet for a lambda statement" }, @@ -187,7 +187,7 @@ "prefix": "if(main)", "body": [ "def main():", - "\t${pass}", + "\t${1:pass}", "", "if __name__ == '__main__':", "\tmain()" @@ -197,37 +197,42 @@ "async/def": { "prefix": "async/def", "body": [ - "async def ${funcname}(${parameter_list}):", - "\t${pass}" + "async def ${1:funcname}(${2:parameter_list}):", + "\t${3:pass}" ], "description": "Code snippet for a async statement" }, "async/for": { "prefix": "async/for", "body": [ - "async for ${target} in ${iter}:", - "\t${block}" + "async for ${1:target} in ${2:iter}:", + "\t${3:block}" ], "description": "Code snippet for a async for statement" }, "async/for/else": { "prefix": "async/for/else", "body": [ - "async for ${target} in ${iter}:", - "\t${1:block}", + "async for ${1:target} in ${2:iter}:", + "\t${3:block}", "else:", - "\t${2:block}" + "\t${4:block}" ], "description": "Code snippet for a async for statement with else" }, "async/with": { "prefix": "async/with", "body": [ - "async with ${expr} in ${var}:", - "\t${block}" + "async with ${1:expr} as ${2:var}:", + "\t${3:block}" ], "description": "Code snippet for a async with statement" }, + "ipdb": { + "prefix": "ipdb", + "body": "import ipdb; ipdb.set_trace()", + "description": "Code snippet for ipdb debug" + }, "pdb": { "prefix": "pdb", "body": "import pdb; pdb.set_trace()", diff --git a/src/client/banner.ts b/src/client/banner.ts new file mode 100644 index 000000000000..097358e00ed9 --- /dev/null +++ b/src/client/banner.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as child_process from 'child_process'; +import * as os from 'os'; +import { window } from 'vscode'; +import { IPersistentStateFactory, PersistentState } from './common/persistentState'; + +const BANNER_URL = 'https://aka.ms/pvsc-at-msft'; + +export class BannerService { + private shouldShowBanner: PersistentState; + constructor(persistentStateFactory: IPersistentStateFactory) { + this.shouldShowBanner = persistentStateFactory.createGlobalPersistentState('SHOW_NEW_PUBLISHER_BANNER', true); + this.showBanner(); + } + private showBanner() { + if (!this.shouldShowBanner.value) { + return; + } + this.shouldShowBanner.value = false; + + const message = 'The Python extension is now published by Microsoft!'; + const yesButton = 'Read more'; + window.showInformationMessage(message, yesButton).then((value) => { + if (value === yesButton) { + this.displayBanner(); + } + }); + } + private displayBanner() { + let openCommand: string | undefined; + if (os.platform() === 'win32') { + openCommand = 'explorer'; + } else if (os.platform() === 'darwin') { + openCommand = '/usr/bin/open'; + } else { + openCommand = '/usr/bin/xdg-open'; + } + if (!openCommand) { + console.error(`Unable open ${BANNER_URL} on platform '${os.platform()}'.`); + } + child_process.spawn(openCommand, [BANNER_URL]); + } +} diff --git a/src/client/common/configSettingMonitor.ts b/src/client/common/configSettingMonitor.ts new file mode 100644 index 000000000000..fe73dee11b1e --- /dev/null +++ b/src/client/common/configSettingMonitor.ts @@ -0,0 +1,87 @@ +import { EventEmitter } from 'events'; +import { ConfigurationTarget, Disposable, Uri, workspace, WorkspaceFolder } from 'vscode'; +import { PythonSettings } from '../common/configSettings'; + +type settingsToMonitor = 'linting'; + +export class ConfigSettingMonitor extends EventEmitter implements Disposable { + private oldSettings = new Map(); + // tslint:disable-next-line:no-any + private timeout?: any; + constructor(private settingToMonitor: settingsToMonitor) { + super(); + this.initializeSettings(); + // tslint:disable-next-line:no-void-expression + PythonSettings.getInstance().on('change', () => this.onConfigChange()); + } + public dispose() { + if (this.timeout) { + // tslint:disable-next-line:no-unsafe-any + clearTimeout(this.timeout); + } + } + private onConfigChange() { + if (this.timeout) { + // tslint:disable-next-line:no-unsafe-any + clearTimeout(this.timeout); + } + this.timeout = setTimeout(() => { + this.timeout = undefined; + this.checkChangesToSettingsInWorkspace(); + this.checkChangesToSettingsInWorkspaceFolders(); + }, 1000); + } + private initializeSettings() { + if (!Array.isArray(workspace.workspaceFolders)) { + return; + } + if (workspace.workspaceFolders.length === 1) { + const key = this.getWorkspaceKey(); + const currentValue = JSON.stringify(PythonSettings.getInstance()[this.settingToMonitor]); + this.oldSettings.set(key, currentValue); + } else { + workspace.workspaceFolders.forEach(wkspaceFolder => { + const key = this.getWorkspaceFolderKey(wkspaceFolder.uri); + const currentValue = JSON.stringify(PythonSettings.getInstance(wkspaceFolder.uri)[this.settingToMonitor]); + this.oldSettings.set(key, currentValue); + }); + } + } + private checkChangesToSettingsInWorkspace() { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + return; + } + const newValue = JSON.stringify(PythonSettings.getInstance()[this.settingToMonitor]); + this.checkChangesAndNotifiy(ConfigurationTarget.Workspace, workspace.workspaceFolders[0].uri, newValue); + } + private checkChangesToSettingsInWorkspaceFolders() { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length <= 1) { + return; + } + // tslint:disable-next-line:no-void-expression + workspace.workspaceFolders.forEach(folder => this.checkChangesToSettingsInWorkspaceFolder(folder)); + } + private checkChangesToSettingsInWorkspaceFolder(workspaceFolder: WorkspaceFolder) { + const newValue = JSON.stringify(PythonSettings.getInstance(workspaceFolder.uri)[this.settingToMonitor]); + this.checkChangesAndNotifiy(ConfigurationTarget.WorkspaceFolder, workspaceFolder.uri, newValue); + } + private checkChangesAndNotifiy(configTarget: ConfigurationTarget, uri: Uri, newValue: string) { + const key = configTarget === ConfigurationTarget.Workspace ? this.getWorkspaceKey() : this.getWorkspaceFolderKey(uri); + if (this.oldSettings.has(key)) { + const oldValue = this.oldSettings.get(key); + if (oldValue !== newValue) { + this.oldSettings.set(key, newValue); + this.emit('change', configTarget, uri); + } + } else { + this.oldSettings.set(key, newValue); + } + } + private getWorkspaceKey() { + // tslint:disable-next-line:no-non-null-assertion + return workspace.workspaceFolders[0]!.uri.fsPath; + } + private getWorkspaceFolderKey(wkspaceFolder: Uri) { + return `${ConfigurationTarget.WorkspaceFolder}:${wkspaceFolder.fsPath}`; + } +} diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 7f91c712a589..797e644d3b3f 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -1,15 +1,21 @@ 'use strict'; -import * as vscode from 'vscode'; -import { SystemVariables } from './systemVariables'; +import * as child_process from 'child_process'; import { EventEmitter } from 'events'; import * as path from 'path'; -import * as child_process from 'child_process'; +import * as vscode from 'vscode'; +import { Uri } from 'vscode'; +import { InterpreterInfoCache } from './interpreterInfoCache'; +import { SystemVariables } from './systemVariables'; + +// tslint:disable-next-line:no-require-imports no-var-requires +const untildify = require('untildify'); export const IS_WINDOWS = /^win/.test(process.platform); export interface IPythonSettings { pythonPath: string; + venvPath: string; jediPath: string; devOptions: string[]; linting: ILintingSettings; @@ -20,9 +26,11 @@ export interface IPythonSettings { jupyter: JupyterSettings; sortImports: ISortImportSettings; workspaceSymbols: IWorkspaceSymbolSettings; + envFile: string; + disablePromptForFeatures: string[]; } - export interface ISortImportSettings { + path: string; args: string[]; } @@ -38,6 +46,7 @@ export interface IUnitTestSettings { unittestEnabled: boolean; unittestArgs: string[]; outputWindow: string; + cwd?: string; } export interface IPylintCategorySeverity { convention: vscode.DiagnosticSeverity; @@ -46,8 +55,23 @@ export interface IPylintCategorySeverity { error: vscode.DiagnosticSeverity; fatal: vscode.DiagnosticSeverity; } +export interface IPep8CategorySeverity { + W: vscode.DiagnosticSeverity; + E: vscode.DiagnosticSeverity; +} +// tslint:disable-next-line:interface-name +export interface Flake8CategorySeverity { + F: vscode.DiagnosticSeverity; + E: vscode.DiagnosticSeverity; + W: vscode.DiagnosticSeverity; +} +export interface IMypyCategorySeverity { + error: vscode.DiagnosticSeverity; + note: vscode.DiagnosticSeverity; +} export interface ILintingSettings { enabled: boolean; + enabledWithoutWorkspace: boolean; ignorePatterns: string[]; prospectorEnabled: boolean; prospectorArgs: string[]; @@ -65,6 +89,9 @@ export interface ILintingSettings { lintOnSave: boolean; maxNumberOfProblems: number; pylintCategorySeverity: IPylintCategorySeverity; + pep8CategorySeverity: IPep8CategorySeverity; + flake8CategorySeverity: Flake8CategorySeverity; + mypyCategorySeverity: IMypyCategorySeverity; prospectorPath: string; pylintPath: string; pep8Path: string; @@ -88,6 +115,7 @@ export interface IFormattingSettings { export interface IAutoCompeteSettings { addBrackets: boolean; extraPaths: string[]; + preloadModules: string[]; } export interface IWorkspaceSymbolSettings { enabled: boolean; @@ -101,65 +129,126 @@ export interface ITerminalSettings { executeInFileDir: boolean; launchArgs: string[]; } +// tslint:disable-next-line:interface-name export interface JupyterSettings { appendResults: boolean; defaultKernel: string; startupCode: string[]; } -const IS_TEST_EXECUTION = process.env['PYTHON_DONJAYAMANNE_TEST'] === '1'; +// tslint:disable-next-line:no-string-literal +const IS_TEST_EXECUTION = process.env['VSC_PYTHON_CI_TEST'] === '1'; +// tslint:disable-next-line:completed-docs export class PythonSettings extends EventEmitter implements IPythonSettings { - private static pythonSettings: PythonSettings = new PythonSettings(); + private static pythonSettings: Map = new Map(); + + public jediPath: string; + public envFile: string; + public disablePromptForFeatures: string[]; + public venvPath: string; + public devOptions: string[]; + public linting: ILintingSettings; + public formatting: IFormattingSettings; + public autoComplete: IAutoCompeteSettings; + public unitTest: IUnitTestSettings; + public terminal: ITerminalSettings; + public jupyter: JupyterSettings; + public sortImports: ISortImportSettings; + public workspaceSymbols: IWorkspaceSymbolSettings; + + private workspaceRoot: vscode.Uri; private disposables: vscode.Disposable[] = []; - constructor() { + // tslint:disable-next-line:variable-name + private _pythonPath: string; + + constructor(workspaceFolder?: Uri) { super(); - if (PythonSettings.pythonSettings) { - throw new Error('Singleton class, Use getInstance method'); - } + this.workspaceRoot = workspaceFolder ? workspaceFolder : vscode.Uri.file(__dirname); this.disposables.push(vscode.workspace.onDidChangeConfiguration(() => { this.initializeSettings(); })); this.initializeSettings(); } - public static getInstance(): PythonSettings { - return PythonSettings.pythonSettings; + // tslint:disable-next-line:function-name + public static getInstance(resource?: Uri): PythonSettings { + const workspaceFolder = resource ? vscode.workspace.getWorkspaceFolder(resource) : undefined; + let workspaceFolderUri: Uri | undefined = workspaceFolder ? workspaceFolder.uri : undefined; + if (!workspaceFolderUri && Array.isArray(vscode.workspace.workspaceFolders) && vscode.workspace.workspaceFolders.length > 0) { + workspaceFolderUri = vscode.workspace.workspaceFolders[0].uri; + } + const workspaceFolderKey = workspaceFolderUri ? workspaceFolderUri.fsPath : ''; + if (!PythonSettings.pythonSettings.has(workspaceFolderKey)) { + const settings = new PythonSettings(workspaceFolderUri); + PythonSettings.pythonSettings.set(workspaceFolderKey, settings); + } + // tslint:disable-next-line:no-non-null-assertion + return PythonSettings.pythonSettings.get(workspaceFolderKey)!; + } + // tslint:disable-next-line:function-name + public static dispose() { + if (!IS_TEST_EXECUTION) { + throw new Error('Dispose can only be called from unit tests'); + } + // tslint:disable-next-line:no-void-expression + PythonSettings.pythonSettings.forEach(item => item.dispose()); + PythonSettings.pythonSettings.clear(); } + public dispose() { + // tslint:disable-next-line:no-unsafe-any + this.disposables.forEach(disposable => disposable.dispose()); + this.disposables = []; + InterpreterInfoCache.clear(); + } + + // tslint:disable-next-line:cyclomatic-complexity max-func-body-length private initializeSettings() { - const systemVariables: SystemVariables = new SystemVariables(); - const workspaceRoot = (IS_TEST_EXECUTION || typeof vscode.workspace.rootPath !== 'string') ? __dirname : vscode.workspace.rootPath; - let pythonSettings = vscode.workspace.getConfiguration('python'); - this.pythonPath = systemVariables.resolveAny(pythonSettings.get('pythonPath')); - this.pythonPath = getAbsolutePath(this.pythonPath, IS_TEST_EXECUTION ? __dirname : workspaceRoot); - this.jediPath = systemVariables.resolveAny(pythonSettings.get('jediPath')); + InterpreterInfoCache.clear(); + const workspaceRoot = this.workspaceRoot.fsPath; + const systemVariables: SystemVariables = new SystemVariables(this.workspaceRoot ? this.workspaceRoot.fsPath : undefined); + const pythonSettings = vscode.workspace.getConfiguration('python', this.workspaceRoot); + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + this.pythonPath = systemVariables.resolveAny(pythonSettings.get('pythonPath'))!; + this.pythonPath = getAbsolutePath(this.pythonPath, workspaceRoot); + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + this.venvPath = systemVariables.resolveAny(pythonSettings.get('venvPath'))!; + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + this.jediPath = systemVariables.resolveAny(pythonSettings.get('jediPath'))!; if (typeof this.jediPath === 'string' && this.jediPath.length > 0) { - this.jediPath = getAbsolutePath(this.jediPath, IS_TEST_EXECUTION ? __dirname : workspaceRoot); - } - else { + this.jediPath = getAbsolutePath(systemVariables.resolveAny(this.jediPath), workspaceRoot); + } else { this.jediPath = ''; } - this.devOptions = systemVariables.resolveAny(pythonSettings.get('devOptions')); + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + this.envFile = systemVariables.resolveAny(pythonSettings.get('envFile'))!; + // tslint:disable-next-line:no-any + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion no-any + this.devOptions = systemVariables.resolveAny(pythonSettings.get('devOptions'))!; this.devOptions = Array.isArray(this.devOptions) ? this.devOptions : []; - let lintingSettings = systemVariables.resolveAny(pythonSettings.get('linting')); + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const lintingSettings = systemVariables.resolveAny(pythonSettings.get('linting'))!; + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + this.disablePromptForFeatures = pythonSettings.get('disablePromptForFeatures')!; + this.disablePromptForFeatures = Array.isArray(this.disablePromptForFeatures) ? this.disablePromptForFeatures : []; if (this.linting) { Object.assign(this.linting, lintingSettings); - } - else { + } else { this.linting = lintingSettings; } - let sortImportSettings = systemVariables.resolveAny(pythonSettings.get('sortImports')); + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const sortImportSettings = systemVariables.resolveAny(pythonSettings.get('sortImports'))!; if (this.sortImports) { Object.assign(this.sortImports, sortImportSettings); - } - else { + } else { this.sortImports = sortImportSettings; } - // Support for travis - this.sortImports = this.sortImports ? this.sortImports : { args: [] }; - // Support for travis + // Support for travis. + this.sortImports = this.sortImports ? this.sortImports : { path: '', args: [] }; + // Support for travis. this.linting = this.linting ? this.linting : { enabled: false, + enabledWithoutWorkspace: false, ignorePatterns: [], flake8Args: [], flake8Enabled: false, flake8Path: 'flake', lintOnSave: false, lintOnTextChange: false, maxNumberOfProblems: 100, @@ -175,23 +264,37 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { fatal: vscode.DiagnosticSeverity.Error, refactor: vscode.DiagnosticSeverity.Hint, warning: vscode.DiagnosticSeverity.Warning + }, + pep8CategorySeverity: { + E: vscode.DiagnosticSeverity.Error, + W: vscode.DiagnosticSeverity.Warning + }, + flake8CategorySeverity: { + F: vscode.DiagnosticSeverity.Error, + E: vscode.DiagnosticSeverity.Error, + W: vscode.DiagnosticSeverity.Warning + }, + mypyCategorySeverity: { + error: vscode.DiagnosticSeverity.Error, + note: vscode.DiagnosticSeverity.Hint } }; - this.linting.pylintPath = getAbsolutePath(this.linting.pylintPath, workspaceRoot); - this.linting.flake8Path = getAbsolutePath(this.linting.flake8Path, workspaceRoot); - this.linting.pep8Path = getAbsolutePath(this.linting.pep8Path, workspaceRoot); - this.linting.pylamaPath = getAbsolutePath(this.linting.pylamaPath, workspaceRoot); - this.linting.prospectorPath = getAbsolutePath(this.linting.prospectorPath, workspaceRoot); - this.linting.pydocstylePath = getAbsolutePath(this.linting.pydocstylePath, workspaceRoot); + this.linting.pylintPath = getAbsolutePath(systemVariables.resolveAny(this.linting.pylintPath), workspaceRoot); + this.linting.flake8Path = getAbsolutePath(systemVariables.resolveAny(this.linting.flake8Path), workspaceRoot); + this.linting.pep8Path = getAbsolutePath(systemVariables.resolveAny(this.linting.pep8Path), workspaceRoot); + this.linting.pylamaPath = getAbsolutePath(systemVariables.resolveAny(this.linting.pylamaPath), workspaceRoot); + this.linting.prospectorPath = getAbsolutePath(systemVariables.resolveAny(this.linting.prospectorPath), workspaceRoot); + this.linting.pydocstylePath = getAbsolutePath(systemVariables.resolveAny(this.linting.pydocstylePath), workspaceRoot); + this.linting.mypyPath = getAbsolutePath(systemVariables.resolveAny(this.linting.mypyPath), workspaceRoot); - let formattingSettings = systemVariables.resolveAny(pythonSettings.get('formatting')); + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const formattingSettings = systemVariables.resolveAny(pythonSettings.get('formatting'))!; if (this.formatting) { Object.assign(this.formatting, formattingSettings); - } - else { + } else { this.formatting = formattingSettings; } - // Support for travis + // Support for travis. this.formatting = this.formatting ? this.formatting : { autopep8Args: [], autopep8Path: 'autopep8', outputWindow: 'python', @@ -199,51 +302,59 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { yapfArgs: [], yapfPath: 'yapf', formatOnSave: false }; - this.formatting.autopep8Path = getAbsolutePath(this.formatting.autopep8Path, workspaceRoot); - this.formatting.yapfPath = getAbsolutePath(this.formatting.yapfPath, workspaceRoot); + this.formatting.autopep8Path = getAbsolutePath(systemVariables.resolveAny(this.formatting.autopep8Path), workspaceRoot); + this.formatting.yapfPath = getAbsolutePath(systemVariables.resolveAny(this.formatting.yapfPath), workspaceRoot); - let autoCompleteSettings = systemVariables.resolveAny(pythonSettings.get('autoComplete')); + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const autoCompleteSettings = systemVariables.resolveAny(pythonSettings.get('autoComplete'))!; if (this.autoComplete) { Object.assign(this.autoComplete, autoCompleteSettings); - } - else { + } else { this.autoComplete = autoCompleteSettings; } - // Support for travis + // Support for travis. this.autoComplete = this.autoComplete ? this.autoComplete : { extraPaths: [], - addBrackets: false + addBrackets: false, + preloadModules: [] }; - let workspaceSymbolsSettings = systemVariables.resolveAny(pythonSettings.get('workspaceSymbols')); + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const workspaceSymbolsSettings = systemVariables.resolveAny(pythonSettings.get('workspaceSymbols'))!; if (this.workspaceSymbols) { Object.assign(this.workspaceSymbols, workspaceSymbolsSettings); - } - else { + } else { this.workspaceSymbols = workspaceSymbolsSettings; } - // Support for travis + // Support for travis. this.workspaceSymbols = this.workspaceSymbols ? this.workspaceSymbols : { ctagsPath: 'ctags', enabled: true, exclusionPatterns: [], rebuildOnFileSave: true, rebuildOnStart: true, - tagFilePath: path.join(workspaceRoot, "tags") + tagFilePath: path.join(workspaceRoot, 'tags') }; + this.workspaceSymbols.tagFilePath = getAbsolutePath(systemVariables.resolveAny(this.workspaceSymbols.tagFilePath), workspaceRoot); - let unitTestSettings = systemVariables.resolveAny(pythonSettings.get('unitTest')); + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const unitTestSettings = systemVariables.resolveAny(pythonSettings.get('unitTest'))!; if (this.unitTest) { Object.assign(this.unitTest, unitTestSettings); - } - else { + } else { this.unitTest = unitTestSettings; if (IS_TEST_EXECUTION && !this.unitTest) { - this.unitTest = { nosetestArgs: [], pyTestArgs: [], unittestArgs: [] } as IUnitTestSettings; + // tslint:disable-next-line:prefer-type-cast + this.unitTest = { + nosetestArgs: [], pyTestArgs: [], unittestArgs: [], + promptToConfigure: true, debugPort: 3000, + nosetestsEnabled: false, pyTestEnabled: false, unittestEnabled: false, + nosetestPath: 'nosetests', pyTestPath: 'py.test', outputWindow: 'Python Test Log' + } as IUnitTestSettings; } } - // Support for travis + // Support for travis. this.unitTest = this.unitTest ? this.unitTest : { promptToConfigure: true, debugPort: 3000, @@ -252,21 +363,25 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { pyTestArgs: [], pyTestEnabled: false, pyTestPath: 'pytest', unittestArgs: [], unittestEnabled: false }; - this.unitTest.pyTestPath = getAbsolutePath(this.unitTest.pyTestPath, workspaceRoot); - this.unitTest.nosetestPath = getAbsolutePath(this.unitTest.nosetestPath, workspaceRoot); + this.unitTest.pyTestPath = getAbsolutePath(systemVariables.resolveAny(this.unitTest.pyTestPath), workspaceRoot); + this.unitTest.nosetestPath = getAbsolutePath(systemVariables.resolveAny(this.unitTest.nosetestPath), workspaceRoot); + if (this.unitTest.cwd) { + this.unitTest.cwd = getAbsolutePath(systemVariables.resolveAny(this.unitTest.cwd), workspaceRoot); + } - // Resolve any variables found in the test arguments + // Resolve any variables found in the test arguments. this.unitTest.nosetestArgs = this.unitTest.nosetestArgs.map(arg => systemVariables.resolveAny(arg)); this.unitTest.pyTestArgs = this.unitTest.pyTestArgs.map(arg => systemVariables.resolveAny(arg)); this.unitTest.unittestArgs = this.unitTest.unittestArgs.map(arg => systemVariables.resolveAny(arg)); - let terminalSettings = systemVariables.resolveAny(pythonSettings.get('terminal')); + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const terminalSettings = systemVariables.resolveAny(pythonSettings.get('terminal'))!; if (this.terminal) { Object.assign(this.terminal, terminalSettings); - } - else { + } else { this.terminal = terminalSettings; if (IS_TEST_EXECUTION && !this.terminal) { + // tslint:disable-next-line:prefer-type-cast this.terminal = {} as ITerminalSettings; } } @@ -276,16 +391,18 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { launchArgs: [] }; - this.jupyter = pythonSettings.get('jupyter'); - // Support for travis + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + this.jupyter = pythonSettings.get('jupyter')!; + // Support for travis. this.jupyter = this.jupyter ? this.jupyter : { appendResults: true, defaultKernel: '', startupCode: [] }; - this.emit('change'); + // If workspace config changes, then we could have a cascading effect of on change events. + // Let's defer the change notification. + setTimeout(() => this.emit('change'), 1); } - private _pythonPath: string; public get pythonPath(): string { return this._pythonPath; } @@ -293,28 +410,19 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { if (this._pythonPath === value) { return; } - // Add support for specifying just the directory where the python executable will be located - // E.g. virtual directory name + // Add support for specifying just the directory where the python executable will be located. + // E.g. virtual directory name. try { this._pythonPath = getPythonExecutable(value); - } - catch (ex) { + } catch (ex) { this._pythonPath = value; } } - public jediPath: string; - public devOptions: string[]; - public linting: ILintingSettings; - public formatting: IFormattingSettings; - public autoComplete: IAutoCompeteSettings; - public unitTest: IUnitTestSettings; - public terminal: ITerminalSettings; - public jupyter: JupyterSettings; - public sortImports: ISortImportSettings; - public workspaceSymbols: IWorkspaceSymbolSettings; } function getAbsolutePath(pathToCheck: string, rootDir: string): string { + // tslint:disable-next-line:prefer-type-cast no-unsafe-any + pathToCheck = untildify(pathToCheck) as string; if (IS_TEST_EXECUTION && !pathToCheck) { return rootDir; } if (pathToCheck.indexOf(path.sep) === -1) { return pathToCheck; @@ -323,7 +431,10 @@ function getAbsolutePath(pathToCheck: string, rootDir: string): string { } function getPythonExecutable(pythonPath: string): string { - // If only 'python' + // tslint:disable-next-line:prefer-type-cast no-unsafe-any + pythonPath = untildify(pythonPath) as string; + + // If only 'python'. if (pythonPath === 'python' || pythonPath.indexOf(path.sep) === -1 || path.basename(pythonPath) === path.dirname(pythonPath)) { @@ -333,34 +444,38 @@ function getPythonExecutable(pythonPath: string): string { if (isValidPythonPath(pythonPath)) { return pythonPath; } + // Keep python right on top, for backwards compatibility. + // tslint:disable-next-line:variable-name + const KnownPythonExecutables = ['python', 'python4', 'python3.6', 'python3.5', 'python3', 'python2.7', 'python2']; - // Suffix with 'python' for linux and 'osx', and 'python.exe' for 'windows' - if (IS_WINDOWS) { - if (isValidPythonPath(path.join(pythonPath, 'python.exe'))) { - return path.join(pythonPath, 'python.exe'); - } - if (isValidPythonPath(path.join(pythonPath, 'scripts', 'python.exe'))) { - return path.join(pythonPath, 'scripts', 'python.exe'); - } - } - else { - if (isValidPythonPath(path.join(pythonPath, 'python'))) { - return path.join(pythonPath, 'python'); - } - if (isValidPythonPath(path.join(pythonPath, 'bin', 'python'))) { - return path.join(pythonPath, 'bin', 'python'); + for (let executableName of KnownPythonExecutables) { + // Suffix with 'python' for linux and 'osx', and 'python.exe' for 'windows'. + if (IS_WINDOWS) { + executableName = `${executableName}.exe`; + if (isValidPythonPath(path.join(pythonPath, executableName))) { + return path.join(pythonPath, executableName); + } + if (isValidPythonPath(path.join(pythonPath, 'scripts', executableName))) { + return path.join(pythonPath, 'scripts', executableName); + } + } else { + if (isValidPythonPath(path.join(pythonPath, executableName))) { + return path.join(pythonPath, executableName); + } + if (isValidPythonPath(path.join(pythonPath, 'bin', executableName))) { + return path.join(pythonPath, 'bin', executableName); + } } } return pythonPath; } -function isValidPythonPath(pythonPath): boolean { +function isValidPythonPath(pythonPath: string): boolean { try { - let output = child_process.execFileSync(pythonPath, ['-c', 'print(1234)'], { encoding: 'utf8' }); + const output = child_process.execFileSync(pythonPath, ['-c', 'print(1234)'], { encoding: 'utf8' }); return output.startsWith('1234'); - } - catch (ex) { + } catch (ex) { return false; } -} \ No newline at end of file +} diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index 16bfe28e93da..c332257d1170 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -1,10 +1,11 @@ -export const PythonLanguage = { language: 'python', scheme: 'file' }; +export const PythonLanguage = { language: 'python' }; export namespace Commands { export const Set_Interpreter = 'python.setInterpreter'; export const Exec_In_Terminal = 'python.execInTerminal'; export const Exec_Selection_In_Terminal = 'python.execSelectionInTerminal'; + export const Exec_Selection_In_Django_Shell = 'python.execSelectionInDjangoShell'; export const Tests_View_UI = 'python.viewTestUI'; export const Tests_Picker_UI = 'python.selectTestToRun'; export const Tests_Picker_UI_Debug = 'python.selectTestToDebug'; @@ -19,32 +20,34 @@ export namespace Commands { export const Tests_ViewOutput = 'python.viewTestOutput'; export const Tests_Select_And_Run_Method = 'python.selectAndRunTestMethod'; export const Tests_Select_And_Debug_Method = 'python.selectAndDebugTestMethod'; + export const Tests_Select_And_Run_File = 'python.selectAndRunTestFile'; + export const Tests_Run_Current_File = 'python.runCurrentTestFile'; export const Refactor_Extract_Variable = 'python.refactorExtractVariable'; export const Refaactor_Extract_Method = 'python.refactorExtractMethod'; export const Update_SparkLibrary = 'python.updateSparkLibrary'; export const Build_Workspace_Symbols = 'python.buildWorkspaceSymbols'; export const Start_REPL = 'python.startREPL'; export namespace Jupyter { - export const Get_All_KernelSpecs_For_Language = 'jupyter:getAllKernelSpecsForLanguage'; - export const Get_All_KernelSpecs = 'jupyter:getAllKernelSpecs'; - export const Select_Kernel = 'jupyter:selectKernel'; - export const Kernel_Options = 'jupyter:kernelOptions'; - export const StartKernelForKernelSpeck = 'jupyter:sartKernelForKernelSpecs'; - export const ExecuteRangeInKernel = 'jupyter:execRangeInKernel'; + export const Get_All_KernelSpecs_For_Language = 'jupyter.getAllKernelSpecsForLanguage'; + export const Get_All_KernelSpecs = 'jupyter.getAllKernelSpecs'; + export const Select_Kernel = 'jupyter.selectKernel'; + export const Kernel_Options = 'jupyter.kernelOptions'; + export const StartKernelForKernelSpeck = 'jupyter.sartKernelForKernelSpecs'; + export const ExecuteRangeInKernel = 'jupyter.execRangeInKernel'; export const ExecuteSelectionOrLineInKernel = 'jupyter.runSelectionLine'; export namespace Cell { - export const ExecuteCurrentCell = 'jupyter:execCurrentCell'; - export const ExecuteCurrentCellAndAdvance = 'jupyter:execCurrentCellAndAdvance'; - export const AdcanceToCell = 'jupyter:advanceToNextCell'; - export const DisplayCellMenu = 'jupyter:displayCellMenu'; - export const GoToPreviousCell = 'jupyter:gotToPreviousCell'; - export const GoToNextCell = 'jupyter:gotToNextCell'; + export const ExecuteCurrentCell = 'jupyter.execCurrentCell'; + export const ExecuteCurrentCellAndAdvance = 'jupyter.execCurrentCellAndAdvance'; + export const AdcanceToCell = 'jupyter.advanceToNextCell'; + export const DisplayCellMenu = 'jupyter.displayCellMenu'; + export const GoToPreviousCell = 'jupyter.gotToPreviousCell'; + export const GoToNextCell = 'jupyter.gotToNextCell'; } export namespace Kernel { - export const Kernel_Interrupt = 'jupyter:kernelInterrupt'; - export const Kernel_Restart = 'jupyter:kernelRestart'; - export const Kernel_Shut_Down = 'jupyter:kernelShutDown'; - export const Kernel_Details = 'jupyter:kernelDetails'; + export const Kernel_Interrupt = 'jupyter.kernelInterrupt'; + export const Kernel_Restart = 'jupyter.kernelRestart'; + export const Kernel_Shut_Down = 'jupyter.kernelShutDown'; + export const Kernel_Details = 'jupyter.kernelDetails'; } } } @@ -77,20 +80,3 @@ export namespace LinterErrors { export const InvalidSyntax = 'E999'; } } - -export namespace Documentation { - export const Home = '/docs/python-path/'; - export namespace Jupyter { - export const GettingStarted = '/docs/jupyter_getting-started/'; - export const Examples = '/docs/jupyter_examples/'; - export const Setup = '/docs/jupyter_prerequisites/'; - export const VersionIncompatiblity = '/docs/troubleshooting_jupyter/#Incompatible-dependencies'; - } - export namespace Formatting { - export const FormatOnSave = '/docs/formatting/'; - } - export namespace Workspace { - export const Home = '/docs/workspaceSymbols/'; - export const InstallOnWindows = '/docs/workspaceSymbols/#Install-Windows'; - } -} \ No newline at end of file diff --git a/src/client/common/contextKey.ts b/src/client/common/contextKey.ts new file mode 100644 index 000000000000..87fe57f07d34 --- /dev/null +++ b/src/client/common/contextKey.ts @@ -0,0 +1,15 @@ +import { commands } from 'vscode'; + +export class ContextKey { + private lastValue: boolean; + + constructor(private name: string) { } + + public async set(value: boolean): Promise { + if (this.lastValue === value) { + return; + } + this.lastValue = value; + await commands.executeCommand('setContext', this.name, this.lastValue); + } +} diff --git a/src/client/common/editor.ts b/src/client/common/editor.ts index 9d23d38ff678..7da7a2657e8b 100644 --- a/src/client/common/editor.ts +++ b/src/client/common/editor.ts @@ -75,7 +75,7 @@ export function getTextEditsFromPatch(before: string, patch: string): TextEdit[] return textEdits; } -export function getWorkspaceEditsFromPatch(filePatches: string[]): WorkspaceEdit { +export function getWorkspaceEditsFromPatch(filePatches: string[], workspaceRoot?:string): WorkspaceEdit { const workspaceEdit = new WorkspaceEdit(); filePatches.forEach(patch => { const indexOfAtAt = patch.indexOf('@@'); @@ -101,7 +101,7 @@ export function getWorkspaceEditsFromPatch(filePatches: string[]): WorkspaceEdit } let fileName = fileNameLines[0].substring(fileNameLines[0].indexOf(' a') + 3).trim(); - fileName = path.isAbsolute(fileName) ? fileName : path.resolve(vscode.workspace.rootPath, fileName); + fileName = workspaceRoot && !path.isAbsolute(fileName) ? path.resolve(workspaceRoot, fileName) : fileName; if (!fs.existsSync(fileName)) { return; } diff --git a/src/client/common/enumUtils.ts b/src/client/common/enumUtils.ts new file mode 100644 index 000000000000..59736d799f80 --- /dev/null +++ b/src/client/common/enumUtils.ts @@ -0,0 +1,17 @@ +export class EnumEx { + static getNamesAndValues(e: any) { + return EnumEx.getNames(e).map(n => ({ name: n, value: e[n] as T })); + } + + static getNames(e: any) { + return EnumEx.getObjValues(e).filter(v => typeof v === "string") as string[]; + } + + static getValues(e: any) { + return EnumEx.getObjValues(e).filter(v => typeof v === "number") as T[]; + } + + private static getObjValues(e: any): (number | string)[] { + return Object.keys(e).map(k => e[k]); + } +} diff --git a/src/client/common/envFileParser.ts b/src/client/common/envFileParser.ts new file mode 100644 index 000000000000..9322742bec7a --- /dev/null +++ b/src/client/common/envFileParser.ts @@ -0,0 +1,41 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +export function parseEnvFile(envFile: string): any { + const buffer = fs.readFileSync(envFile, 'utf8'); + const env = {}; + buffer.split('\n').forEach(line => { + const r = line.match(/^\s*([\w\.\-]+)\s*=\s*(.*)?\s*$/); + if (r !== null) { + let value = r[2] || ''; + if (value.length > 0 && value.charAt(0) === '"' && value.charAt(value.length - 1) === '"') { + value = value.replace(/\\n/gm, '\n'); + } + env[r[1]] = value.replace(/(^['"]|['"]$)/g, ''); + } + }); + return mergeEnvVariables(env); +} + +export function mergeEnvVariables(newVariables: { [key: string]: string }, mergeWith: any = process.env): any { + for (let setting in mergeWith) { + if (setting === 'PYTHONPATH') { + let PYTHONPATH: string = newVariables['PYTHONPATH']; + if (typeof PYTHONPATH !== 'string') { + PYTHONPATH = ''; + } + if (mergeWith['PYTHONPATH']) { + PYTHONPATH += (PYTHONPATH.length > 0 ? path.delimiter : '') + mergeWith['PYTHONPATH']; + } + if (PYTHONPATH.length > 0) { + newVariables[setting] = PYTHONPATH; + } + continue; + } + if (!newVariables[setting]) { + newVariables[setting] = mergeWith[setting]; + } + } + + return newVariables; +} diff --git a/src/client/common/helpers.ts b/src/client/common/helpers.ts index f191ef05bfc1..47e88d03e24f 100644 --- a/src/client/common/helpers.ts +++ b/src/client/common/helpers.ts @@ -1,7 +1,13 @@ const tmp = require('tmp'); export function isNotInstalledError(error: Error): boolean { - return typeof (error) === 'object' && error !== null && ((error).code === 'ENOENT' || (error).code === 127); + const isError = typeof (error) === 'object' && error !== null; + const errorObj = error; + if (!isError) { + return false; + } + const isModuleNoInstalledError = errorObj.code === 1 && error.message.indexOf('No module named') >= 0; + return errorObj.code === 'ENOENT' || errorObj.code === 127 || isModuleNoInstalledError; } export interface Deferred { @@ -29,7 +35,7 @@ class DeferredImpl implements Deferred { this._resolve.apply(this.scope ? this.scope : this, arguments); this._resolved = true; } - reject(reason?: any){ + reject(reason?: any) { this._reject.apply(this.scope ? this.scope : this, arguments); this._rejected = true; } diff --git a/src/client/common/installer.ts b/src/client/common/installer.ts index 6e5b79ad5158..e8e93e76c7ef 100644 --- a/src/client/common/installer.ts +++ b/src/client/common/installer.ts @@ -1,30 +1,33 @@ +import * as os from 'os'; import * as vscode from 'vscode'; +import { commands, ConfigurationTarget, Disposable, OutputChannel, Terminal, Uri, window, workspace } from 'vscode'; import * as settings from './configSettings'; -import { createDeferred, isNotInstalledError } from './helpers'; -import { execPythonFile } from './utils'; -import * as os from 'os'; -import { Documentation } from './constants'; +import { isNotInstalledError } from './helpers'; +import { error } from './logger'; +import { execPythonFile, getFullyQualifiedPythonInterpreterPath, IS_WINDOWS } from './utils'; export enum Product { - pytest, - nosetest, - pylint, - flake8, - pep8, - pylama, - prospector, - pydocstyle, - yapf, - autopep8, - mypy, - unittest, - ctags + pytest = 1, + nosetest = 2, + pylint = 3, + flake8 = 4, + pep8 = 5, + pylama = 6, + prospector = 7, + pydocstyle = 8, + yapf = 9, + autopep8 = 10, + mypy = 11, + unittest = 12, + ctags = 13, + rope = 14 } +// tslint:disable-next-line:variable-name const ProductInstallScripts = new Map(); ProductInstallScripts.set(Product.autopep8, ['-m', 'pip', 'install', 'autopep8']); ProductInstallScripts.set(Product.flake8, ['-m', 'pip', 'install', 'flake8']); -ProductInstallScripts.set(Product.mypy, ['-m', 'pip', 'install', 'mypy-lang']); +ProductInstallScripts.set(Product.mypy, ['-m', 'pip', 'install', 'mypy']); ProductInstallScripts.set(Product.nosetest, ['-m', 'pip', 'install', 'nose']); ProductInstallScripts.set(Product.pep8, ['-m', 'pip', 'install', 'pep8']); ProductInstallScripts.set(Product.pylama, ['-m', 'pip', 'install', 'pylama']); @@ -33,6 +36,37 @@ ProductInstallScripts.set(Product.pydocstyle, ['-m', 'pip', 'install', 'pydocsty ProductInstallScripts.set(Product.pylint, ['-m', 'pip', 'install', 'pylint']); ProductInstallScripts.set(Product.pytest, ['-m', 'pip', 'install', '-U', 'pytest']); ProductInstallScripts.set(Product.yapf, ['-m', 'pip', 'install', 'yapf']); +ProductInstallScripts.set(Product.rope, ['-m', 'pip', 'install', 'rope']); + +// tslint:disable-next-line:variable-name +const ProductUninstallScripts = new Map(); +ProductUninstallScripts.set(Product.autopep8, ['-m', 'pip', 'uninstall', 'autopep8', '--yes']); +ProductUninstallScripts.set(Product.flake8, ['-m', 'pip', 'uninstall', 'flake8', '--yes']); +ProductUninstallScripts.set(Product.mypy, ['-m', 'pip', 'uninstall', 'mypy', '--yes']); +ProductUninstallScripts.set(Product.nosetest, ['-m', 'pip', 'uninstall', 'nose', '--yes']); +ProductUninstallScripts.set(Product.pep8, ['-m', 'pip', 'uninstall', 'pep8', '--yes']); +ProductUninstallScripts.set(Product.pylama, ['-m', 'pip', 'uninstall', 'pylama', '--yes']); +ProductUninstallScripts.set(Product.prospector, ['-m', 'pip', 'uninstall', 'prospector', '--yes']); +ProductUninstallScripts.set(Product.pydocstyle, ['-m', 'pip', 'uninstall', 'pydocstyle', '--yes']); +ProductUninstallScripts.set(Product.pylint, ['-m', 'pip', 'uninstall', 'pylint', '--yes']); +ProductUninstallScripts.set(Product.pytest, ['-m', 'pip', 'uninstall', 'pytest', '--yes']); +ProductUninstallScripts.set(Product.yapf, ['-m', 'pip', 'uninstall', 'yapf', '--yes']); +ProductUninstallScripts.set(Product.rope, ['-m', 'pip', 'uninstall', 'rope', '--yes']); + +// tslint:disable-next-line:variable-name +export const ProductExecutableAndArgs = new Map(); +ProductExecutableAndArgs.set(Product.mypy, { executable: 'python', args: ['-m', 'mypy'] }); +ProductExecutableAndArgs.set(Product.nosetest, { executable: 'python', args: ['-m', 'nose'] }); +ProductExecutableAndArgs.set(Product.pylama, { executable: 'python', args: ['-m', 'pylama'] }); +ProductExecutableAndArgs.set(Product.prospector, { executable: 'python', args: ['-m', 'prospector'] }); +ProductExecutableAndArgs.set(Product.pylint, { executable: 'python', args: ['-m', 'pylint'] }); +ProductExecutableAndArgs.set(Product.pytest, { executable: 'python', args: ['-m', 'pytest'] }); +ProductExecutableAndArgs.set(Product.autopep8, { executable: 'python', args: ['-m', 'autopep8'] }); +ProductExecutableAndArgs.set(Product.pep8, { executable: 'python', args: ['-m', 'pep8'] }); +ProductExecutableAndArgs.set(Product.pydocstyle, { executable: 'python', args: ['-m', 'pydocstyle'] }); +ProductExecutableAndArgs.set(Product.yapf, { executable: 'python', args: ['-m', 'yapf'] }); +ProductExecutableAndArgs.set(Product.flake8, { executable: 'python', args: ['-m', 'flake8'] }); + switch (os.platform()) { case 'win32': { // Nothing @@ -46,10 +80,18 @@ switch (os.platform()) { } } -const Linters: Product[] = [Product.flake8, Product.pep8, Product.pylama, Product.prospector, Product.pylint, Product.mypy, Product.pydocstyle]; -const Formatters: Product[] = [Product.autopep8, Product.yapf]; -const TestFrameworks: Product[] = [Product.pytest, Product.nosetest, Product.unittest]; +// tslint:disable-next-line:variable-name +export const Linters: Product[] = [ + Product.flake8, + Product.pep8, + Product.pylama, + Product.prospector, + Product.pylint, + Product.mypy, + Product.pydocstyle +]; +// tslint:disable-next-line:variable-name const ProductNames = new Map(); ProductNames.set(Product.autopep8, 'autopep8'); ProductNames.set(Product.flake8, 'flake8'); @@ -62,9 +104,10 @@ ProductNames.set(Product.pydocstyle, 'pydocstyle'); ProductNames.set(Product.pylint, 'pylint'); ProductNames.set(Product.pytest, 'py.test'); ProductNames.set(Product.yapf, 'yapf'); +ProductNames.set(Product.rope, 'rope'); -const SettingToDisableProduct = new Map(); -SettingToDisableProduct.set(Product.autopep8, ''); +// tslint:disable-next-line:variable-name +export const SettingToDisableProduct = new Map(); SettingToDisableProduct.set(Product.flake8, 'linting.flake8Enabled'); SettingToDisableProduct.set(Product.mypy, 'linting.mypyEnabled'); SettingToDisableProduct.set(Product.nosetest, 'unitTest.nosetestsEnabled'); @@ -74,12 +117,55 @@ SettingToDisableProduct.set(Product.prospector, 'linting.prospectorEnabled'); SettingToDisableProduct.set(Product.pydocstyle, 'linting.pydocstyleEnabled'); SettingToDisableProduct.set(Product.pylint, 'linting.pylintEnabled'); SettingToDisableProduct.set(Product.pytest, 'unitTest.pyTestEnabled'); -SettingToDisableProduct.set(Product.yapf, 'yapf'); -export class Installer { - private static terminal: vscode.Terminal; +// tslint:disable-next-line:variable-name +const ProductInstallationPrompt = new Map(); +ProductInstallationPrompt.set(Product.ctags, 'Install CTags to enable Python workspace symbols'); + +enum ProductType { + Linter, + Formatter, + TestFramework, + RefactoringLibrary, + WorkspaceSymbols +} + +// tslint:disable-next-line:variable-name +const ProductTypeNames = new Map(); +ProductTypeNames.set(ProductType.Formatter, 'Formatter'); +ProductTypeNames.set(ProductType.Linter, 'Linter'); +ProductTypeNames.set(ProductType.RefactoringLibrary, 'Refactoring library'); +ProductTypeNames.set(ProductType.TestFramework, 'Test Framework'); +ProductTypeNames.set(ProductType.WorkspaceSymbols, 'Workspace Symbols'); + +// tslint:disable-next-line:variable-name +const ProductTypes = new Map(); +ProductTypes.set(Product.flake8, ProductType.Linter); +ProductTypes.set(Product.mypy, ProductType.Linter); +ProductTypes.set(Product.pep8, ProductType.Linter); +ProductTypes.set(Product.prospector, ProductType.Linter); +ProductTypes.set(Product.pydocstyle, ProductType.Linter); +ProductTypes.set(Product.pylama, ProductType.Linter); +ProductTypes.set(Product.pylint, ProductType.Linter); +ProductTypes.set(Product.ctags, ProductType.WorkspaceSymbols); +ProductTypes.set(Product.nosetest, ProductType.TestFramework); +ProductTypes.set(Product.pytest, ProductType.TestFramework); +ProductTypes.set(Product.unittest, ProductType.TestFramework); +ProductTypes.set(Product.autopep8, ProductType.Formatter); +ProductTypes.set(Product.yapf, ProductType.Formatter); +ProductTypes.set(Product.rope, ProductType.RefactoringLibrary); + +const IS_POWERSHELL = /powershell.exe$/i; + +export enum InstallerResponse { + Installed, + Disabled, + Ignore +} +export class Installer implements vscode.Disposable { + private static terminal: vscode.Terminal | undefined | null; private disposables: vscode.Disposable[] = []; - constructor(private outputChannel: vscode.OutputChannel = null) { + constructor(private outputChannel?: vscode.OutputChannel) { this.disposables.push(vscode.window.onDidCloseTerminal(term => { if (term === Installer.terminal) { Installer.terminal = null; @@ -89,120 +175,218 @@ export class Installer { public dispose() { this.disposables.forEach(d => d.dispose()); } + private shouldDisplayPrompt(product: Product) { + // tslint:disable-next-line:no-non-null-assertion + const productName = ProductNames.get(product)!; + const pythonConfig = workspace.getConfiguration('python'); + // tslint:disable-next-line:prefer-type-cast + const disablePromptForFeatures = pythonConfig.get('disablePromptForFeatures', [] as string[]); + return disablePromptForFeatures.indexOf(productName) === -1; + } - promptToInstall(product: Product): Thenable { - let productType = Linters.indexOf(product) >= 0 ? 'Linter' : (Formatters.indexOf(product) >= 0 ? 'Formatter' : 'Test Framework'); - const productName = ProductNames.get(product); + // tslint:disable-next-line:member-ordering + public async promptToInstall(product: Product, resource?: Uri): Promise { + // tslint:disable-next-line:no-non-null-assertion + const productType = ProductTypes.get(product)!; + // tslint:disable-next-line:no-non-null-assertion + const productTypeName = ProductTypeNames.get(productType)!; + // tslint:disable-next-line:no-non-null-assertion + const productName = ProductNames.get(product)!; - const installOption = 'Install ' + productName; - const disableOption = 'Disable this ' + productType; + if (!this.shouldDisplayPrompt(product)) { + const message = `${productTypeName} '${productName}' not installed.`; + if (this.outputChannel) { + this.outputChannel.appendLine(message); + } else { + console.warn(message); + } + return InstallerResponse.Ignore; + } + + const installOption = ProductInstallationPrompt.has(product) ? ProductInstallationPrompt.get(product) : `Install ${productName}`; + const disableOption = `Disable ${productTypeName}`; + const dontShowAgain = 'Don\'t show this prompt again'; const alternateFormatter = product === Product.autopep8 ? 'yapf' : 'autopep8'; const useOtherFormatter = `Use '${alternateFormatter}' formatter`; const options = []; - if (Formatters.indexOf(product) === -1) { - options.push(...[installOption, disableOption]); + options.push(installOption); + if (productType === ProductType.Formatter) { + options.push(...[useOtherFormatter]); } - else { - options.push(...[installOption, useOtherFormatter]); + if (SettingToDisableProduct.has(product)) { + options.push(...[disableOption, dontShowAgain]); } - return vscode.window.showErrorMessage(`${productType} ${productName} is not installed`, ...options).then(item => { - switch (item) { - case installOption: { - return this.installProduct(product); - } - case disableOption: { - if (Linters.indexOf(product) >= 0) { - return disableLinter(product); - } - else { - const pythonConfig = vscode.workspace.getConfiguration('python'); - const settingToDisable = SettingToDisableProduct.get(product); - return pythonConfig.update(settingToDisable, false); - } - } - case useOtherFormatter: { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('formatting.provider', alternateFormatter); - } - case 'Help': { - return Promise.resolve(); + const item = await window.showErrorMessage(`${productTypeName} ${productName} is not installed`, ...options); + switch (item) { + case installOption: { + return this.install(product, resource); + } + case disableOption: { + if (Linters.indexOf(product) >= 0) { + return this.disableLinter(product, resource).then(() => InstallerResponse.Disabled); + } else { + // tslint:disable-next-line:no-non-null-assertion + const settingToDisable = SettingToDisableProduct.get(product)!; + return this.updateSetting(settingToDisable, false, resource).then(() => InstallerResponse.Disabled); } } - }); + case useOtherFormatter: { + return this.updateSetting('formatting.provider', alternateFormatter, resource) + .then(() => InstallerResponse.Installed); + } + case dontShowAgain: { + const pythonConfig = workspace.getConfiguration('python'); + // tslint:disable-next-line:prefer-type-cast + const features = pythonConfig.get('disablePromptForFeatures', [] as string[]); + features.push(productName); + return pythonConfig.update('disablePromptForFeatures', features, true).then(() => InstallerResponse.Ignore); + } + default: { + throw new Error('Invalid selection'); + } + } } - - installProduct(product: Product): Promise { + // tslint:disable-next-line:member-ordering + public async install(product: Product, resource?: Uri): Promise { if (!this.outputChannel && !Installer.terminal) { - Installer.terminal = vscode.window.createTerminal('Python Installer'); + Installer.terminal = window.createTerminal('Python Installer'); } - if (product === Product.ctags && os.platform() === 'win32') { - vscode.commands.executeCommand('python.displayHelp', Documentation.Workspace.InstallOnWindows); - return Promise.resolve(); + if (product === Product.ctags && settings.IS_WINDOWS) { + if (this.outputChannel) { + this.outputChannel.appendLine('Install Universal Ctags Win32 to enable support for Workspace Symbols'); + this.outputChannel.appendLine('Download the CTags binary from the Universal CTags site.'); + this.outputChannel.appendLine('Option 1: Extract ctags.exe from the downloaded zip to any folder within your PATH so that Visual Studio Code can run it.'); + this.outputChannel.appendLine('Option 2: Extract to any folder and add the path to this folder to the command setting.'); + this.outputChannel.appendLine('Option 3: Extract to any folder and define that path in the python.workspaceSymbols.ctagsPath setting of your user settings file (settings.json).'); + this.outputChannel.show(); + } else { + window.showInformationMessage('Install Universal Ctags and set it in your path or define the path in your python.workspaceSymbols.ctagsPath settings'); + } + return InstallerResponse.Ignore; } - let installArgs = ProductInstallScripts.get(product); - const pythonPath = settings.PythonSettings.getInstance().pythonPath; - + // tslint:disable-next-line:no-non-null-assertion + let installArgs = ProductInstallScripts.get(product)!; + const pipIndex = installArgs.indexOf('pip'); + if (pipIndex > 0) { + installArgs = installArgs.slice(); + const proxy = vscode.workspace.getConfiguration('http').get('proxy', ''); + if (proxy.length > 0) { + installArgs.splice(2, 0, proxy); + installArgs.splice(2, 0, '--proxy'); + } + } + // tslint:disable-next-line:no-any + let installationPromise: Promise; if (this.outputChannel && installArgs[0] === '-m') { // Errors are just displayed to the user this.outputChannel.show(); - return execPythonFile(pythonPath, installArgs, vscode.workspace.rootPath, true, (data) => { - this.outputChannel.append(data); - }); - } - else { - let installScript = installArgs.join(' '); - if (installArgs[0] === '-m') { - if (pythonPath.indexOf(' ') >= 0) { - installScript = `"${pythonPath}" ${installScript}`; - } - else { - installScript = `${pythonPath} ${installScript}`; - } - } - Installer.terminal.sendText(installScript); - Installer.terminal.show(false); - // Unfortunately we won't know when the command has completed - return Promise.resolve(); + installationPromise = execPythonFile(resource, settings.PythonSettings.getInstance(resource).pythonPath, + // tslint:disable-next-line:no-non-null-assertion + installArgs, getCwdForInstallScript(resource), true, (data) => { this.outputChannel!.append(data); }); + } else { + // When using terminal get the fully qualitified path + // Cuz people may launch vs code from terminal when they have activated the appropriate virtual env + // Problem is terminal doesn't use the currently activated virtual env + // Must have something to do with the process being launched in the terminal + installationPromise = getFullyQualifiedPythonInterpreterPath(resource) + .then(pythonPath => { + let installScript = installArgs.join(' '); + + if (installArgs[0] === '-m') { + if (pythonPath.indexOf(' ') >= 0) { + installScript = `"${pythonPath}" ${installScript}`; + } else { + installScript = `${pythonPath} ${installScript}`; + } + } + if (this.terminalIsPowershell(resource)) { + installScript = `& ${installScript}`; + } + + // tslint:disable-next-line:no-non-null-assertion + Installer.terminal!.sendText(installScript); + // tslint:disable-next-line:no-non-null-assertion + Installer.terminal!.show(false); + }); } + + return installationPromise + .then(() => this.isInstalled(product)) + .then(isInstalled => isInstalled ? InstallerResponse.Installed : InstallerResponse.Ignore); } - isProductInstalled(product: Product): Promise { - return isProductInstalled(product); + // tslint:disable-next-line:member-ordering + public isInstalled(product: Product, resource?: Uri): Promise { + return isProductInstalled(product, resource); + } + + // tslint:disable-next-line:member-ordering no-any + public uninstall(product: Product, resource?: Uri): Promise { + return uninstallproduct(product, resource); + } + // tslint:disable-next-line:member-ordering + public disableLinter(product: Product, resource: Uri) { + if (resource && !workspace.getWorkspaceFolder(resource)) { + // tslint:disable-next-line:no-non-null-assertion + const settingToDisable = SettingToDisableProduct.get(product)!; + const pythonConfig = workspace.getConfiguration('python', resource); + return pythonConfig.update(settingToDisable, false, ConfigurationTarget.Workspace); + } else { + const pythonConfig = workspace.getConfiguration('python'); + return pythonConfig.update('linting.enabledWithoutWorkspace', false, true); + } + } + private terminalIsPowershell(resource?: Uri) { + if (!IS_WINDOWS) { + return false; + } + // tslint:disable-next-line:no-backbone-get-set-outside-model + const terminal = workspace.getConfiguration('terminal.integrated.shell', resource).get('windows'); + return typeof terminal === 'string' && IS_POWERSHELL.test(terminal); + } + // tslint:disable-next-line:no-any + private updateSetting(setting: string, value: any, resource?: Uri) { + if (resource && !workspace.getWorkspaceFolder(resource)) { + const pythonConfig = workspace.getConfiguration('python', resource); + return pythonConfig.update(setting, value, ConfigurationTarget.Workspace); + } else { + const pythonConfig = workspace.getConfiguration('python'); + return pythonConfig.update(setting, value, true); + } } } -export function disableLinter(product: Product) { - const pythonConfig = vscode.workspace.getConfiguration('python'); - const settingToDisable = SettingToDisableProduct.get(product); - pythonConfig.update(settingToDisable, false); +function getCwdForInstallScript(resource?: Uri) { + const workspaceFolder = resource ? workspace.getWorkspaceFolder(resource) : undefined; + if (workspaceFolder) { + return workspaceFolder.uri.fsPath; + } + if (Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0) { + return workspace.workspaceFolders[0].uri.fsPath; + } + return __dirname; } -function isTestFrameworkInstalled(product: Product): Promise { - const fileToRun = product === Product.pytest ? 'py.test' : 'nosetests'; - const def = createDeferred(); - execPythonFile(fileToRun, ['--version'], vscode.workspace.rootPath, false) - .then(() => { - def.resolve(true); - }).catch(reason => { - if (isNotInstalledError(reason)) { - def.resolve(false); - } - else { - def.resolve(true); - } - }); - return def.promise; +async function isProductInstalled(product: Product, resource?: Uri): Promise { + if (!ProductExecutableAndArgs.has(product)) { + return; + } + // tslint:disable-next-line:no-non-null-assertion + const prodExec = ProductExecutableAndArgs.get(product)!; + const cwd = getCwdForInstallScript(resource); + return execPythonFile(resource, prodExec.executable, prodExec.args.concat(['--version']), cwd, false) + .then(() => true) + .catch(reason => !isNotInstalledError(reason)); } -function isProductInstalled(product: Product): Promise { - switch (product) { - case Product.pytest: { - return isTestFrameworkInstalled(product); - } - case Product.nosetest: { - return isTestFrameworkInstalled(product); - } + +// tslint:disable-next-line:no-any +function uninstallproduct(product: Product, resource?: Uri): Promise { + if (!ProductUninstallScripts.has(product)) { + return Promise.resolve(); } - throw new Error('Not supported'); -} \ No newline at end of file + // tslint:disable-next-line:no-non-null-assertion + const uninstallArgs = ProductUninstallScripts.get(product)!; + return execPythonFile(resource, 'python', uninstallArgs, getCwdForInstallScript(resource), false); +} diff --git a/src/client/common/interpreterInfoCache.ts b/src/client/common/interpreterInfoCache.ts new file mode 100644 index 000000000000..f23f6ef7563b --- /dev/null +++ b/src/client/common/interpreterInfoCache.ts @@ -0,0 +1,54 @@ +import { Uri, workspace } from 'vscode'; + +type InterpreterCache = { + pythonInterpreterDirectory?: string; + pythonInterpreterPath?: string; + pythonSettingsPath?: string; + // tslint:disable-next-line:no-any + customEnvVariables?: any; +}; + +const cache = new Map(); + +// tslint:disable-next-line:no-stateless-class +export class InterpreterInfoCache { + // tslint:disable-next-line:function-name + public static clear(): void { + cache.clear(); + } + // tslint:disable-next-line:function-name + public static get(resource?: Uri) { + const cacheKey = InterpreterInfoCache.getCacheKey(resource) || ''; + return cache.has(cacheKey) ? cache.get(cacheKey) : {}; + } + // tslint:disable-next-line:function-name + public static setPaths(resource?: Uri, pythonSettingsPath?: string, pythonInterpreterPath?: string, pythonInterpreterDirectory?: string) { + InterpreterInfoCache.setCacheData('pythonInterpreterDirectory', resource, pythonInterpreterDirectory); + InterpreterInfoCache.setCacheData('pythonInterpreterPath', resource, pythonInterpreterPath); + InterpreterInfoCache.setCacheData('pythonSettingsPath', resource, pythonSettingsPath); + } + + // tslint:disable-next-line:no-any function-name + public static setCustomEnvVariables(resource?: Uri, envVars?: any) { + // tslint:disable-next-line:no-any + InterpreterInfoCache.setCacheData('customEnvVariables', resource, envVars); + } + // tslint:disable-next-line:no-any function-name + private static setCacheData(property: keyof InterpreterCache, resource?: Uri, value?: any) { + const cacheKey = InterpreterInfoCache.getCacheKey(resource) || ''; + // tslint:disable-next-line:prefer-type-cast + const data = cache.has(cacheKey) ? cache.get(cacheKey) : {} as InterpreterCache; + data[property] = value; + cache.set(cacheKey, data); + } + private static getCacheKey(resource?: Uri): string { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + return ''; + } + if (!resource || workspace.workspaceFolders.length === 1) { + return workspace.workspaceFolders[0].uri.fsPath; + } + const folder = workspace.getWorkspaceFolder(resource); + return folder ? folder.uri.fsPath : ''; + } +} diff --git a/src/client/common/logger.ts b/src/client/common/logger.ts index ac372199e258..076e1a008964 100644 --- a/src/client/common/logger.ts +++ b/src/client/common/logger.ts @@ -22,7 +22,9 @@ class Logger { Logger.writeLine(category, message); } static writeLine(category: string = "log", line: any) { - console[category](line); + if (process.env['VSC_PYTHON_CI_TEST'] !== '1') { + console[category](line); + } if (outChannel) { outChannel.appendLine(line); } diff --git a/src/client/common/open.ts b/src/client/common/open.ts index 1f17d7a0cc08..28efbb9afdd5 100644 --- a/src/client/common/open.ts +++ b/src/client/common/open.ts @@ -16,10 +16,10 @@ export function open(opts: any): Promise { var appArgs = []; var args = []; var cpOpts: any = {}; - if (opts.cwd) { + if (opts.cwd && typeof opts.cwd === 'string' && opts.cwd.length > 0) { cpOpts.cwd = opts.cwd; } - if (opts.env) { + if (opts.env && Object.keys(opts.env).length > 0) { cpOpts.env = opts.env; } @@ -30,11 +30,11 @@ export function open(opts: any): Promise { if (process.platform === 'darwin') { const sudoPrefix = opts.sudo === true ? 'sudo ' : ''; - cmd = 'osascript'; - args = [ '-e', 'tell application "terminal"', - '-e', 'activate', - '-e', 'do script "' + sudoPrefix + [opts.app].concat(appArgs).join(" ") + '"', - '-e', 'end tell' ]; + cmd = 'osascript'; + args = ['-e', 'tell application "terminal"', + '-e', 'activate', + '-e', 'do script "' + sudoPrefix + [opts.app].concat(appArgs).join(" ") + '"', + '-e', 'end tell']; } else if (process.platform === 'win32') { cmd = 'cmd'; args.push('/c', 'start'); @@ -59,10 +59,10 @@ export function open(opts: any): Promise { var cp = childProcess.spawn(cmd, args, cpOpts); if (opts.wait) { - return new Promise(function(resolve, reject) { + return new Promise(function (resolve, reject) { cp.once('error', reject); - cp.once('close', function(code) { + cp.once('close', function (code) { if (code > 0) { reject(new Error('Exited with code ' + code)); return; diff --git a/src/client/common/persistentState.ts b/src/client/common/persistentState.ts new file mode 100644 index 000000000000..09321b649310 --- /dev/null +++ b/src/client/common/persistentState.ts @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Memento } from 'vscode'; + +export class PersistentState { + constructor(private storage: Memento, private key: string, private defaultValue: T) { } + + public get value(): T { + return this.storage.get(this.key, this.defaultValue); + } + + public set value(newValue: T) { + this.storage.update(this.key, newValue); + } +} + +export interface IPersistentStateFactory { + createGlobalPersistentState(key: string, defaultValue: T): PersistentState; + createWorkspacePersistentState(key: string, defaultValue: T): PersistentState; +} + +export class PersistentStateFactory implements IPersistentStateFactory { + constructor(private globalState: Memento, private workspaceState: Memento) { } + public createGlobalPersistentState(key: string, defaultValue: T): PersistentState { + return new PersistentState(this.globalState, key, defaultValue); + } + public createWorkspacePersistentState(key: string, defaultValue: T): PersistentState { + return new PersistentState(this.workspaceState, key, defaultValue); + } +} diff --git a/src/client/common/registry.ts b/src/client/common/registry.ts new file mode 100644 index 000000000000..3f0bb1c907d8 --- /dev/null +++ b/src/client/common/registry.ts @@ -0,0 +1,81 @@ +import * as Registry from 'winreg'; +enum RegistryArchitectures { + x86 = 'x86', + x64 = 'x64' +} + +export enum Architecture { + Unknown = 1, + x86 = 2, + x64 = 3 +} +export enum Hive { + HKCU, HKLM +} + +export interface IRegistry { + getKeys(key: string, hive: Hive, arch?: Architecture): Promise; + getValue(key: string, hive: Hive, arch?: Architecture, name?: string): Promise; +} + +export class RegistryImplementation implements IRegistry { + public getKeys(key: string, hive: Hive, arch?: Architecture) { + return getRegistryKeys({ hive: translateHive(hive), arch: translateArchitecture(arch), key }); + } + public getValue(key: string, hive: Hive, arch?: Architecture, name: string = '') { + return getRegistryValue({ hive: translateHive(hive), arch: translateArchitecture(arch), key }, name); + } +} + +export function getArchitectureDislayName(arch?: Architecture) { + switch (arch) { + case Architecture.x64: + return '64-bit'; + case Architecture.x86: + return '32-bit'; + default: + return ''; + } +} + +function getRegistryValue(options: Registry.Options, name: string = '') { + return new Promise((resolve, reject) => { + new Registry(options).get(name, (error, result) => { + if (error) { + return resolve(undefined); + } + resolve(result.value); + }); + }); +} +function getRegistryKeys(options: Registry.Options): Promise { + // https://github.com/python/peps/blob/master/pep-0514.txt#L85 + return new Promise((resolve, reject) => { + new Registry(options).keys((error, result) => { + if (error) { + return resolve([]); + } + resolve(result.map(item => item.key)); + }); + }); +} +function translateArchitecture(arch?: Architecture): RegistryArchitectures | null | undefined { + switch (arch) { + case Architecture.x86: + return RegistryArchitectures.x86; + case Architecture.x64: + return RegistryArchitectures.x64; + default: + return; + } +} +function translateHive(hive: Hive): string | null | undefined { + switch (hive) { + case Hive.HKCU: + return Registry.HKCU; + case Hive.HKLM: + return Registry.HKLM; + default: + return; + } +} diff --git a/src/client/common/systemVariables.ts b/src/client/common/systemVariables.ts index 8861ce4a4b15..444d782d6eea 100644 --- a/src/client/common/systemVariables.ts +++ b/src/client/common/systemVariables.ts @@ -95,7 +95,7 @@ export abstract class AbstractSystemVariables implements ISystemVariables { if (Types.isString(newValue)) { return newValue; } else { - return match && match.indexOf('env.') > 0 ? '' : match; + return match && (match.indexOf('env.') > 0 || match.indexOf('env:') > 0) ? '' : match; } }); } @@ -133,14 +133,13 @@ export abstract class AbstractSystemVariables implements ISystemVariables { export class SystemVariables extends AbstractSystemVariables { private _workspaceRoot: string; private _workspaceRootFolderName: string; - private _execPath: string; - constructor() { + constructor(workspaceRoot?: string) { super(); - this._workspaceRoot = typeof vscode.workspace.rootPath === 'string' ? vscode.workspace.rootPath : __dirname;; + this._workspaceRoot = typeof workspaceRoot === 'string' ? workspaceRoot : __dirname; this._workspaceRootFolderName = Path.basename(this._workspaceRoot); Object.keys(process.env).forEach(key => { - this[`env.${key}`] = process.env[key]; + this[`env:${key}`] = this[`env.${key}`] = process.env[key]; }); } @@ -155,4 +154,4 @@ export class SystemVariables extends AbstractSystemVariables { public get workspaceRootFolderName(): string { return this._workspaceRootFolderName; } -} \ No newline at end of file +} diff --git a/src/client/common/telemetry.ts b/src/client/common/telemetry.ts deleted file mode 100644 index 5ce0fd097e5f..000000000000 --- a/src/client/common/telemetry.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { extensions } from "vscode"; -import TelemetryReporter from "vscode-extension-telemetry"; - -// Borrowed from omnisharpServer.ts (omnisharp-vscode) -export class Delays { - immediateDelays: number = 0; // 0-25 milliseconds - nearImmediateDelays: number = 0; // 26-50 milliseconds - shortDelays: number = 0; // 51-250 milliseconds - mediumDelays: number = 0; // 251-500 milliseconds - idleDelays: number = 0; // 501-1500 milliseconds - nonFocusDelays: number = 0; // 1501-3000 milliseconds - bigDelays: number = 0; // 3000+ milliseconds - private startTime: number = Date.now(); - public stop() { - let endTime = Date.now(); - let elapsedTime = endTime - this.startTime; - - if (elapsedTime <= 25) { - this.immediateDelays += 1; - } - else if (elapsedTime <= 50) { - this.nearImmediateDelays += 1; - } - else if (elapsedTime <= 250) { - this.shortDelays += 1; - } - else if (elapsedTime <= 500) { - this.mediumDelays += 1; - } - else if (elapsedTime <= 1500) { - this.idleDelays += 1; - } - else if (elapsedTime <= 3000) { - this.nonFocusDelays += 1; - } - else { - this.bigDelays += 1; - } - } - public toMeasures(): { [key: string]: number } { - return { - immedateDelays: this.immediateDelays, - nearImmediateDelays: this.nearImmediateDelays, - shortDelays: this.shortDelays, - mediumDelays: this.mediumDelays, - idleDelays: this.idleDelays, - nonFocusDelays: this.nonFocusDelays - }; - } -} - -const extensionId = "donjayamanne.python"; -const extension = extensions.getExtension(extensionId); -const extensionVersion = extension.packageJSON.version; -const aiKey = "fce7a3d5-4665-4404-b786-31a6306749a6"; -let reporter: TelemetryReporter; - -/** - * Sends a telemetry event - * @param {string} eventName The event name - * @param {object} properties An associative array of strings - * @param {object} measures An associative array of numbers - */ -export function sendTelemetryEvent(eventName: string, properties?: { - [key: string]: string; -}, measures?: { - [key: string]: number; -}) { - reporter = reporter ? reporter : new TelemetryReporter(extensionId, extensionVersion, aiKey); - reporter.sendTelemetryEvent.apply(reporter, arguments); -} - diff --git a/src/client/common/telemetryContracts.ts b/src/client/common/telemetryContracts.ts deleted file mode 100644 index 735793d078b5..000000000000 --- a/src/client/common/telemetryContracts.ts +++ /dev/null @@ -1,31 +0,0 @@ -export namespace Debugger { - export const Load = 'DEBUGGER_LOAD'; - export const Attach = 'DEBUGGER_ATTACH'; -} -export namespace Commands { - export const SortImports = 'COMMAND_SORT_IMPORTS'; - export const UnitTests = 'COMMAND_UNIT_TEST'; -} -export namespace IDE { - export const Completion = 'CODE_COMPLETION'; - export const Definition = 'CODE_DEFINITION'; - export const Format = 'CODE_FORMAT'; - export const HoverDefinition = 'CODE_HOVER_DEFINITION'; - export const Reference = 'CODE_REFERENCE'; - export const Rename = 'CODE_RENAME'; - export const Symbol = 'CODE_SYMBOL'; - export const Lint = 'LINTING'; -} -export namespace REFACTOR { - export const Rename = 'REFACTOR_RENAME'; - export const ExtractVariable = 'REFACTOR_EXTRACT_VAR'; - export const ExtractMethod = 'REFACTOR_EXTRACT_METHOD'; -} -export namespace UnitTests { - export const Run = 'UNITTEST_RUN'; - export const Discover = 'UNITTEST_DISCOVER'; -} -export namespace Jupyter { - export const Usage = 'JUPYTER'; -} -export const EVENT_LOAD = 'IDE_LOAD'; \ No newline at end of file diff --git a/src/client/common/utils.ts b/src/client/common/utils.ts index 470531675a92..820c81b596da 100644 --- a/src/client/common/utils.ts +++ b/src/client/common/utils.ts @@ -1,17 +1,19 @@ -/// -/// - 'use strict'; // TODO: Cleanup this place // Add options for execPythonFile -import * as path from 'path'; -import * as fs from 'fs'; import * as child_process from 'child_process'; +import * as fs from 'fs'; +import * as fsExtra from 'fs-extra'; +import * as os from 'os'; +import * as path from 'path'; +import { CancellationToken, Range, TextDocument, Uri } from 'vscode'; import * as settings from './configSettings'; -import { CancellationToken } from 'vscode'; +import { mergeEnvVariables, parseEnvFile } from './envFileParser'; import { isNotInstalledError } from './helpers'; +import { InterpreterInfoCache } from './interpreterInfoCache'; export const IS_WINDOWS = /^win/.test(process.platform); +export const Is_64Bit = os.arch() === 'x64'; export const PATH_VARIABLE_NAME = IS_WINDOWS ? 'Path' : 'PATH'; const PathValidity: Map = new Map(); @@ -37,124 +39,207 @@ export function fsExistsAsync(filePath: string): Promise { }); }); } +export function fsReaddirAsync(root: string): Promise { + return new Promise(resolve => { + // Now look for Interpreters in this directory + fs.readdir(root, (err, subDirs) => { + if (err) { + return resolve([]); + } + resolve(subDirs.map(subDir => path.join(root, subDir))); + }); + }); +} -let pythonInterpretterDirectory: string = null; -let previouslyIdentifiedPythonPath: string = null; -let customEnvVariables: any = null; - -// If config settings change then clear env variables that we have cached -// Remember, the path to the python interpreter can change, hence we need to re-set the paths -settings.PythonSettings.getInstance().on('change', function () { - customEnvVariables = null; -}); +async function getPythonInterpreterDirectory(resource?: Uri): Promise { + const cache = InterpreterInfoCache.get(resource); + const pythonFileName = settings.PythonSettings.getInstance(resource).pythonPath; -export function getPythonInterpreterDirectory(): Promise { // If we already have it and the python path hasn't changed, yay - if (pythonInterpretterDirectory && previouslyIdentifiedPythonPath === settings.PythonSettings.getInstance().pythonPath) { - return Promise.resolve(pythonInterpretterDirectory); + if (cache.pythonInterpreterDirectory && cache.pythonInterpreterDirectory.length > 0 + && cache.pythonSettingsPath === pythonFileName) { + return cache.pythonInterpreterDirectory; } - return new Promise(resolve => { - let pythonFileName = settings.PythonSettings.getInstance().pythonPath; - // Check if we have the path - if (path.basename(pythonFileName) === pythonFileName) { - // No path provided - return resolve(''); + // Check if we have the path + if (path.basename(pythonFileName) === pythonFileName) { + try { + const pythonInterpreterPath = await getPathFromPythonCommand(pythonFileName); + const pythonInterpreterDirectory = path.dirname(pythonInterpreterPath); + InterpreterInfoCache.setPaths(resource, pythonFileName, pythonInterpreterPath, pythonInterpreterDirectory); + return pythonInterpreterDirectory; + // tslint:disable-next-line:variable-name + } catch (_ex) { + InterpreterInfoCache.setPaths(resource, pythonFileName, pythonFileName, ''); + return ''; } + } + return new Promise(resolve => { // If we can execute the python, then get the path from the fully qualified name child_process.execFile(pythonFileName, ['-c', 'print(1234)'], (error, stdout, stderr) => { // Yes this is a valid python path if (stdout.startsWith('1234')) { - return resolve(path.dirname(pythonFileName)); + const pythonInterpreterDirectory = path.dirname(pythonFileName); + InterpreterInfoCache.setPaths(resource, pythonFileName, pythonFileName, pythonInterpreterDirectory); + resolve(pythonInterpreterDirectory); + } else { + // No idea, didn't work, hence don't reject, but return empty path + InterpreterInfoCache.setPaths(resource, pythonFileName, pythonFileName, ''); + resolve(''); + } + }); + }); +} +export async function getFullyQualifiedPythonInterpreterPath(resource?: Uri): Promise { + const pyDir = await getPythonInterpreterDirectory(resource); + const cache = InterpreterInfoCache.get(resource); + return cache.pythonInterpreterPath; +} +export async function getPathFromPythonCommand(pythonPath: string): Promise { + return await new Promise((resolve, reject) => { + child_process.execFile(pythonPath, ['-c', 'import sys;print(sys.executable)'], (_, stdout) => { + if (stdout) { + const lines = stdout.split(/\r?\n/g).map(line => line.trim()).filter(line => line.length > 0); + resolve(lines.length > 0 ? lines[0] : ''); + } else { + reject(); } - // No idea, didn't work, hence don't reject, but return empty path - resolve(''); }); - }).then(value => { - // Cache and return - previouslyIdentifiedPythonPath = settings.PythonSettings.getInstance().pythonPath; - return pythonInterpretterDirectory = value; - }).catch(() => { - // Don't care what the error is, all we know is that this doesn't work - return pythonInterpretterDirectory = ''; }); } +async function getEnvVariables(resource?: Uri): Promise<{}> { + const cache = InterpreterInfoCache.get(resource); + if (cache.customEnvVariables) { + return cache.customEnvVariables; + } + + const pyPath = await getPythonInterpreterDirectory(resource); + let customEnvVariables = await getCustomEnvVars(resource) || {}; -export function execPythonFile(file: string, args: string[], cwd: string, includeErrorAsResponse: boolean = false, stdOut: (line: string) => void = null, token?: CancellationToken): Promise { - // If running the python file, then always revert to execFileInternal - // Cuz python interpreter is always a file and we can and will always run it using child_process.execFile() - if (file === settings.PythonSettings.getInstance().pythonPath) { - if (stdOut) { - return spawnFileInternal(file, args, { cwd }, includeErrorAsResponse, stdOut, token); + if (pyPath.length > 0) { + // Ensure to include the path of the current python. + let newPath = ''; + const currentPath = typeof customEnvVariables[PATH_VARIABLE_NAME] === 'string' ? customEnvVariables[PATH_VARIABLE_NAME] : process.env[PATH_VARIABLE_NAME]; + if (IS_WINDOWS) { + newPath = `${pyPath}\\${path.delimiter}${path.join(pyPath, 'Scripts\\')}${path.delimiter}${currentPath}`; + // This needs to be done for windows. + process.env[PATH_VARIABLE_NAME] = newPath; + } else { + newPath = `${pyPath}${path.delimiter}${currentPath}`; } - return execFileInternal(file, args, { cwd: cwd }, includeErrorAsResponse); + customEnvVariables = mergeEnvVariables(customEnvVariables, process.env); + customEnvVariables[PATH_VARIABLE_NAME] = newPath; } - return getPythonInterpreterDirectory().then(pyPath => { - // We don't have a path - if (pyPath.length === 0) { - if (stdOut) { - return spawnFileInternal(file, args, { cwd }, includeErrorAsResponse, stdOut, token); - } - return execFileInternal(file, args, { cwd: cwd }, includeErrorAsResponse); - } + InterpreterInfoCache.setCustomEnvVariables(resource, customEnvVariables); + return customEnvVariables; +} +export async function execPythonFile(resource: string | Uri | undefined, file: string, args: string[], cwd: string, includeErrorAsResponse: boolean = false, stdOut: (line: string) => void = null, token?: CancellationToken): Promise { + const resourceUri = typeof resource === 'string' ? Uri.file(resource) : resource; + const env = await getEnvVariables(resourceUri); + const options = { cwd, env }; - if (customEnvVariables === null) { - // Ensure to include the path of the current python - let newPath = ''; - if (IS_WINDOWS) { - newPath = pyPath + '\\' + path.delimiter + path.join(pyPath, 'Scripts\\') + path.delimiter + process.env[PATH_VARIABLE_NAME]; - // This needs to be done for windows - process.env[PATH_VARIABLE_NAME] = newPath; - } - else { - newPath = pyPath + path.delimiter + process.env[PATH_VARIABLE_NAME]; - } - let customSettings = <{ [key: string]: string }>{}; - customSettings[PATH_VARIABLE_NAME] = newPath; - customEnvVariables = mergeEnvVariables(customSettings); - } + if (stdOut) { + return spawnFileInternal(file, args, options, includeErrorAsResponse, stdOut, token); + } - if (stdOut) { - return spawnFileInternal(file, args, { cwd, env: customEnvVariables }, includeErrorAsResponse, stdOut, token); - } - return execFileInternal(file, args, { cwd, env: customEnvVariables }, includeErrorAsResponse); - }); + const fileIsPythonInterpreter = (file.toUpperCase() === 'PYTHON' || file === settings.PythonSettings.getInstance(resourceUri).pythonPath); + const execAsModule = fileIsPythonInterpreter && args.length > 0 && args[0] === '-m'; + + if (execAsModule) { + return getFullyQualifiedPythonInterpreterPath(resourceUri) + .then(p => execPythonModule(p, args, options, includeErrorAsResponse, token)); + } + return execFileInternal(file, args, options, includeErrorAsResponse, token); } -function handleResponse(file: string, includeErrorAsResponse: boolean, error: Error, stdout: string, stderr: string): Promise { - return new Promise((resolve, reject) => { - if (isNotInstalledError(error)) { - return reject(error); - } +function handleResponse(file: string, includeErrorAsResponse: boolean, error: Error, stdout: string, stderr: string, token?: CancellationToken): Promise { + if (token && token.isCancellationRequested) { + return Promise.resolve(undefined); + } + if (isNotInstalledError(error)) { + return Promise.reject(error); + } - // pylint: - // In the case of pylint we have some messages (such as config file not found and using default etc...) being returned in stderr - // These error messages are useless when using pylint - if (includeErrorAsResponse && (stdout.length > 0 || stderr.length > 0)) { - return resolve(stdout + '\n' + stderr); - } + // pylint: + // In the case of pylint we have some messages (such as config file not found and using default etc...) being returned in stderr + // These error messages are useless when using pylint + if (includeErrorAsResponse && (stdout.length > 0 || stderr.length > 0)) { + return Promise.resolve(stdout + '\n' + stderr); + } - let hasErrors = (error && error.message.length > 0) || (stderr && stderr.length > 0); - if (hasErrors && (typeof stdout !== 'string' || stdout.length === 0)) { - let errorMsg = (error && error.message) ? error.message : (stderr && stderr.length > 0 ? stderr + '' : ''); - return reject(errorMsg); - } + let hasErrors = (error && error.message.length > 0) || (stderr && stderr.length > 0); + if (hasErrors && (typeof stdout !== 'string' || stdout.length === 0)) { + let errorMsg = (error && error.message) ? error.message : (stderr && stderr.length > 0 ? stderr + '' : ''); + return Promise.reject(errorMsg); + } + else { + return Promise.resolve(stdout + ''); + } +} +function handlePythonModuleResponse(includeErrorAsResponse: boolean, error: Error, stdout: string, stderr: string, token?: CancellationToken): Promise { + if (token && token.isCancellationRequested) { + return Promise.resolve(undefined); + } + if (isNotInstalledError(error)) { + return Promise.reject(error); + } + + // pylint: + // In the case of pylint we have some messages (such as config file not found and using default etc...) being returned in stderr + // These error messages are useless when using pylint + if (includeErrorAsResponse && (stdout.length > 0 || stderr.length > 0)) { + return Promise.resolve(stdout + '\n' + stderr); + } + if (!includeErrorAsResponse && stderr.length > 0) { + return Promise.reject(stderr); + } - resolve(stdout + ''); + return Promise.resolve(stdout + ''); +} +function execPythonModule(file: string, args: string[], options: child_process.ExecFileOptions, includeErrorAsResponse: boolean, token?: CancellationToken): Promise { + options.maxBuffer = options.maxBuffer ? options.maxBuffer : 1024 * 102400; + return new Promise((resolve, reject) => { + let proc = child_process.execFile(file, args, options, (error, stdout, stderr) => { + handlePythonModuleResponse(includeErrorAsResponse, error, stdout, stderr, token) + .then(resolve) + .catch(reject); + }); + if (token && token.onCancellationRequested) { + token.onCancellationRequested(() => { + if (proc) { + proc.kill(); + proc = null; + } + }); + } }); } -function execFileInternal(file: string, args: string[], options: child_process.ExecFileOptions, includeErrorAsResponse: boolean): Promise { + +function execFileInternal(file: string, args: string[], options: child_process.ExecFileOptions, includeErrorAsResponse: boolean, token?: CancellationToken): Promise { + options.maxBuffer = options.maxBuffer ? options.maxBuffer : 1024 * 102400; return new Promise((resolve, reject) => { - child_process.execFile(file, args, options, (error, stdout, stderr) => { - handleResponse(file, includeErrorAsResponse, error, stdout, stderr).then(resolve, reject); + let proc = child_process.execFile(file, args, options, (error, stdout, stderr) => { + handleResponse(file, includeErrorAsResponse, error, stdout, stderr, token) + .then(data => resolve(data)) + .catch(err => reject(err)); }); + if (token && token.onCancellationRequested) { + token.onCancellationRequested(() => { + if (proc) { + proc.kill(); + proc = null; + } + }); + } }); } function spawnFileInternal(file: string, args: string[], options: child_process.ExecFileOptions, includeErrorAsResponse: boolean, stdOut: (line: string) => void, token?: CancellationToken): Promise { return new Promise((resolve, reject) => { + options.env = options.env || {}; + options.env['PYTHONIOENCODING'] = 'UTF-8'; let proc = child_process.spawn(file, args, options); let error = ''; let exited = false; @@ -167,7 +252,7 @@ function spawnFileInternal(file: string, args: string[], options: child_process. }); } proc.on('error', error => { - return reject(error); + reject(error); }); proc.stdout.setEncoding('utf8'); proc.stderr.setEncoding('utf8'); @@ -208,21 +293,13 @@ function spawnFileInternal(file: string, args: string[], options: child_process. function execInternal(command: string, args: string[], options: child_process.ExecFileOptions, includeErrorAsResponse: boolean): Promise { return new Promise((resolve, reject) => { child_process.exec([command].concat(args).join(' '), options, (error, stdout, stderr) => { - handleResponse(command, includeErrorAsResponse, error, stdout, stderr).then(resolve, reject); + handleResponse(command, includeErrorAsResponse, error, stdout, stderr) + .then(data => resolve(data)) + .catch(err => reject(err)); }); }); } -export function mergeEnvVariables(newVariables: { [key: string]: string }): any { - for (let setting in process.env) { - if (!newVariables[setting]) { - newVariables[setting] = process.env[setting]; - } - } - - return newVariables; -} - export function formatErrorForLogging(error: Error | string): string { let message: string = ''; if (typeof error === 'string') { @@ -268,4 +345,90 @@ export function getSubDirectories(rootDir: string): Promise { resolve(subDirs); }); }); -} \ No newline at end of file +} + +export async function getCustomEnvVars(resource?: Uri): Promise<{} | undefined | null> { + const envFile = settings.PythonSettings.getInstance(resource).envFile; + if (typeof envFile !== 'string' || envFile.length === 0) { + return null; + } + const exists = await fsExtra.pathExists(envFile); + if (!exists) { + return null; + } + try { + const vars = parseEnvFile(envFile); + if (vars && typeof vars === 'object' && Object.keys(vars).length > 0) { + return vars; + } + } catch (ex) { + console.error('Failed to parse env file', ex); + } + return null; +} +export function getCustomEnvVarsSync(resource?: Uri): {} | undefined | null { + const envFile = settings.PythonSettings.getInstance(resource).envFile; + if (typeof envFile !== 'string' || envFile.length === 0) { + return null; + } + const exists = fsExtra.pathExistsSync(envFile); + if (!exists) { + return null; + } + try { + const vars = parseEnvFile(envFile); + if (vars && typeof vars === 'object' && Object.keys(vars).length > 0) { + return vars; + } + } catch (ex) { + console.error('Failed to parse env file', ex); + } + return null; +} + +export function getWindowsLineEndingCount(document: TextDocument, offset: Number) { + const eolPattern = new RegExp('\r\n', 'g'); + const readBlock = 1024; + let count = 0; + let offsetDiff = offset.valueOf(); + + // In order to prevent the one-time loading of large files from taking up too much memory + for (let pos = 0; pos < offset; pos += readBlock) { + let startAt = document.positionAt(pos); + let endAt = null; + + if (offsetDiff >= readBlock) { + endAt = document.positionAt(pos + readBlock); + offsetDiff = offsetDiff - readBlock; + } else { + endAt = document.positionAt(pos + offsetDiff); + } + + let text = document.getText(new Range(startAt, endAt)); + let cr = text.match(eolPattern); + + count += cr ? cr.length : 0; + } + return count; +} + +export function arePathsSame(path1: string, path2: string) { + path1 = IS_WINDOWS ? path1.replace(/\//g, "\\") : path1; + path2 = IS_WINDOWS ? path2.replace(/\//g, "\\") : path2; + return path1.toUpperCase() === path2.toUpperCase(); +} + +export function areBasePathsSame(path1: string, path2: string) { + path1 = IS_WINDOWS ? path1.replace(/\//g, "\\") : path1; + path2 = IS_WINDOWS ? path2.replace(/\//g, "\\") : path2; + return path.dirname(path1).toUpperCase() === path.dirname(path2).toUpperCase(); +} +export async function getInterpreterVersion(pythonPath: string) { + return await new Promise((resolve, reject) => { + child_process.execFile(pythonPath, ['--version'], (error, stdout, stdErr) => { + const out = (typeof stdErr === 'string' ? stdErr : '') + os.EOL + (typeof stdout === 'string' ? stdout : ''); + const lines = out.split(/\r?\n/g).map(line => line.trim()).filter(line => line.length > 0); + resolve(lines.length > 0 ? lines[0] : ''); + }); + }); +} diff --git a/src/client/common/versionUtils.ts b/src/client/common/versionUtils.ts new file mode 100644 index 000000000000..e742c9e0b1c6 --- /dev/null +++ b/src/client/common/versionUtils.ts @@ -0,0 +1,22 @@ +import * as semver from 'semver'; + +export class VersionUtils { + public static convertToSemver(version: string) { + const versionParts = (version || '').split('.').filter(item => item.length > 0); + while (versionParts.length < 3) { + versionParts.push('0'); + } + return versionParts.join('.'); + } + public static compareVersion(versionA: string, versionB: string) { + try { + versionA = VersionUtils.convertToSemver(versionA); + versionB = VersionUtils.convertToSemver(versionB); + return semver.gt(versionA, versionB) ? 1 : 0; + } + catch { + return 0; + } + } + +} \ No newline at end of file diff --git a/src/client/debugger/Common/Contracts.ts b/src/client/debugger/Common/Contracts.ts index 8069c9701932..41222ddf0c79 100644 --- a/src/client/debugger/Common/Contracts.ts +++ b/src/client/debugger/Common/Contracts.ts @@ -53,6 +53,7 @@ export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArgum cwd?: string; debugOptions?: string[]; env?: Object; + envFile: string; exceptionHandling?: ExceptionHandling; console?: "none" | "integratedTerminal" | "externalTerminal"; } diff --git a/src/client/debugger/Common/Utils.ts b/src/client/debugger/Common/Utils.ts index a907106263c8..a380451f2877 100644 --- a/src/client/debugger/Common/Utils.ts +++ b/src/client/debugger/Common/Utils.ts @@ -1,8 +1,14 @@ "use strict"; -import {IPythonProcess, IPythonThread, IPythonModule, IPythonEvaluationResult} from "./Contracts"; +import { IPythonProcess, IPythonThread, IPythonModule, IPythonEvaluationResult } from "./Contracts"; import * as path from "path"; import * as fs from 'fs'; +import * as child_process from 'child_process'; +import { mergeEnvVariables, parseEnvFile } from '../../common/envFileParser'; +import * as untildify from 'untildify'; + +export const IS_WINDOWS = /^win/.test(process.platform); +export const PATH_VARIABLE_NAME = IS_WINDOWS ? 'Path' : 'PATH'; const PathValidity: Map = new Map(); export function validatePath(filePath: string): Promise { @@ -66,3 +72,80 @@ export function FixupEscapedUnicodeChars(value: string): string { return value; } +export function getPythonExecutable(pythonPath: string): string { + pythonPath = untildify(pythonPath); + // If only 'python' + if (pythonPath === 'python' || + pythonPath.indexOf(path.sep) === -1 || + path.basename(pythonPath) === path.dirname(pythonPath)) { + return pythonPath; + } + + if (isValidPythonPath(pythonPath)) { + return pythonPath; + } + // Keep python right on top, for backwards compatibility + const KnownPythonExecutables = ['python', 'python4', 'python3.6', 'python3.5', 'python3', 'python2.7', 'python2']; + + for (let executableName of KnownPythonExecutables) { + // Suffix with 'python' for linux and 'osx', and 'python.exe' for 'windows' + if (IS_WINDOWS) { + executableName = executableName + '.exe'; + if (isValidPythonPath(path.join(pythonPath, executableName))) { + return path.join(pythonPath, executableName); + } + if (isValidPythonPath(path.join(pythonPath, 'scripts', executableName))) { + return path.join(pythonPath, 'scripts', executableName); + } + } + else { + if (isValidPythonPath(path.join(pythonPath, executableName))) { + return path.join(pythonPath, executableName); + } + if (isValidPythonPath(path.join(pythonPath, 'bin', executableName))) { + return path.join(pythonPath, 'bin', executableName); + } + } + } + + return pythonPath; +} + +function isValidPythonPath(pythonPath): boolean { + try { + let output = child_process.execFileSync(pythonPath, ['-c', 'print(1234)'], { encoding: 'utf8' }); + return output.startsWith('1234'); + } + catch (ex) { + return false; + } +} + + +export function getCustomEnvVars(envVars: any, envFile: string): any { + let envFileVars = null; + if (typeof envFile === 'string' && envFile.length > 0 && fs.existsSync(envFile)) { + try { + envFileVars = parseEnvFile(envFile); + } + catch (ex) { + console.error('Failed to load env file'); + console.error(ex); + } + } + let configVars = null; + if (envVars && Object.keys(envVars).length > 0 && envFileVars) { + configVars = mergeEnvVariables(envVars, envFileVars); + } + if (envVars && Object.keys(envVars).length > 0) { + configVars = envVars; + } + if (envFileVars) { + configVars = envFileVars; + } + if (configVars && typeof configVars === 'object' && Object.keys(configVars).length > 0) { + return configVars; + } + + return null; +} \ No newline at end of file diff --git a/src/client/debugger/DebugClients/LocalDebugClient.ts b/src/client/debugger/DebugClients/LocalDebugClient.ts index e785d23a11cd..2cbc911d5b80 100644 --- a/src/client/debugger/DebugClients/LocalDebugClient.ts +++ b/src/client/debugger/DebugClients/LocalDebugClient.ts @@ -8,6 +8,8 @@ import * as child_process from 'child_process'; import { LaunchRequestArguments } from '../Common/Contracts'; import { DebugClient, DebugType } from './DebugClient'; import { open } from '../../common/open'; +import { getCustomEnvVars } from '../Common/Utils'; + const VALID_DEBUG_OPTIONS = ['WaitOnAbnormalExit', 'WaitOnNormalExit', 'RedirectOutput', @@ -64,16 +66,17 @@ export class LocalDebugClient extends DebugClient { } public LaunchApplicationToDebug(dbgServer: IDebugServer, processErrored: (error: any) => void): Promise { return new Promise((resolve, reject) => { - let fileDir = path.dirname(this.args.program); + let fileDir = this.args && this.args.program ? path.dirname(this.args.program) : ''; let processCwd = fileDir; - if (typeof this.args.cwd === 'string' && this.args.cwd.length > 0) { + if (typeof this.args.cwd === 'string' && this.args.cwd.length > 0 && this.args.cwd !== 'null') { processCwd = this.args.cwd; } let pythonPath = 'python'; if (typeof this.args.pythonPath === 'string' && this.args.pythonPath.trim().length > 0) { pythonPath = this.args.pythonPath; } - let environmentVariables = this.args.env ? this.args.env : {}; + let environmentVariables = getCustomEnvVars(this.args.env, this.args.envFile); + environmentVariables = environmentVariables ? environmentVariables : {}; let newEnvVars = {}; if (environmentVariables) { for (let setting in environmentVariables) { @@ -93,6 +96,11 @@ export class LocalDebugClient extends DebugClient { newEnvVars['PYTHONIOENCODING'] = 'UTF-8'; process.env['PYTHONIOENCODING'] = 'UTF-8'; } + if (!environmentVariables.hasOwnProperty('PYTHONUNBUFFERED')) { + environmentVariables['PYTHONUNBUFFERED'] = '1'; + newEnvVars['PYTHONUNBUFFERED'] = '1'; + process.env['PYTHONUNBUFFERED'] = '1'; + } let ptVSToolsFilePath = this.getPTVSToolsFilePath(); let launcherArgs = this.buildLauncherArguments(); diff --git a/src/client/debugger/DebugClients/NonDebugClient.ts b/src/client/debugger/DebugClients/NonDebugClient.ts index 5c94c3773d11..c2619d185fd5 100644 --- a/src/client/debugger/DebugClients/NonDebugClient.ts +++ b/src/client/debugger/DebugClients/NonDebugClient.ts @@ -42,7 +42,7 @@ export class NonDebugClient extends DebugClient { return new Promise((resolve, reject) => { let fileDir = path.dirname(this.args.program); let processCwd = fileDir; - if (typeof this.args.cwd === "string" && this.args.cwd.length > 0) { + if (typeof this.args.cwd === "string" && this.args.cwd.length > 0 && this.args.cwd !== 'null') { processCwd = this.args.cwd; } let pythonPath = "python"; diff --git a/src/client/debugger/DebugServers/BaseDebugServer.ts b/src/client/debugger/DebugServers/BaseDebugServer.ts index 3820ff780208..0513b5c07223 100644 --- a/src/client/debugger/DebugServers/BaseDebugServer.ts +++ b/src/client/debugger/DebugServers/BaseDebugServer.ts @@ -1,8 +1,7 @@ "use strict"; -import {DebugSession, OutputEvent} from "vscode-debugadapter"; +import {DebugSession} from "vscode-debugadapter"; import {IPythonProcess, IDebugServer} from "../Common/Contracts"; -import * as net from "net"; import {EventEmitter} from "events"; import {Deferred, createDeferred} from '../../common/helpers'; @@ -14,7 +13,7 @@ export abstract class BaseDebugServer extends EventEmitter { public get IsRunning(): boolean { return this.isRunning; } - protected debugClientConnected: Deferred; + protected debugClientConnected: Deferred; public get DebugClientConnected(): Promise { return this.debugClientConnected.promise; } @@ -22,7 +21,7 @@ export abstract class BaseDebugServer extends EventEmitter { super(); this.debugSession = debugSession; this.pythonProcess = pythonProcess; - this.debugClientConnected = createDeferred(); + this.debugClientConnected = createDeferred(); } public abstract Start(): Promise; diff --git a/src/client/debugger/Main.ts b/src/client/debugger/Main.ts index 5cf08b201cf8..5b6b89f849a8 100644 --- a/src/client/debugger/Main.ts +++ b/src/client/debugger/Main.ts @@ -1,26 +1,21 @@ "use strict"; -import { Variable, DebugSession, InitializedEvent, TerminatedEvent, StoppedEvent, OutputEvent, Thread, StackFrame, Scope, Source, Handles } from "vscode-debugadapter"; +import { DebugSession, InitializedEvent, TerminatedEvent, StoppedEvent, OutputEvent, Thread, StackFrame, Scope, Source, Handles } from "vscode-debugadapter"; import { ThreadEvent } from "vscode-debugadapter"; import { DebugProtocol } from "vscode-debugprotocol"; -import { readFileSync } from "fs"; -import { basename } from "path"; import * as path from "path"; -import * as os from "os"; import * as fs from "fs"; -import * as child_process from "child_process"; -import * as StringDecoder from "string_decoder"; -import * as net from "net"; import { PythonProcess } from "./PythonProcess"; -import { FrameKind, IPythonProcess, IPythonThread, IPythonModule, IPythonEvaluationResult, IPythonStackFrame, IDebugServer } from "./Common/Contracts"; +import { IPythonThread, IPythonModule, IPythonEvaluationResult, IPythonStackFrame, IDebugServer } from "./Common/Contracts"; import { IPythonBreakpoint, PythonBreakpointConditionKind, PythonBreakpointPassCountKind, IPythonException, PythonEvaluationResultReprKind, enum_EXCEPTION_STATE } from "./Common/Contracts"; import { BaseDebugServer } from "./DebugServers/BaseDebugServer"; -import { DebugClient, DebugType } from "./DebugClients/DebugClient"; +import { DebugClient } from "./DebugClients/DebugClient"; import { CreateAttachDebugClient, CreateLaunchDebugClient } from "./DebugClients/DebugFactory"; -import { DjangoApp, LaunchRequestArguments, AttachRequestArguments, DebugFlags, DebugOptions, TelemetryEvent, PythonEvaluationResultFlags } from "./Common/Contracts"; -import * as telemetryContracts from "../common/telemetryContracts"; -import { validatePath, validatePathSync } from './Common/Utils'; +import { LaunchRequestArguments, AttachRequestArguments, DebugOptions, TelemetryEvent, PythonEvaluationResultFlags } from "./Common/Contracts"; +import { validatePath, getPythonExecutable } from './Common/Utils'; import { isNotInstalledError } from '../common/helpers'; +import { DEBUGGER } from '../../client/telemetry/constants'; +import { DebuggerTelemetry } from '../../client/telemetry/types'; const CHILD_ENUMEARATION_TIMEOUT = 5000; @@ -81,7 +76,10 @@ export class PythonDebugger extends DebugSession { private debugServer: BaseDebugServer; private startDebugServer(): Promise { - let programDirectory = this.launchArgs ? path.dirname(this.launchArgs.program) : this.attachArgs.localRoot; + let programDirectory = ''; + if ((this.launchArgs && this.launchArgs.program) || (this.attachArgs && this.attachArgs.localRoot)) { + programDirectory = this.launchArgs ? path.dirname(this.launchArgs.program) : this.attachArgs.localRoot; + } if (this.launchArgs && typeof this.launchArgs.cwd === 'string' && this.launchArgs.cwd.length > 0 && this.launchArgs.cwd !== 'null') { programDirectory = this.launchArgs.cwd; } @@ -101,7 +99,7 @@ export class PythonDebugger extends DebugSession { } } private InitializeEventHandlers() { - this.pythonProcess.on("last", arg => this.onDetachDebugger()); + this.pythonProcess.on("last", arg => this.onLastCommand()); this.pythonProcess.on("threadExited", arg => this.onPythonThreadExited(arg)); this.pythonProcess.on("moduleLoaded", arg => this.onPythonModuleLoaded(arg)); this.pythonProcess.on("threadCreated", arg => this.onPythonThreadCreated(arg)); @@ -116,6 +114,17 @@ export class PythonDebugger extends DebugSession { this.debugServer.on("detach", () => this.onDetachDebugger()); } + private onLastCommand() { + // If we're running in terminal (integrated or external) + // Then don't stop the debug server + if (this.launchArgs && (this.launchArgs.console === "externalTerminal" || + this.launchArgs.console === "integratedTerminal")) { + return; + } + + // Else default behaviour as previous, which was to perform the same as onDetachDebugger + this.onDetachDebugger(); + } private onDetachDebugger() { this.stopDebugServer(); this.sendEvent(new TerminatedEvent()); @@ -180,6 +189,26 @@ export class PythonDebugger extends DebugSession { return Promise.resolve(true); } protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { + // Some versions may still exist with incorrect launch.json values + const setting = '${config.python.pythonPath}'; + if (args.pythonPath === setting) { + return this.sendErrorResponse(response, 2001, `Invalid launch.json (re-create it or replace 'config.python.pythonPath' with 'config:python.pythonPath')`); + } + // Add support for specifying just the directory where the python executable will be located + // E.g. virtual directory name + try { + args.pythonPath = getPythonExecutable(args.pythonPath); + } + catch (ex) { + } + if (Array.isArray(args.debugOptions) && args.debugOptions.indexOf("Pyramid") >= 0) { + if (fs.existsSync(args.pythonPath)) { + args.program = path.join(path.dirname(args.pythonPath), "pserve"); + } + else { + args.program = "pserve"; + } + } // Confirm the file exists if (typeof args.module !== 'string' || args.module.length === 0) { if (!fs.existsSync(args.program)) { @@ -192,13 +221,26 @@ export class PythonDebugger extends DebugSession { return this.sendErrorResponse(response, 2001, `'cwd' in 'launch.json' needs to point to the working directory`); } } - this.sendEvent(new TelemetryEvent(telemetryContracts.Debugger.Load, { - Debug_Console: args.console, - Debug_DebugOptions: args.debugOptions.join(","), - Debug_DJango: args.debugOptions.indexOf("DjangoDebugging") >= 0 ? "true" : "false", - Debug_PySpark: typeof args.pythonPath === 'string' && args.pythonPath.indexOf('spark-submit') > 0 ? 'true' : 'false', - Debug_HasEnvVaraibles: args.env && typeof args.env === "object" ? "true" : "false" - })); + + let programDirectory = ''; + if (args && args.program) { + programDirectory = path.dirname(args.program); + } + if (args && typeof args.cwd === 'string' && args.cwd.length > 0 && args.cwd !== 'null') { + programDirectory = args.cwd; + } + if (programDirectory.length > 0 && fs.existsSync(path.join(programDirectory, 'pyenv.cfg'))) { + this.sendEvent(new OutputEvent(`Warning 'pyenv.cfg' can interfere with the debugger. Please rename or delete this file (temporary solution)`)); + } + + const telemetryProps: DebuggerTelemetry = { + trigger: 'launch', + console: args.console, + debugOptions: args.debugOptions.join(","), + pyspark: typeof args.pythonPath === 'string' && args.pythonPath.indexOf('spark-submit') > 0, + hasEnvVars: args.env && typeof args.env === "object" && Object.keys(args.env).length > 0 + }; + this.sendEvent(new TelemetryEvent(DEBUGGER, telemetryProps)); this.launchArgs = args; this.debugClient = CreateLaunchDebugClient(args, this); @@ -234,7 +276,8 @@ export class PythonDebugger extends DebugSession { this.sendEvent(new TerminatedEvent()); } protected attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments) { - this.sendEvent(new TelemetryEvent(telemetryContracts.Debugger.Attach)); + this.sendEvent(new TelemetryEvent(DEBUGGER, { trigger: 'attach' })); + this.attachArgs = args; this.debugClient = CreateAttachDebugClient(args, this); this.entryResponse = response; @@ -301,11 +344,10 @@ export class PythonDebugger extends DebugSession { } let breakpoints: { verified: boolean, line: number }[] = []; - let breakpointsToRemove = []; let linesToAdd = args.breakpoints.map(b => b.line); let registeredBks = this.registeredBreakpointsByFileName.get(args.source.path); let linesToRemove = registeredBks.map(b => b.LineNo).filter(oldLine => linesToAdd.indexOf(oldLine) === -1); - let linesToUpdate = registeredBks.map(b => b.LineNo).filter(oldLine => linesToAdd.indexOf(oldLine) >= 0); + // let linesToUpdate = registeredBks.map(b => b.LineNo).filter(oldLine => linesToAdd.indexOf(oldLine) >= 0); // Always add new breakpoints, don't re-enable previous breakpoints // Cuz sometimes some breakpoints get added too early (e.g. in django) and don't get registeredBks diff --git a/src/client/debugger/PythonProcessCallbackHandler.ts b/src/client/debugger/PythonProcessCallbackHandler.ts index 6a03a51a9af3..a11dc85e82e1 100644 --- a/src/client/debugger/PythonProcessCallbackHandler.ts +++ b/src/client/debugger/PythonProcessCallbackHandler.ts @@ -49,6 +49,7 @@ export class PythonProcessCallbackHandler extends EventEmitter { case "CHLD": this.HandleEnumChildren(); break; case "REQH": this.HandleRequestHandlers(); break; case "EXCP": this.HandleException(); break; + case "EXC2": this.HandleRichException(); break; case "EXCR": this.HandleExecutionResult(); break; case "EXCE": this.HandleExecutionException(); break; case "ASBR": this.HandleAsyncBreak(); break; @@ -270,6 +271,28 @@ export class PythonProcessCallbackHandler extends EventEmitter { } this._stoppedForException = true; } + private HandleRichException() { + // let typeName = this.stream.ReadString(); + // let threadId = this.stream.ReadInt64(); + // let breakType = this.stream.ReadInt32(); + // let desc = this.stream.ReadString(); + // if (this.stream.HasInsufficientDataForReading) { + // return; + // } + + // if (typeName != null && desc != null) { + // let ex: IPythonException = { + // TypeName: typeName, + // Description: desc + // }; + // let pyThread: IPythonThread; + // if (this.process.Threads.has(threadId)) { + // pyThread = this.process.Threads.get(threadId); + // } + // this.emit("exceptionRaised", pyThread, ex, breakType === 1 /* BREAK_TYPE_UNHANLDED */); + // } + // this._stoppedForException = true; + } private HandleExecutionException() { let execId = this.stream.ReadInt32(); let exceptionText = this.stream.ReadString(); diff --git a/src/client/debugger/configProviders/simpleProvider.ts b/src/client/debugger/configProviders/simpleProvider.ts new file mode 100644 index 000000000000..391edd782d99 --- /dev/null +++ b/src/client/debugger/configProviders/simpleProvider.ts @@ -0,0 +1,66 @@ +import * as path from 'path'; +import { PythonSettings } from '../../common/configSettings'; +import { CancellationToken, DebugConfiguration, DebugConfigurationProvider, ProviderResult, Uri, window, workspace, WorkspaceFolder } from 'vscode'; + +type PythonDebugConfiguration = DebugConfiguration & { + stopOnEntry?: boolean, + pythonPath?: string, + program?: string, + cwd?: string, + env?: object, + envFile?: string, + debugOptions?: string[] +}; + +export class SimpleConfigurationProvider implements DebugConfigurationProvider { + private getProgram(config: PythonDebugConfiguration): string | undefined { + const editor = window.activeTextEditor; + if (editor && editor.document.languageId === 'python') { + return editor.document.fileName; + } + return undefined; + } + private getWorkspaceFolder(config: PythonDebugConfiguration): string | undefined { + const program = this.getProgram(config); + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + return program ? path.dirname(program) : undefined; + } + if (workspace.workspaceFolders.length === 1) { + return workspace.workspaceFolders[0].uri.fsPath; + } + if (program) { + const workspaceFolder = workspace.getWorkspaceFolder(Uri.file(program)); + if (workspaceFolder) { + return workspaceFolder.uri.fsPath; + } + } + + return undefined; + } + resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): ProviderResult { + if (Object.keys(debugConfiguration).length > 0) { + return debugConfiguration; + } + + const config = debugConfiguration as PythonDebugConfiguration; + const defaultProgram = this.getProgram(config); + const workspaceFolder = this.getWorkspaceFolder(config); + const envFile = workspaceFolder ? path.join(workspaceFolder, '.env') : undefined; + return { + name: 'Launch', + type: 'python', + request: 'launch', + stopOnEntry: true, + pythonPath: PythonSettings.getInstance(workspaceFolder ? Uri.file(workspaceFolder) : undefined).pythonPath, + program: defaultProgram, + cwd: workspaceFolder, + envFile, + env: {}, + debugOptions: [ + 'WaitOnAbnormalExit', + 'WaitOnNormalExit', + 'RedirectOutput' + ] + }; + } +} diff --git a/src/client/debugger/index.ts b/src/client/debugger/index.ts new file mode 100644 index 000000000000..d2343dd26a9e --- /dev/null +++ b/src/client/debugger/index.ts @@ -0,0 +1 @@ +export * from './configProviders/simpleProvider'; \ No newline at end of file diff --git a/src/client/extension.ts b/src/client/extension.ts index cbf1336e59c6..d1fd4df30fba 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -1,47 +1,56 @@ 'use strict'; - +import * as os from 'os'; import * as vscode from 'vscode'; +import { BannerService } from './banner'; +import * as settings from './common/configSettings'; +import { createDeferred } from './common/helpers'; +import { PersistentStateFactory } from './common/persistentState'; +import { SimpleConfigurationProvider } from './debugger'; +import { FeedbackService } from './feedback'; +import { InterpreterManager } from './interpreter'; +import { SetInterpreterProvider } from './interpreter/configuration/setInterpreterProvider'; +import { ShebangCodeLensProvider } from './interpreter/display/shebangCodeLensProvider'; +import { getCondaVersion } from './interpreter/helpers'; +import { InterpreterVersionService } from './interpreter/interpreterVersion'; +import * as jup from './jupyter/main'; +import { JupyterProvider } from './jupyter/provider'; +import { JediFactory } from './languageServices/jediProxyFactory'; import { PythonCompletionItemProvider } from './providers/completionProvider'; -import { PythonHoverProvider } from './providers/hoverProvider'; import { PythonDefinitionProvider } from './providers/definitionProvider'; -import { PythonReferenceProvider } from './providers/referenceProvider'; -import { PythonRenameProvider } from './providers/renameProvider'; +import { activateExecInTerminalProvider } from './providers/execInTerminalProvider'; +import { activateFormatOnSaveProvider } from './providers/formatOnSaveProvider'; import { PythonFormattingEditProvider } from './providers/formatProvider'; -import * as sortImports from './sortImports'; +import { PythonHoverProvider } from './providers/hoverProvider'; import { LintProvider } from './providers/lintProvider'; -import { PythonSymbolProvider } from './providers/symbolProvider'; +import { activateGoToObjectDefinitionProvider } from './providers/objectDefinitionProvider'; +import { PythonReferenceProvider } from './providers/referenceProvider'; +import { PythonRenameProvider } from './providers/renameProvider'; +import { ReplProvider } from './providers/replProvider'; import { PythonSignatureProvider } from './providers/signatureProvider'; -import * as settings from './common/configSettings'; -import * as telemetryHelper from './common/telemetry'; -import * as telemetryContracts from './common/telemetryContracts'; import { activateSimplePythonRefactorProvider } from './providers/simpleRefactorProvider'; -import { activateSetInterpreterProvider } from './providers/setInterpreterProvider'; -import { activateExecInTerminalProvider } from './providers/execInTerminalProvider'; -import { Commands } from './common/constants'; -import * as tests from './unittests/main'; -import * as jup from './jupyter/main'; -import { HelpProvider } from './helpProvider'; +import { PythonSymbolProvider } from './providers/symbolProvider'; import { activateUpdateSparkLibraryProvider } from './providers/updateSparkLibraryProvider'; -import { activateFormatOnSaveProvider } from './providers/formatOnSaveProvider'; -import { WorkspaceSymbols } from './workspaceSymbols/main'; +import * as sortImports from './sortImports'; +import { sendTelemetryEvent } from './telemetry'; +import { EDITOR_LOAD } from './telemetry/constants'; +import { StopWatch } from './telemetry/stopWatch'; import { BlockFormatProviders } from './typeFormatters/blockFormatProvider'; -import * as os from 'os'; - +import * as tests from './unittests/main'; +import { WorkspaceSymbols } from './workspaceSymbols/main'; -const PYTHON: vscode.DocumentFilter = { language: 'python', scheme: 'file' }; +const PYTHON: vscode.DocumentFilter = { language: 'python' }; let unitTestOutChannel: vscode.OutputChannel; let formatOutChannel: vscode.OutputChannel; let lintingOutChannel: vscode.OutputChannel; let jupMain: jup.Jupyter; +const activationDeferred = createDeferred(); +export const activated = activationDeferred.promise; +// tslint:disable-next-line:max-func-body-length +export async function activate(context: vscode.ExtensionContext) { + const pythonSettings = settings.PythonSettings.getInstance(); + // tslint:disable-next-line:no-floating-promises + sendStartupTelemetry(activated); -export function activate(context: vscode.ExtensionContext) { - let pythonSettings = settings.PythonSettings.getInstance(); - const hasPySparkInCompletionPath = pythonSettings.autoComplete.extraPaths.some(p => p.toLowerCase().indexOf('spark') >= 0); - telemetryHelper.sendTelemetryEvent(telemetryContracts.EVENT_LOAD, { - CodeComplete_Has_ExtraPaths: pythonSettings.autoComplete.extraPaths.length > 0 ? 'true' : 'false', - Format_Has_Custom_Python_Path: pythonSettings.pythonPath.length !== 'python'.length ? 'true' : 'false', - Has_PySpark_Path: hasPySparkInCompletionPath ? 'true' : 'false' - }); lintingOutChannel = vscode.window.createOutputChannel(pythonSettings.linting.outputWindow); formatOutChannel = lintingOutChannel; if (pythonSettings.linting.outputWindow !== pythonSettings.formatting.outputWindow) { @@ -54,20 +63,25 @@ export function activate(context: vscode.ExtensionContext) { } sortImports.activate(context, formatOutChannel); - context.subscriptions.push(activateSetInterpreterProvider()); + const interpreterManager = new InterpreterManager(); + await interpreterManager.autoSetInterpreter(); + // tslint:disable-next-line:no-floating-promises + interpreterManager.refresh(); + context.subscriptions.push(interpreterManager); + const interpreterVersionService = new InterpreterVersionService(); + context.subscriptions.push(new SetInterpreterProvider(interpreterManager, interpreterVersionService)); context.subscriptions.push(...activateExecInTerminalProvider()); context.subscriptions.push(activateUpdateSparkLibraryProvider()); activateSimplePythonRefactorProvider(context, formatOutChannel); - context.subscriptions.push(activateFormatOnSaveProvider(PYTHON, settings.PythonSettings.getInstance(), formatOutChannel)); + context.subscriptions.push(activateFormatOnSaveProvider(PYTHON, formatOutChannel)); + const jediFactory = new JediFactory(context.asAbsolutePath('.')); + context.subscriptions.push(...activateGoToObjectDefinitionProvider(jediFactory)); - context.subscriptions.push(vscode.commands.registerCommand(Commands.Start_REPL, () => { - let term = vscode.window.createTerminal('Python', pythonSettings.pythonPath); - term.show(); - context.subscriptions.push(term); - })); + context.subscriptions.push(new ReplProvider()); // Enable indentAction - vscode.languages.setLanguageConfiguration(PYTHON.language, { + // tslint:disable-next-line:no-non-null-assertion + vscode.languages.setLanguageConfiguration(PYTHON.language!, { onEnterRules: [ { beforeText: /^\s*(?:def|class|for|if|elif|else|while|try|with|finally|except|async).*?:\s*$/, @@ -77,34 +91,61 @@ export function activate(context: vscode.ExtensionContext) { beforeText: /^ *#.*$/, afterText: /.+$/, action: { indentAction: vscode.IndentAction.None, appendText: '# ' } + }, + { + beforeText: /^\s+(continue|break|return)\b.*$/, + action: { indentAction: vscode.IndentAction.Outdent } } ] }); + context.subscriptions.push(jediFactory); context.subscriptions.push(vscode.languages.registerRenameProvider(PYTHON, new PythonRenameProvider(formatOutChannel))); - const definitionProvider = new PythonDefinitionProvider(context); - const jediProx = definitionProvider.JediProxy; + const definitionProvider = new PythonDefinitionProvider(jediFactory); context.subscriptions.push(vscode.languages.registerDefinitionProvider(PYTHON, definitionProvider)); - context.subscriptions.push(vscode.languages.registerHoverProvider(PYTHON, new PythonHoverProvider(context, jediProx))); - context.subscriptions.push(vscode.languages.registerReferenceProvider(PYTHON, new PythonReferenceProvider(context, jediProx))); - context.subscriptions.push(vscode.languages.registerCompletionItemProvider(PYTHON, new PythonCompletionItemProvider(context, jediProx), '.')); + context.subscriptions.push(vscode.languages.registerHoverProvider(PYTHON, new PythonHoverProvider(jediFactory))); + context.subscriptions.push(vscode.languages.registerReferenceProvider(PYTHON, new PythonReferenceProvider(jediFactory))); + context.subscriptions.push(vscode.languages.registerCompletionItemProvider(PYTHON, new PythonCompletionItemProvider(jediFactory), '.')); + context.subscriptions.push(vscode.languages.registerCodeLensProvider(PYTHON, new ShebangCodeLensProvider())); - context.subscriptions.push(vscode.languages.registerDocumentSymbolProvider(PYTHON, new PythonSymbolProvider(context, jediProx))); + const symbolProvider = new PythonSymbolProvider(jediFactory); + context.subscriptions.push(vscode.languages.registerDocumentSymbolProvider(PYTHON, symbolProvider)); if (pythonSettings.devOptions.indexOf('DISABLE_SIGNATURE') === -1) { - context.subscriptions.push(vscode.languages.registerSignatureHelpProvider(PYTHON, new PythonSignatureProvider(context, jediProx), '(', ',')); + context.subscriptions.push(vscode.languages.registerSignatureHelpProvider(PYTHON, new PythonSignatureProvider(jediFactory), '(', ',')); + } + if (pythonSettings.formatting.provider !== 'none') { + const formatProvider = new PythonFormattingEditProvider(context, formatOutChannel); + context.subscriptions.push(vscode.languages.registerDocumentFormattingEditProvider(PYTHON, formatProvider)); + context.subscriptions.push(vscode.languages.registerDocumentRangeFormattingEditProvider(PYTHON, formatProvider)); } - const formatProvider = new PythonFormattingEditProvider(context, formatOutChannel, pythonSettings); - context.subscriptions.push(vscode.languages.registerDocumentFormattingEditProvider(PYTHON, formatProvider)); - context.subscriptions.push(vscode.languages.registerDocumentRangeFormattingEditProvider(PYTHON, formatProvider)); - - - jupMain = new jup.Jupyter(lintingOutChannel); - const documentHasJupyterCodeCells = jupMain.hasCodeCells.bind(jupMain); - jupMain.activate(); - context.subscriptions.push(jupMain); - context.subscriptions.push(new LintProvider(context, lintingOutChannel, documentHasJupyterCodeCells)); - tests.activate(context, unitTestOutChannel); + const jupyterExtInstalled = vscode.extensions.getExtension('donjayamanne.jupyter'); + // tslint:disable-next-line:promise-function-async + const linterProvider = new LintProvider(context, lintingOutChannel, (a, b) => Promise.resolve(false)); + context.subscriptions.push(); + if (jupyterExtInstalled) { + if (jupyterExtInstalled.isActive) { + // tslint:disable-next-line:no-unsafe-any + jupyterExtInstalled.exports.registerLanguageProvider(PYTHON.language, new JupyterProvider()); + // tslint:disable-next-line:no-unsafe-any + linterProvider.documentHasJupyterCodeCells = jupyterExtInstalled.exports.hasCodeCells; + } + + jupyterExtInstalled.activate().then(() => { + // tslint:disable-next-line:no-unsafe-any + jupyterExtInstalled.exports.registerLanguageProvider(PYTHON.language, new JupyterProvider()); + // tslint:disable-next-line:no-unsafe-any + linterProvider.documentHasJupyterCodeCells = jupyterExtInstalled.exports.hasCodeCells; + }); + } else { + jupMain = new jup.Jupyter(lintingOutChannel); + const documentHasJupyterCodeCells = jupMain.hasCodeCells.bind(jupMain); + jupMain.activate(); + context.subscriptions.push(jupMain); + // tslint:disable-next-line:no-unsafe-any + linterProvider.documentHasJupyterCodeCells = documentHasJupyterCodeCells; + } + tests.activate(context, unitTestOutChannel, symbolProvider); context.subscriptions.push(new WorkspaceSymbols(lintingOutChannel)); @@ -113,10 +154,27 @@ export function activate(context: vscode.ExtensionContext) { const triggerCharacters: string[] = os.EOL.split(''); triggerCharacters.shift(); - const hepProvider = new HelpProvider(); - context.subscriptions.push(hepProvider); + context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider('python', new SimpleConfigurationProvider())); + activationDeferred.resolve(); + + const persistentStateFactory = new PersistentStateFactory(context.globalState, context.workspaceState); + const feedbackService = new FeedbackService(persistentStateFactory); + context.subscriptions.push(feedbackService); + // tslint:disable-next-line:no-unused-expression + new BannerService(persistentStateFactory); } -// this method is called when your extension is deactivated -export function deactivate() { -} \ No newline at end of file +async function sendStartupTelemetry(activatedPromise: Promise) { + const stopWatch = new StopWatch(); + // tslint:disable-next-line:no-floating-promises + activatedPromise.then(async () => { + const duration = stopWatch.elapsedTime; + let condaVersion: string | undefined; + try { + condaVersion = await getCondaVersion(); + // tslint:disable-next-line:no-empty + } catch { } + const props = condaVersion ? { condaVersion } : undefined; + sendTelemetryEvent(EDITOR_LOAD, duration, props); + }); +} diff --git a/src/client/feedback/counters.ts b/src/client/feedback/counters.ts new file mode 100644 index 000000000000..6c67d2cdacba --- /dev/null +++ b/src/client/feedback/counters.ts @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { EventEmitter } from 'events'; + +const THRESHOLD_FOR_FEATURE_USAGE = 1000; +const THRESHOLD_FOR_TEXT_EDIT = 5000; + +const FEARTURES_USAGE_COUNTER = 'FEARTURES_USAGE'; +const TEXT_EDIT_COUNTER = 'TEXT_EDIT'; +type counters = 'FEARTURES_USAGE' | 'TEXT_EDIT'; + +export class FeedbackCounters extends EventEmitter { + private counters = new Map(); + constructor() { + super(); + this.createCounters(); + } + public incrementEditCounter(): void { + this.incrementCounter(TEXT_EDIT_COUNTER); + } + public incrementFeatureUsageCounter(): void { + this.incrementCounter(FEARTURES_USAGE_COUNTER); + } + private createCounters() { + this.counters.set(TEXT_EDIT_COUNTER, { counter: 0, threshold: THRESHOLD_FOR_TEXT_EDIT }); + this.counters.set(FEARTURES_USAGE_COUNTER, { counter: 0, threshold: THRESHOLD_FOR_FEATURE_USAGE }); + } + private incrementCounter(counterName: counters): void { + if (!this.counters.has(counterName)) { + console.error(`Counter ${counterName} not supported in the feedback module of the Python Extension`); + return; + } + + // tslint:disable-next-line:no-non-null-assertion + const value = this.counters.get(counterName)!; + value.counter += 1; + + this.checkThreshold(counterName); + } + private checkThreshold(counterName: string) { + // tslint:disable-next-line:no-non-null-assertion + const value = this.counters.get(counterName)!; + if (value.counter < value.threshold) { + return; + } + + this.emit('thresholdReached'); + } +} diff --git a/src/client/feedback/feedbackService.ts b/src/client/feedback/feedbackService.ts new file mode 100644 index 000000000000..261ce617ae6d --- /dev/null +++ b/src/client/feedback/feedbackService.ts @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as child_process from 'child_process'; +import * as os from 'os'; +import { window } from 'vscode'; +import { commands, Disposable, TextDocument, workspace } from 'vscode'; +import { PythonLanguage } from '../common/constants'; +import { IPersistentStateFactory, PersistentState } from '../common/persistentState'; +import { FEEDBACK } from '../telemetry/constants'; +import { captureTelemetry, sendTelemetryEvent } from '../telemetry/index'; +import { FeedbackCounters } from './counters'; + +const FEEDBACK_URL = 'https://www.surveymonkey.com/r/293C9HY'; + +export class FeedbackService implements Disposable { + private counters?: FeedbackCounters; + private showFeedbackPrompt: PersistentState; + private userResponded: PersistentState; + private promptDisplayed: boolean; + private disposables: Disposable[] = []; + private get canShowPrompt(): boolean { + return this.showFeedbackPrompt.value && !this.userResponded.value && + !this.promptDisplayed && this.counters !== undefined; + } + constructor(persistentStateFactory: IPersistentStateFactory) { + this.showFeedbackPrompt = persistentStateFactory.createGlobalPersistentState('SHOW_FEEDBACK_PROMPT', true); + this.userResponded = persistentStateFactory.createGlobalPersistentState('RESPONDED_TO_FEEDBACK', false); + if (this.showFeedbackPrompt.value && !this.userResponded.value) { + this.initialize(); + } + } + public dispose() { + this.counters = undefined; + this.disposables.forEach(disposable => { + // tslint:disable-next-line:no-unsafe-any + disposable.dispose(); + }); + this.disposables = []; + } + private initialize() { + // tslint:disable-next-line:no-void-expression + let commandDisable = commands.registerCommand('python.updateFeedbackCounter', (telemetryEventName: string) => this.updateFeedbackCounter(telemetryEventName)); + this.disposables.push(commandDisable); + // tslint:disable-next-line:no-void-expression + commandDisable = workspace.onDidChangeTextDocument(changeEvent => this.handleChangesToTextDocument(changeEvent.document), this, this.disposables); + this.disposables.push(commandDisable); + + this.counters = new FeedbackCounters(); + this.counters.on('thresholdReached', () => { + this.thresholdHandler(); + }); + } + private handleChangesToTextDocument(textDocument: TextDocument) { + if (textDocument.languageId !== PythonLanguage.language) { + return; + } + if (!this.canShowPrompt) { + return; + } + this.counters.incrementEditCounter(); + } + private updateFeedbackCounter(telemetryEventName: string): void { + // Ignore feedback events. + if (telemetryEventName === FEEDBACK) { + return; + } + if (!this.canShowPrompt) { + return; + } + this.counters.incrementFeatureUsageCounter(); + } + private thresholdHandler() { + if (!this.canShowPrompt) { + return; + } + this.showPrompt(); + } + private showPrompt() { + this.promptDisplayed = true; + + const message = 'Would you tell us how likely you are to recommend the Microsoft Python extension for VS Code to a friend or colleague?'; + const yesButton = 'Yes'; + const dontShowAgainButton = 'Don\'t Show Again'; + window.showInformationMessage(message, yesButton, dontShowAgainButton).then((value) => { + switch (value) { + case yesButton: { + this.displaySurvey(); + break; + } + case dontShowAgainButton: { + this.doNotShowFeedbackAgain(); + break; + } + default: { + sendTelemetryEvent(FEEDBACK, undefined, { action: 'dismissed' }); + break; + } + } + // Stop everything for this session. + this.dispose(); + }); + } + @captureTelemetry(FEEDBACK, { action: 'accepted' }) + private displaySurvey() { + this.userResponded.value = true; + + let openCommand: string | undefined; + if (os.platform() === 'win32') { + openCommand = 'explorer'; + } else if (os.platform() === 'darwin') { + openCommand = '/usr/bin/open'; + } else { + openCommand = '/usr/bin/xdg-open'; + } + if (!openCommand) { + console.error(`Unable to determine platform to capture user feedback in Python extension ${os.platform()}`); + console.error(`Survey link is: ${FEEDBACK_URL}`); + } + child_process.spawn(openCommand, [FEEDBACK_URL]); + } + @captureTelemetry(FEEDBACK, { action: 'doNotShowAgain' }) + private doNotShowFeedbackAgain() { + this.showFeedbackPrompt.value = false; + } +} diff --git a/src/client/feedback/index.ts b/src/client/feedback/index.ts new file mode 100644 index 000000000000..38982500c51c --- /dev/null +++ b/src/client/feedback/index.ts @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +export * from './feedbackService'; diff --git a/src/client/formatters/autoPep8Formatter.ts b/src/client/formatters/autoPep8Formatter.ts index 8a81c1dc01be..0da76fd8d129 100644 --- a/src/client/formatters/autoPep8Formatter.ts +++ b/src/client/formatters/autoPep8Formatter.ts @@ -1,22 +1,33 @@ 'use strict'; import * as vscode from 'vscode'; -import { BaseFormatter } from './baseFormatter'; -import * as settings from '../common/configSettings'; +import { PythonSettings } from '../common/configSettings'; import { Product } from '../common/installer'; +import { sendTelemetryWhenDone } from '../telemetry'; +import { FORMAT } from '../telemetry/constants'; +import { StopWatch } from '../telemetry/stopWatch'; +import { BaseFormatter } from './baseFormatter'; export class AutoPep8Formatter extends BaseFormatter { - constructor(outputChannel: vscode.OutputChannel, pythonSettings: settings.IPythonSettings, workspaceRootPath?: string) { - super('autopep8', Product.autopep8, outputChannel, pythonSettings, workspaceRootPath); + constructor(outputChannel: vscode.OutputChannel) { + super('autopep8', Product.autopep8, outputChannel); } public formatDocument(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, range?: vscode.Range): Thenable { - let autopep8Path = this.pythonSettings.formatting.autopep8Path; - let autoPep8Args = Array.isArray(this.pythonSettings.formatting.autopep8Args) ? this.pythonSettings.formatting.autopep8Args : []; - autoPep8Args = autoPep8Args.concat(['--diff']); - if (range && !range.isEmpty) { - autoPep8Args = autoPep8Args.concat(['--line-range', (range.start.line + 1).toString(), (range.end.line + 1).toString()]); + const stopWatch = new StopWatch(); + const settings = PythonSettings.getInstance(document.uri); + const autopep8Path = settings.formatting.autopep8Path; + const autoPep8Args = Array.isArray(settings.formatting.autopep8Args) ? settings.formatting.autopep8Args : []; + const hasCustomArgs = autoPep8Args.length > 0; + const formatSelection = range ? !range.isEmpty : false; + + autoPep8Args.push('--diff'); + if (formatSelection) { + // tslint:disable-next-line:no-non-null-assertion + autoPep8Args.push(...['--line-range', (range!.start.line + 1).toString(), (range!.end.line + 1).toString()]); } - return super.provideDocumentFormattingEdits(document, options, token, autopep8Path, autoPep8Args); + const promise = super.provideDocumentFormattingEdits(document, options, token, autopep8Path, autoPep8Args); + sendTelemetryWhenDone(FORMAT, promise, stopWatch, { tool: 'autoppep8', hasCustomArgs, formatSelection }); + return promise; } -} \ No newline at end of file +} diff --git a/src/client/formatters/baseFormatter.ts b/src/client/formatters/baseFormatter.ts index bf634fd6e5c2..ec957fcb55f7 100644 --- a/src/client/formatters/baseFormatter.ts +++ b/src/client/formatters/baseFormatter.ts @@ -1,23 +1,44 @@ 'use strict'; -import * as vscode from 'vscode'; import * as fs from 'fs'; -import { execPythonFile } from './../common/utils'; -import * as settings from './../common/configSettings'; -import { getTextEditsFromPatch, getTempFileWithDocumentContents } from './../common/editor'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { Uri } from 'vscode'; import { isNotInstalledError } from '../common/helpers'; import { Installer, Product } from '../common/installer'; +import * as settings from './../common/configSettings'; +import { getTempFileWithDocumentContents, getTextEditsFromPatch } from './../common/editor'; +import { execPythonFile } from './../common/utils'; + export abstract class BaseFormatter { private installer: Installer; - constructor(public Id: string, private product: Product, protected outputChannel: vscode.OutputChannel, protected pythonSettings: settings.IPythonSettings, protected workspaceRootPath?: string) { + constructor(public Id: string, private product: Product, protected outputChannel: vscode.OutputChannel) { this.installer = new Installer(); } public abstract formatDocument(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, range?: vscode.Range): Thenable; - - protected provideDocumentFormattingEdits(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, command: string, args: string[]): Thenable { + protected getDocumentPath(document: vscode.TextDocument, fallbackPath: string) { + if (path.basename(document.uri.fsPath) === document.uri.fsPath) { + return fallbackPath; + } + return path.dirname(document.fileName); + } + protected getWorkspaceUri(document: vscode.TextDocument) { + const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri); + if (workspaceFolder) { + return workspaceFolder.uri; + } + if (Array.isArray(vscode.workspace.workspaceFolders) && vscode.workspace.workspaceFolders.length > 0) { + return vscode.workspace.workspaceFolders[0].uri; + } + return vscode.Uri.file(__dirname); + } + protected provideDocumentFormattingEdits(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, command: string, args: string[], cwd: string = null): Thenable { this.outputChannel.clear(); + if (typeof cwd !== 'string' || cwd.length === 0) { + cwd = this.getWorkspaceUri(document).fsPath; + } // autopep8 and yapf have the ability to read from the process input stream and return the formatted code out of the output stream // However they don't support returning the diff of the formatted text when reading data from the input stream @@ -29,8 +50,7 @@ export abstract class BaseFormatter { if (token && token.isCancellationRequested) { return [filePath, '']; } - const workspaceRoot = this.workspaceRootPath ? this.workspaceRootPath : vscode.workspace.rootPath; - return Promise.all([Promise.resolve(filePath), execPythonFile(command, args.concat([filePath]), workspaceRoot)]); + return Promise.all([Promise.resolve(filePath), execPythonFile(document.uri, command, args.concat([filePath]), cwd)]); }).then(data => { // Delete the temporary file created if (tmpFileCreated) { @@ -41,14 +61,14 @@ export abstract class BaseFormatter { } return getTextEditsFromPatch(document.getText(), data[1]); }).catch(error => { - this.handleError(this.Id, command, error); + this.handleError(this.Id, command, error, document.uri); return []; }); vscode.window.setStatusBarMessage(`Formatting with ${this.Id}`, promise); return promise; } - protected handleError(expectedFileName: string, fileName: string, error: Error) { + protected handleError(expectedFileName: string, fileName: string, error: Error, resource?: Uri) { let customError = `Formatting with ${this.Id} failed.`; if (isNotInstalledError(error)) { @@ -59,12 +79,10 @@ export abstract class BaseFormatter { // Ok if we have a space after the file name, this means we have some arguments defined and this isn't supported if (stuffAfterFileName.trim().indexOf(' ') > 0) { customError = `Formatting failed, custom arguments in the 'python.formatting.${this.Id}Path' is not supported.\n` + - `Custom arguments to the formatter can be defined in 'python.formatter.${this.Id}Args' setting of settings.json.\n` + - 'For further details, please see https://github.com/DonJayamanne/pythonVSCode/wiki/Troubleshooting-Linting#2-linting-with-xxx-failed-'; - } - else { + `Custom arguments to the formatter can be defined in 'python.formatter.${this.Id}Args' setting of settings.json.`; + } else { customError += `\nYou could either install the '${this.Id}' formatter, turn it off or use another formatter.`; - this.installer.promptToInstall(this.product); + this.installer.promptToInstall(this.product, resource); } } diff --git a/src/client/formatters/dummyFormatter.ts b/src/client/formatters/dummyFormatter.ts new file mode 100644 index 000000000000..481b57c69ae6 --- /dev/null +++ b/src/client/formatters/dummyFormatter.ts @@ -0,0 +1,15 @@ +'use strict'; + +import * as vscode from 'vscode'; +import { BaseFormatter } from './baseFormatter'; +import { Product } from '../common/installer'; + +export class DummyFormatter extends BaseFormatter { + constructor(outputChannel: vscode.OutputChannel) { + super('none', Product.yapf, outputChannel); + } + + public formatDocument(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, range?: vscode.Range): Thenable { + return Promise.resolve([]); + } +} diff --git a/src/client/formatters/yapfFormatter.ts b/src/client/formatters/yapfFormatter.ts index af106a40f344..4f6d2474b717 100644 --- a/src/client/formatters/yapfFormatter.ts +++ b/src/client/formatters/yapfFormatter.ts @@ -1,22 +1,36 @@ 'use strict'; import * as vscode from 'vscode'; -import {BaseFormatter} from './baseFormatter'; -import * as settings from './../common/configSettings'; +import { PythonSettings } from '../common/configSettings'; import { Product } from '../common/installer'; +import { sendTelemetryWhenDone} from '../telemetry'; +import { FORMAT } from '../telemetry/constants'; +import { StopWatch } from '../telemetry/stopWatch'; +import { BaseFormatter } from './baseFormatter'; export class YapfFormatter extends BaseFormatter { - constructor(outputChannel: vscode.OutputChannel, pythonSettings: settings.IPythonSettings, workspaceRootPath?: string) { - super('yapf', Product.yapf, outputChannel, pythonSettings, workspaceRootPath); + constructor(outputChannel: vscode.OutputChannel) { + super('yapf', Product.yapf, outputChannel); } public formatDocument(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, range?: vscode.Range): Thenable { - let yapfPath = this.pythonSettings.formatting.yapfPath; - let yapfArgs = Array.isArray(this.pythonSettings.formatting.yapfArgs) ? this.pythonSettings.formatting.yapfArgs : []; - yapfArgs = yapfArgs.concat(['--diff']); - if (range && !range.isEmpty) { - yapfArgs = yapfArgs.concat(['--lines', `${range.start.line + 1}-${range.end.line + 1}`]); + const stopWatch = new StopWatch(); + const settings = PythonSettings.getInstance(document.uri); + const yapfPath = settings.formatting.yapfPath; + const yapfArgs = Array.isArray(settings.formatting.yapfArgs) ? settings.formatting.yapfArgs : []; + const hasCustomArgs = yapfArgs.length > 0; + const formatSelection = range ? !range.isEmpty : false; + + yapfArgs.push('--diff'); + if (formatSelection) { + // tslint:disable-next-line:no-non-null-assertion + yapfArgs.push(...['--lines', `${range!.start.line + 1}-${range!.end.line + 1}`]); } - return super.provideDocumentFormattingEdits(document, options, token, yapfPath, yapfArgs); + // Yapf starts looking for config file starting from the file path. + const fallbarFolder = this.getWorkspaceUri(document).fsPath; + const cwd = this.getDocumentPath(document, fallbarFolder); + const promise = super.provideDocumentFormattingEdits(document, options, token, yapfPath, yapfArgs, cwd); + sendTelemetryWhenDone(FORMAT, promise, stopWatch, { tool: 'yapf', hasCustomArgs, formatSelection }); + return promise; } -} \ No newline at end of file +} diff --git a/src/client/helpProvider.ts b/src/client/helpProvider.ts deleted file mode 100644 index d95de685c537..000000000000 --- a/src/client/helpProvider.ts +++ /dev/null @@ -1,113 +0,0 @@ -'use strict'; - -import * as vscode from 'vscode'; -import {Disposable} from 'vscode'; -import * as path from 'path'; -import * as http from 'http'; -import {createDeferred} from './common/helpers'; -import {Documentation} from './common/constants'; -const nodeStatic = require('node-static'); - -let serverAddress = "http://localhost:8080"; -let helpPageToDisplay = Documentation.Home; -export class TextDocumentContentProvider extends Disposable implements vscode.TextDocumentContentProvider { - private _onDidChange = new vscode.EventEmitter(); - private lastUri: vscode.Uri; - constructor() { - super(() => { }); - } - public provideTextDocumentContent(uri: vscode.Uri, token: vscode.CancellationToken): Thenable { - this.lastUri = uri; - return this.generateResultsView(); - } - - get onDidChange(): vscode.Event { - return this._onDidChange.event; - } - - public update() { - this._onDidChange.fire(this.lastUri); - } - - private generateResultsView(): Promise { - const addresss = serverAddress + helpPageToDisplay; - const htmlContent = ` - - - - - - - - `; - return Promise.resolve(htmlContent); - } -} -const helpSchema = 'help-viewer'; -const previewUri = vscode.Uri.parse(helpSchema + '://authority/jupyter'); - -export class HelpProvider { - private disposables: Disposable[] = []; - constructor() { - const textProvider = new TextDocumentContentProvider(); - this.disposables.push(vscode.workspace.registerTextDocumentContentProvider(helpSchema, textProvider)); - this.disposables.push(vscode.commands.registerCommand('python.displayHelp', (page: string) => { - this.startServer().then(port => { - let viewColumn = vscode.ViewColumn.Two; - if (!page || typeof page !== 'string' || page.length === 0) { - helpPageToDisplay = Documentation.Home; - viewColumn = vscode.ViewColumn.One; - } - else { - helpPageToDisplay = page; - } - vscode.commands.executeCommand('vscode.previewHtml', previewUri, viewColumn, 'Help'); - }); - })); - } - dispose() { - this.disposables.forEach(d => d.dispose()); - this.stop(); - } - private httpServer: http.Server; - private port: number; - private startServer(): Promise { - if (this.port) { - return Promise.resolve(this.port); - } - - let def = createDeferred(); - var file = new nodeStatic.Server(path.join(__dirname, '..', '..', 'docs')); - this.httpServer = http.createServer((request, response) => { - request.addListener('end', function () { - // - // Serve files! - // - file.serve(request, response); - }).resume(); - }); - - this.httpServer.listen(0, () => { - this.port = this.httpServer.address().port; - serverAddress = 'http://localhost:' + this.port.toString(); - def.resolve(this.port); - def = null; - }); - this.httpServer.on('error', error => { - if (def) { - def.reject(error); - def = null; - } - }); - - return def.promise; - } - - private stop() { - if (!this.httpServer) { - return; - } - this.httpServer.close(); - this.httpServer = null; - } -} \ No newline at end of file diff --git a/src/client/interpreter/configuration/pythonPathUpdaterService.ts b/src/client/interpreter/configuration/pythonPathUpdaterService.ts new file mode 100644 index 000000000000..b0e52a6a94a4 --- /dev/null +++ b/src/client/interpreter/configuration/pythonPathUpdaterService.ts @@ -0,0 +1,65 @@ +import * as path from 'path'; +import { ConfigurationTarget, Uri, window } from 'vscode'; +import { sendTelemetryEvent } from '../../telemetry'; +import { PYTHON_INTERPRETER } from '../../telemetry/constants'; +import { StopWatch } from '../../telemetry/stopWatch'; +import { IInterpreterVersionService } from '../interpreterVersion'; +import { IPythonPathUpdaterServiceFactory } from './types'; + +export class PythonPathUpdaterService { + constructor(private pythonPathSettingsUpdaterFactory: IPythonPathUpdaterServiceFactory, + private interpreterVersionService: IInterpreterVersionService) { } + public async updatePythonPath(pythonPath: string, configTarget: ConfigurationTarget, trigger: 'ui' | 'shebang' | 'load', wkspace?: Uri): Promise { + const stopWatch = new StopWatch(); + const pythonPathUpdater = this.getPythonUpdaterService(configTarget, wkspace); + let failed = false; + try { + await pythonPathUpdater.updatePythonPath(path.normalize(pythonPath)); + } catch (reason) { + failed = true; + // tslint:disable-next-line:no-unsafe-any prefer-type-cast + const message = reason && typeof reason.message === 'string' ? reason.message as string : ''; + window.showErrorMessage(`Failed to set 'pythonPath'. Error: ${message}`); + console.error(reason); + } + // do not wait for this to complete + this.sendTelemetry(stopWatch.elapsedTime, failed, trigger, pythonPath); + } + private async sendTelemetry(duration: number, failed: boolean, trigger: 'ui' | 'shebang' | 'load', pythonPath: string) { + let version: string | undefined; + let pipVersion: string | undefined; + if (!failed) { + const pyVersionPromise = this.interpreterVersionService.getVersion(pythonPath, '') + .then(pyVersion => pyVersion.length === 0 ? undefined : pyVersion); + const pipVersionPromise = this.interpreterVersionService.getPipVersion(pythonPath) + .then(value => value.length === 0 ? undefined : value) + .catch(() => undefined); + const versions = await Promise.all([pyVersionPromise, pipVersionPromise]); + version = versions[0]; + // tslint:disable-next-line:prefer-type-cast + pipVersion = versions[1] as string; + } + sendTelemetryEvent(PYTHON_INTERPRETER, duration, { failed, trigger, version, pipVersion }); + } + private getPythonUpdaterService(configTarget: ConfigurationTarget, wkspace?: Uri) { + switch (configTarget) { + case ConfigurationTarget.Global: { + return this.pythonPathSettingsUpdaterFactory.getGlobalPythonPathConfigurationService(); + } + case ConfigurationTarget.Workspace: { + if (!wkspace) { + throw new Error('Workspace Uri not defined'); + } + // tslint:disable-next-line:no-non-null-assertion + return this.pythonPathSettingsUpdaterFactory.getWorkspacePythonPathConfigurationService(wkspace!); + } + default: { + if (!wkspace) { + throw new Error('Workspace Uri not defined'); + } + // tslint:disable-next-line:no-non-null-assertion + return this.pythonPathSettingsUpdaterFactory.getWorkspaceFolderPythonPathConfigurationService(wkspace!); + } + } + } +} diff --git a/src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts b/src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts new file mode 100644 index 000000000000..c48d20cdac1c --- /dev/null +++ b/src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts @@ -0,0 +1,17 @@ +import { Uri } from 'vscode'; +import { GlobalPythonPathUpdaterService } from './services/globalUpdaterService'; +import { WorkspaceFolderPythonPathUpdaterService } from './services/workspaceFolderUpdaterService'; +import { WorkspacePythonPathUpdaterService } from './services/workspaceUpdaterService'; +import { IPythonPathUpdaterService, IPythonPathUpdaterServiceFactory } from './types'; + +export class PythonPathUpdaterServiceFactory implements IPythonPathUpdaterServiceFactory { + public getGlobalPythonPathConfigurationService(): IPythonPathUpdaterService { + return new GlobalPythonPathUpdaterService(); + } + public getWorkspacePythonPathConfigurationService(wkspace: Uri): IPythonPathUpdaterService { + return new WorkspacePythonPathUpdaterService(wkspace); + } + public getWorkspaceFolderPythonPathConfigurationService(workspaceFolder: Uri): IPythonPathUpdaterService { + return new WorkspaceFolderPythonPathUpdaterService(workspaceFolder); + } +} diff --git a/src/client/interpreter/configuration/services/globalUpdaterService.ts b/src/client/interpreter/configuration/services/globalUpdaterService.ts new file mode 100644 index 000000000000..d25a866bfbcd --- /dev/null +++ b/src/client/interpreter/configuration/services/globalUpdaterService.ts @@ -0,0 +1,14 @@ +import { workspace } from 'vscode'; +import { IPythonPathUpdaterService } from '../types'; + +export class GlobalPythonPathUpdaterService implements IPythonPathUpdaterService { + public async updatePythonPath(pythonPath: string): Promise { + const pythonConfig = workspace.getConfiguration('python'); + const pythonPathValue = pythonConfig.inspect('pythonPath'); + + if (pythonPathValue && pythonPathValue.globalValue === pythonPath) { + return; + } + await pythonConfig.update('pythonPath', pythonPath, true); + } +} diff --git a/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts b/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts new file mode 100644 index 000000000000..37f45d5cde14 --- /dev/null +++ b/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts @@ -0,0 +1,21 @@ +import * as path from 'path'; +import { ConfigurationTarget, Uri, workspace } from 'vscode'; +import { IPythonPathUpdaterService } from '../types'; + +export class WorkspaceFolderPythonPathUpdaterService implements IPythonPathUpdaterService { + constructor(private workspaceFolder: Uri) { + } + public async updatePythonPath(pythonPath: string): Promise { + const pythonConfig = workspace.getConfiguration('python', this.workspaceFolder); + const pythonPathValue = pythonConfig.inspect('pythonPath'); + + if (pythonPathValue && pythonPathValue.workspaceFolderValue === pythonPath) { + return; + } + if (pythonPath.startsWith(this.workspaceFolder.fsPath)) { + // tslint:disable-next-line:no-invalid-template-strings + pythonPath = path.join('${workspaceRoot}', path.relative(this.workspaceFolder.fsPath, pythonPath)); + } + await pythonConfig.update('pythonPath', pythonPath, ConfigurationTarget.WorkspaceFolder); + } +} diff --git a/src/client/interpreter/configuration/services/workspaceUpdaterService.ts b/src/client/interpreter/configuration/services/workspaceUpdaterService.ts new file mode 100644 index 000000000000..a5a35c3483e5 --- /dev/null +++ b/src/client/interpreter/configuration/services/workspaceUpdaterService.ts @@ -0,0 +1,21 @@ +import * as path from 'path'; +import { Uri, workspace } from 'vscode'; +import { IPythonPathUpdaterService } from '../types'; + +export class WorkspacePythonPathUpdaterService implements IPythonPathUpdaterService { + constructor(private wkspace: Uri) { + } + public async updatePythonPath(pythonPath: string): Promise { + const pythonConfig = workspace.getConfiguration('python', this.wkspace); + const pythonPathValue = pythonConfig.inspect('pythonPath'); + + if (pythonPathValue && pythonPathValue.workspaceValue === pythonPath) { + return; + } + if (pythonPath.startsWith(this.wkspace.fsPath)) { + // tslint:disable-next-line:no-invalid-template-strings + pythonPath = path.join('${workspaceRoot}', path.relative(this.wkspace.fsPath, pythonPath)); + } + await pythonConfig.update('pythonPath', pythonPath, false); + } +} diff --git a/src/client/interpreter/configuration/setInterpreterProvider.ts b/src/client/interpreter/configuration/setInterpreterProvider.ts new file mode 100644 index 000000000000..418c12550c5c --- /dev/null +++ b/src/client/interpreter/configuration/setInterpreterProvider.ts @@ -0,0 +1,114 @@ +'use strict'; +import * as path from 'path'; +import { commands, ConfigurationTarget, Disposable, QuickPickItem, QuickPickOptions, Uri, window, workspace } from 'vscode'; +import { InterpreterManager } from '../'; +import * as settings from '../../common/configSettings'; +import { PythonInterpreter, WorkspacePythonPath } from '../contracts'; +import { ShebangCodeLensProvider } from '../display/shebangCodeLensProvider'; +import { IInterpreterVersionService } from '../interpreterVersion'; +import { PythonPathUpdaterService } from './pythonPathUpdaterService'; +import { PythonPathUpdaterServiceFactory } from './pythonPathUpdaterServiceFactory'; + +// tslint:disable-next-line:interface-name +interface PythonPathQuickPickItem extends QuickPickItem { + path: string; +} + +export class SetInterpreterProvider implements Disposable { + private disposables: Disposable[] = []; + private pythonPathUpdaterService: PythonPathUpdaterService; + constructor(private interpreterManager: InterpreterManager, interpreterVersionService: IInterpreterVersionService) { + this.disposables.push(commands.registerCommand('python.setInterpreter', this.setInterpreter.bind(this))); + this.disposables.push(commands.registerCommand('python.setShebangInterpreter', this.setShebangInterpreter.bind(this))); + this.pythonPathUpdaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory(), interpreterVersionService); + } + public dispose() { + this.disposables.forEach(disposable => disposable.dispose()); + } + private async getWorkspaceToSetPythonPath(): Promise { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + return undefined; + } + if (workspace.workspaceFolders.length === 1) { + return { folderUri: workspace.workspaceFolders[0].uri, configTarget: ConfigurationTarget.Workspace }; + } + + // Ok we have multiple interpreters, get the user to pick a folder. + // tslint:disable-next-line:no-any prefer-type-cast + const workspaceFolder = await (window as any).showWorkspaceFolderPick({ placeHolder: 'Select a workspace' }); + return workspaceFolder ? { folderUri: workspaceFolder.uri, configTarget: ConfigurationTarget.WorkspaceFolder } : undefined; + } + private async suggestionToQuickPickItem(suggestion: PythonInterpreter, workspaceUri?: Uri): Promise { + let detail = suggestion.path; + if (workspaceUri && suggestion.path.startsWith(workspaceUri.fsPath)) { + detail = `.${path.sep}${path.relative(workspaceUri.fsPath, suggestion.path)}`; + } + return { + // tslint:disable-next-line:no-non-null-assertion + label: suggestion.displayName!, + description: suggestion.companyDisplayName || '', + detail: detail, + path: suggestion.path + }; + } + + private async getSuggestions(resourceUri?: Uri) { + const interpreters = await this.interpreterManager.getInterpreters(resourceUri); + // tslint:disable-next-line:no-non-null-assertion + interpreters.sort((a, b) => a.displayName! > b.displayName! ? 1 : -1); + return Promise.all(interpreters.map(item => this.suggestionToQuickPickItem(item, resourceUri))); + } + + private async setInterpreter() { + const setInterpreterGlobally = !Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0; + let configTarget = ConfigurationTarget.Global; + let wkspace: Uri; + if (!setInterpreterGlobally) { + const targetConfig = await this.getWorkspaceToSetPythonPath(); + if (!targetConfig) { + return; + } + configTarget = targetConfig.configTarget; + wkspace = targetConfig.folderUri; + } + + const suggestions = await this.getSuggestions(wkspace); + let currentPythonPath = settings.PythonSettings.getInstance().pythonPath; + if (wkspace && currentPythonPath.startsWith(wkspace.fsPath)) { + currentPythonPath = `.${path.sep}${path.relative(wkspace.fsPath, currentPythonPath)}`; + } + const quickPickOptions: QuickPickOptions = { + matchOnDetail: true, + matchOnDescription: true, + placeHolder: `current: ${currentPythonPath}` + }; + + const selection = await window.showQuickPick(suggestions, quickPickOptions); + if (selection !== undefined) { + await this.pythonPathUpdaterService.updatePythonPath(selection.path, configTarget, 'ui', wkspace); + } + } + + private async setShebangInterpreter(): Promise { + const shebang = await ShebangCodeLensProvider.detectShebang(window.activeTextEditor.document); + if (!shebang) { + return; + } + + const isGlobalChange = !Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0; + const workspaceFolder = workspace.getWorkspaceFolder(window.activeTextEditor.document.uri); + const isWorkspaceChange = Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length === 1; + + if (isGlobalChange) { + await this.pythonPathUpdaterService.updatePythonPath(shebang, ConfigurationTarget.Global, 'shebang'); + return; + } + + if (isWorkspaceChange || !workspaceFolder) { + await this.pythonPathUpdaterService.updatePythonPath(shebang, ConfigurationTarget.Workspace, 'shebang', workspace.workspaceFolders[0].uri); + return; + } + + await this.pythonPathUpdaterService.updatePythonPath(shebang, ConfigurationTarget.WorkspaceFolder, 'shebang', workspaceFolder.uri); + } +} diff --git a/src/client/interpreter/configuration/types.ts b/src/client/interpreter/configuration/types.ts new file mode 100644 index 000000000000..05825416f3bc --- /dev/null +++ b/src/client/interpreter/configuration/types.ts @@ -0,0 +1,12 @@ +import { Uri } from 'vscode'; +import { IPythonPathUpdaterService } from './types'; + +export interface IPythonPathUpdaterService { + updatePythonPath(pythonPath: string): Promise; +} + +export interface IPythonPathUpdaterServiceFactory { + getGlobalPythonPathConfigurationService(): IPythonPathUpdaterService; + getWorkspacePythonPathConfigurationService(wkspace: Uri): IPythonPathUpdaterService; + getWorkspaceFolderPythonPathConfigurationService(workspaceFolder: Uri): IPythonPathUpdaterService; +} diff --git a/src/client/interpreter/contracts.ts b/src/client/interpreter/contracts.ts new file mode 100644 index 000000000000..24c11f906597 --- /dev/null +++ b/src/client/interpreter/contracts.ts @@ -0,0 +1,20 @@ +import { ConfigurationTarget, Disposable, Uri } from 'vscode'; +import { Architecture } from '../common/registry'; + +export interface IInterpreterLocatorService extends Disposable { + getInterpreters(resource?: Uri): Promise; +} + +export type PythonInterpreter = { + path: string; + companyDisplayName?: string; + displayName?: string; + version?: string; + architecture?: Architecture; +}; + +export type WorkspacePythonPath = { + folderUri: Uri; + pytonPath?: string; + configTarget: ConfigurationTarget.Workspace | ConfigurationTarget.WorkspaceFolder; +}; diff --git a/src/client/interpreter/display/index.ts b/src/client/interpreter/display/index.ts new file mode 100644 index 000000000000..b104f4953216 --- /dev/null +++ b/src/client/interpreter/display/index.ts @@ -0,0 +1,82 @@ +'use strict'; +import * as child_process from 'child_process'; +import { EOL } from 'os'; +import * as path from 'path'; +import { Disposable, StatusBarItem } from 'vscode'; +import { PythonSettings } from '../../common/configSettings'; +import * as utils from '../../common/utils'; +import { IInterpreterLocatorService } from '../contracts'; +import { getActiveWorkspaceUri, getFirstNonEmptyLineFromMultilineString } from '../helpers'; +import { IInterpreterVersionService } from '../interpreterVersion'; +import { VirtualEnvironmentManager } from '../virtualEnvs/index'; + +// tslint:disable-next-line:completed-docs +export class InterpreterDisplay implements Disposable { + constructor(private statusBar: StatusBarItem, + private interpreterLocator: IInterpreterLocatorService, + private virtualEnvMgr: VirtualEnvironmentManager, + private versionProvider: IInterpreterVersionService) { + + this.statusBar.command = 'python.setInterpreter'; + } + public dispose() { + // + } + public async refresh() { + const wkspc = getActiveWorkspaceUri(); + if (!wkspc) { + return; + } + const pythonPath = await this.getFullyQualifiedPathToInterpreter(PythonSettings.getInstance(wkspc.folderUri).pythonPath); + await this.updateDisplay(pythonPath); + } + private async getInterpreters() { + return this.interpreterLocator.getInterpreters(); + } + private async updateDisplay(pythonPath: string) { + const interpreters = await this.getInterpreters(); + const interpreter = interpreters.find(i => utils.arePathsSame(i.path, pythonPath)); + + this.statusBar.color = ''; + this.statusBar.tooltip = pythonPath; + if (interpreter) { + // tslint:disable-next-line:no-non-null-assertion + this.statusBar.text = interpreter.displayName!; + if (interpreter.companyDisplayName) { + const toolTipSuffix = `${EOL}${interpreter.companyDisplayName}`; + this.statusBar.tooltip += toolTipSuffix; + } + } else { + const defaultDisplayName = `${path.basename(pythonPath)} [Environment]`; + await Promise.all([ + utils.fsExistsAsync(pythonPath), + this.versionProvider.getVersion(pythonPath, defaultDisplayName), + this.getVirtualEnvironmentName(pythonPath) + ]) + .then(([interpreterExists, displayName, virtualEnvName]) => { + const dislayNameSuffix = virtualEnvName.length > 0 ? ` (${virtualEnvName})` : ''; + this.statusBar.text = `${displayName}${dislayNameSuffix}`; + + if (!interpreterExists && displayName === defaultDisplayName && interpreters.length > 0) { + this.statusBar.color = 'yellow'; + this.statusBar.text = '$(alert) Select Python Environment'; + } + }); + } + this.statusBar.show(); + } + private async getVirtualEnvironmentName(pythonPath: string) { + return this.virtualEnvMgr + .detect(pythonPath) + .then(env => env ? env.name : ''); + } + private async getFullyQualifiedPathToInterpreter(pythonPath: string) { + return new Promise(resolve => { + child_process.execFile(pythonPath, ['-c', 'import sys;print(sys.executable)'], (_, stdout) => { + resolve(getFirstNonEmptyLineFromMultilineString(stdout)); + }); + }) + .then(value => value.length === 0 ? pythonPath : value) + .catch(() => pythonPath); + } +} diff --git a/src/client/interpreter/display/shebangCodeLensProvider.ts b/src/client/interpreter/display/shebangCodeLensProvider.ts new file mode 100644 index 000000000000..d1a43cc70baa --- /dev/null +++ b/src/client/interpreter/display/shebangCodeLensProvider.ts @@ -0,0 +1,72 @@ +'use strict'; +import * as child_process from 'child_process'; +import * as vscode from 'vscode'; +import { CancellationToken, CodeLens, TextDocument } from 'vscode'; +import * as settings from '../../common/configSettings'; +import { IS_WINDOWS } from '../../common/utils'; +import { getFirstNonEmptyLineFromMultilineString } from '../../interpreter/helpers'; + +export class ShebangCodeLensProvider implements vscode.CodeLensProvider { + // tslint:disable-next-line:prefer-type-cast no-any + public onDidChangeCodeLenses: vscode.Event = vscode.workspace.onDidChangeConfiguration as any as vscode.Event; + // tslint:disable-next-line:function-name + public static async detectShebang(document: TextDocument): Promise { + const firstLine = document.lineAt(0); + if (firstLine.isEmptyOrWhitespace) { + return; + } + + if (!firstLine.text.startsWith('#!')) { + return; + } + + const shebang = firstLine.text.substr(2).trim(); + const pythonPath = await ShebangCodeLensProvider.getFullyQualifiedPathToInterpreter(shebang); + return typeof pythonPath === 'string' && pythonPath.length > 0 ? pythonPath : undefined; + } + private static async getFullyQualifiedPathToInterpreter(pythonPath: string) { + if (pythonPath.indexOf('bin/env ') >= 0 && !IS_WINDOWS) { + // In case we have pythonPath as '/usr/bin/env python' + return new Promise(resolve => { + const command = child_process.exec(`${pythonPath} -c 'import sys;print(sys.executable)'`); + let result = ''; + command.stdout.on('data', (data) => { + result += data.toString(); + }); + command.on('close', () => { + resolve(getFirstNonEmptyLineFromMultilineString(result)); + }); + }); + } else { + return new Promise(resolve => { + child_process.execFile(pythonPath, ['-c', 'import sys;print(sys.executable)'], (_, stdout) => { + resolve(getFirstNonEmptyLineFromMultilineString(stdout)); + }); + }); + } + } + + public async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise { + const codeLenses = await this.createShebangCodeLens(document); + return Promise.resolve(codeLenses); + } + + private async createShebangCodeLens(document: TextDocument) { + const shebang = await ShebangCodeLensProvider.detectShebang(document); + if (!shebang || shebang === settings.PythonSettings.getInstance(document.uri).pythonPath) { + return []; + } + + const firstLine = document.lineAt(0); + const startOfShebang = new vscode.Position(0, 0); + const endOfShebang = new vscode.Position(0, firstLine.text.length - 1); + const shebangRange = new vscode.Range(startOfShebang, endOfShebang); + + const cmd: vscode.Command = { + command: 'python.setShebangInterpreter', + title: 'Set as interpreter' + }; + + return [(new CodeLens(shebangRange, cmd))]; + } +} diff --git a/src/client/interpreter/helpers.ts b/src/client/interpreter/helpers.ts new file mode 100644 index 000000000000..f78df63a31f8 --- /dev/null +++ b/src/client/interpreter/helpers.ts @@ -0,0 +1,51 @@ +import * as child_process from 'child_process'; +import { ConfigurationTarget, window, workspace } from 'vscode'; +import { RegistryImplementation } from '../common/registry'; +import { Is_64Bit, IS_WINDOWS } from '../common/utils'; +import { WorkspacePythonPath } from './contracts'; +import { CondaEnvService } from './locators/services/condaEnvService'; +import { WindowsRegistryService } from './locators/services/windowsRegistryService'; + +export function getFirstNonEmptyLineFromMultilineString(stdout: string) { + if (!stdout) { + return ''; + } + const lines = stdout.split(/\r?\n/g).map(line => line.trim()).filter(line => line.length > 0); + return lines.length > 0 ? lines[0] : ''; +} +export function getActiveWorkspaceUri(): WorkspacePythonPath | undefined { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + return undefined; + } + if (workspace.workspaceFolders.length === 1) { + return { folderUri: workspace.workspaceFolders[0].uri, configTarget: ConfigurationTarget.Workspace }; + } + if (window.activeTextEditor) { + const workspaceFolder = workspace.getWorkspaceFolder(window.activeTextEditor.document.uri); + if (workspaceFolder) { + return { configTarget: ConfigurationTarget.WorkspaceFolder, folderUri: workspaceFolder.uri }; + } + } + return undefined; +} +export async function getCondaVersion() { + let condaService: CondaEnvService; + if (IS_WINDOWS) { + const windowsRegistryProvider = new WindowsRegistryService(new RegistryImplementation(), Is_64Bit); + condaService = new CondaEnvService(windowsRegistryProvider); + } else { + condaService = new CondaEnvService(); + } + return condaService.getCondaFile() + .then(async condaFile => { + return new Promise((resolve, reject) => { + child_process.execFile(condaFile, ['--version'], (_, stdout) => { + if (stdout && stdout.length > 0) { + resolve(getFirstNonEmptyLineFromMultilineString(stdout)); + } else { + reject(); + } + }); + }); + }); +} diff --git a/src/client/interpreter/index.ts b/src/client/interpreter/index.ts new file mode 100644 index 000000000000..15f65f1a774e --- /dev/null +++ b/src/client/interpreter/index.ts @@ -0,0 +1,90 @@ +'use strict'; +import * as path from 'path'; +import { ConfigurationTarget, Disposable, StatusBarAlignment, Uri, window, workspace } from 'vscode'; +import { PythonSettings } from '../common/configSettings'; +import { PythonPathUpdaterService } from './configuration/pythonPathUpdaterService'; +import { PythonPathUpdaterServiceFactory } from './configuration/pythonPathUpdaterServiceFactory'; +import { InterpreterDisplay } from './display'; +import { getActiveWorkspaceUri } from './helpers'; +import { InterpreterVersionService } from './interpreterVersion'; +import { PythonInterpreterLocatorService } from './locators'; +import { VirtualEnvironmentManager } from './virtualEnvs/index'; +import { VEnv } from './virtualEnvs/venv'; +import { VirtualEnv } from './virtualEnvs/virtualEnv'; + +export class InterpreterManager implements Disposable { + private disposables: Disposable[] = []; + private display: InterpreterDisplay | null | undefined; + private interpreterProvider: PythonInterpreterLocatorService; + private pythonPathUpdaterService: PythonPathUpdaterService; + constructor() { + const virtualEnvMgr = new VirtualEnvironmentManager([new VEnv(), new VirtualEnv()]); + const statusBar = window.createStatusBarItem(StatusBarAlignment.Left); + this.interpreterProvider = new PythonInterpreterLocatorService(virtualEnvMgr); + const versionService = new InterpreterVersionService(); + this.display = new InterpreterDisplay(statusBar, this.interpreterProvider, virtualEnvMgr, versionService); + const interpreterVersionService = new InterpreterVersionService(); + this.pythonPathUpdaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory(), interpreterVersionService); + PythonSettings.getInstance().addListener('change', () => this.onConfigChanged()); + this.disposables.push(window.onDidChangeActiveTextEditor(() => this.refresh())); + this.disposables.push(statusBar); + this.disposables.push(this.display); + } + public async refresh() { + return this.display.refresh(); + } + public getInterpreters(resource?: Uri) { + return this.interpreterProvider.getInterpreters(resource); + } + public async autoSetInterpreter() { + if (!this.shouldAutoSetInterpreter()) { + return; + } + const activeWorkspace = getActiveWorkspaceUri(); + if (!activeWorkspace) { + return; + } + const interpreters = await this.interpreterProvider.getInterpreters(activeWorkspace.folderUri); + const workspacePathUpper = activeWorkspace.folderUri.fsPath.toUpperCase(); + const interpretersInWorkspace = interpreters.filter(interpreter => interpreter.path.toUpperCase().startsWith(workspacePathUpper)); + if (interpretersInWorkspace.length !== 1) { + return; + } + + // Ensure this new environment is at the same level as the current workspace. + // In windows the interpreter is under scripts/python.exe on linux it is under bin/python. + // Meaning the sub directory must be either scripts, bin or other (but only one level deep). + const pythonPath = interpretersInWorkspace[0].path; + const relativePath = path.dirname(pythonPath).substring(activeWorkspace.folderUri.fsPath.length); + if (relativePath.split(path.sep).filter(l => l.length > 0).length === 2) { + await this.pythonPathUpdaterService.updatePythonPath(pythonPath, activeWorkspace.configTarget, 'load', activeWorkspace.folderUri); + } + } + public dispose(): void { + // tslint:disable-next-line:prefer-type-cast + this.disposables.forEach(disposable => disposable.dispose() as void); + this.display = null; + this.interpreterProvider.dispose(); + } + private shouldAutoSetInterpreter() { + const activeWorkspace = getActiveWorkspaceUri(); + if (!activeWorkspace) { + return false; + } + const pythonConfig = workspace.getConfiguration('python', activeWorkspace.folderUri); + const pythonPathInConfig = pythonConfig.inspect('pythonPath'); + if (activeWorkspace.configTarget === ConfigurationTarget.Workspace) { + return pythonPathInConfig.workspaceValue === undefined || pythonPathInConfig.workspaceValue === 'python'; + } + if (activeWorkspace.configTarget === ConfigurationTarget.WorkspaceFolder) { + return pythonPathInConfig.workspaceFolderValue === undefined || pythonPathInConfig.workspaceFolderValue === 'python'; + } + return false; + } + private onConfigChanged() { + if (this.display) { + // tslint:disable-next-line:no-floating-promises + this.display.refresh(); + } + } +} diff --git a/src/client/interpreter/interpreterVersion.ts b/src/client/interpreter/interpreterVersion.ts new file mode 100644 index 000000000000..0f7ef485e388 --- /dev/null +++ b/src/client/interpreter/interpreterVersion.ts @@ -0,0 +1,36 @@ +import * as child_process from 'child_process'; +import { getInterpreterVersion } from '../common/utils'; + +export interface IInterpreterVersionService { + getVersion(pythonPath: string, defaultValue: string): Promise; + getPipVersion(pythonPath: string): Promise; +} + +const PIP_VERSION_REGEX = '\\d\\.\\d(\\.\\d)+'; + +export class InterpreterVersionService implements IInterpreterVersionService { + public async getVersion(pythonPath: string, defaultValue: string): Promise { + return getInterpreterVersion(pythonPath) + .then(version => version.length === 0 ? defaultValue : version) + .catch(() => defaultValue); + } + public async getPipVersion(pythonPath: string): Promise { + return new Promise((resolve, reject) => { + child_process.execFile(pythonPath, ['-m', 'pip', '--version'], (error, stdout, stdErr) => { + if (stdout && stdout.length > 0) { + // Take the first available version number, see below example. + // pip 9.0.1 from /Users/donjayamanne/anaconda3/lib/python3.6/site-packages (python 3.6). + // Take the second part, see below example. + // pip 9.0.1 from /Users/donjayamanne/anaconda3/lib/python3.6/site-packages (python 3.6). + const re = new RegExp(PIP_VERSION_REGEX, 'g'); + const matches = re.exec(stdout); + if (matches && matches.length > 0) { + resolve(matches[0].trim()); + return; + } + } + reject(); + }); + }); + } +} diff --git a/src/client/interpreter/locators/helpers.ts b/src/client/interpreter/locators/helpers.ts new file mode 100644 index 000000000000..1efaeca88dff --- /dev/null +++ b/src/client/interpreter/locators/helpers.ts @@ -0,0 +1,26 @@ +import { PythonInterpreter } from "../contracts"; +import { IS_WINDOWS, fsReaddirAsync } from "../../common/utils"; +import * as path from 'path'; +import { getArchitectureDislayName } from "../../common/registry"; + +const CheckPythonInterpreterRegEx = IS_WINDOWS ? /^python(\d+(.\d+)?)?\.exe$/ : /^python(\d+(.\d+)?)?$/; + +export function lookForInterpretersInDirectory(pathToCheck: string): Promise { + return fsReaddirAsync(pathToCheck) + .then(subDirs => subDirs.filter(fileName => CheckPythonInterpreterRegEx.test(path.basename(fileName)))); +} + +export function fixInterpreterDisplayName(item: PythonInterpreter) { + if (!item.displayName) { + const arch = getArchitectureDislayName(item.architecture); + const version = item.version || ''; + item.displayName = ['Python', version, arch].filter(item => item.length > 0).join(' ').trim(); + } + return item; +} +export function fixInterpreterPath(item: PythonInterpreter) { + // For some reason anaconda seems to use \\ in the registry path. + item.path = IS_WINDOWS ? item.path.replace(/\\\\/g, "\\") : item.path; + item.path = IS_WINDOWS ? item.path.replace(/\//g, "\\") : item.path; + return item; +} diff --git a/src/client/interpreter/locators/index.ts b/src/client/interpreter/locators/index.ts new file mode 100644 index 000000000000..09ab1c21bc18 --- /dev/null +++ b/src/client/interpreter/locators/index.ts @@ -0,0 +1,90 @@ +'use strict'; +import * as _ from 'lodash'; +import { Disposable, Uri, workspace } from 'vscode'; +import { RegistryImplementation } from '../../common/registry'; +import { areBasePathsSame, arePathsSame, Is_64Bit, IS_WINDOWS } from '../../common/utils'; +import { IInterpreterLocatorService, PythonInterpreter } from '../contracts'; +import { IInterpreterVersionService, InterpreterVersionService } from '../interpreterVersion'; +import { VirtualEnvironmentManager } from '../virtualEnvs'; +import { fixInterpreterDisplayName, fixInterpreterPath } from './helpers'; +import { CondaEnvFileService, getEnvironmentsFile as getCondaEnvFile } from './services/condaEnvFileService'; +import { CondaEnvService } from './services/condaEnvService'; +import { CurrentPathService } from './services/currentPathService'; +import { getKnownSearchPathsForInterpreters, KnownPathsService } from './services/KnownPathsService'; +import { getKnownSearchPathsForVirtualEnvs, VirtualEnvService } from './services/virtualEnvService'; +import { WindowsRegistryService } from './services/windowsRegistryService'; + +export class PythonInterpreterLocatorService implements IInterpreterLocatorService { + private interpretersPerResource: Map; + private disposables: Disposable[] = []; + constructor(private virtualEnvMgr: VirtualEnvironmentManager) { + this.interpretersPerResource = new Map(); + this.disposables.push(workspace.onDidChangeConfiguration(this.onConfigChanged, this)); + } + public async getInterpreters(resource?: Uri) { + const resourceKey = this.getResourceKey(resource); + if (!this.interpretersPerResource.has(resourceKey)) { + const interpreters = await this.getInterpretersPerResource(resource); + this.interpretersPerResource.set(resourceKey, interpreters); + } + + // tslint:disable-next-line:no-non-null-assertion + return this.interpretersPerResource.get(resourceKey)!; + } + public dispose() { + this.disposables.forEach(disposable => disposable.dispose()); + } + private onConfigChanged() { + this.interpretersPerResource.clear(); + } + private getResourceKey(resource?: Uri) { + if (!resource) { + return ''; + } + const workspaceFolder = workspace.getWorkspaceFolder(resource); + return workspaceFolder ? workspaceFolder.uri.fsPath : ''; + } + private async getInterpretersPerResource(resource?: Uri) { + const locators = this.getLocators(resource); + const promises = locators.map(provider => provider.getInterpreters(resource)); + const listOfInterpreters = await Promise.all(promises); + + // tslint:disable-next-line:underscore-consistent-invocation + return _.flatten(listOfInterpreters) + .map(fixInterpreterDisplayName) + .map(fixInterpreterPath) + .reduce((accumulator, current) => { + if (accumulator.findIndex(item => arePathsSame(item.path, current.path)) === -1 && + accumulator.findIndex(item => areBasePathsSame(item.path, current.path)) === -1) { + accumulator.push(current); + } + return accumulator; + }, []); + } + private getLocators(resource?: Uri) { + const locators: IInterpreterLocatorService[] = []; + const versionService = new InterpreterVersionService(); + // The order of the services is important. + if (IS_WINDOWS) { + const windowsRegistryProvider = new WindowsRegistryService(new RegistryImplementation(), Is_64Bit); + locators.push(windowsRegistryProvider); + locators.push(new CondaEnvService(windowsRegistryProvider)); + } else { + locators.push(new CondaEnvService()); + } + // Supplements the above list of conda environments. + locators.push(new CondaEnvFileService(getCondaEnvFile(), versionService)); + locators.push(new VirtualEnvService(getKnownSearchPathsForVirtualEnvs(resource), this.virtualEnvMgr, versionService)); + + if (!IS_WINDOWS) { + // This must be last, it is possible we have paths returned here that are already returned + // in one of the above lists. + locators.push(new KnownPathsService(getKnownSearchPathsForInterpreters(), versionService)); + } + // This must be last, it is possible we have paths returned here that are already returned + // in one of the above lists. + locators.push(new CurrentPathService(this.virtualEnvMgr, versionService)); + + return locators; + } +} diff --git a/src/client/interpreter/locators/services/KnownPathsService.ts b/src/client/interpreter/locators/services/KnownPathsService.ts new file mode 100644 index 000000000000..bdb240ededad --- /dev/null +++ b/src/client/interpreter/locators/services/KnownPathsService.ts @@ -0,0 +1,59 @@ +'use strict'; +import * as _ from 'lodash'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { fsExistsAsync, IS_WINDOWS } from '../../../common/utils'; +import { IInterpreterLocatorService } from '../../contracts'; +import { IInterpreterVersionService } from '../../interpreterVersion'; +import { lookForInterpretersInDirectory } from '../helpers'; +// tslint:disable-next-line:no-require-imports no-var-requires +const untildify = require('untildify'); + +export class KnownPathsService implements IInterpreterLocatorService { + public constructor(private knownSearchPaths: string[], + private versionProvider: IInterpreterVersionService) { } + // tslint:disable-next-line:no-shadowed-variable + public getInterpreters(_?: Uri) { + return this.suggestionsFromKnownPaths(); + } + // tslint:disable-next-line:no-empty + public dispose() { } + private suggestionsFromKnownPaths() { + const promises = this.knownSearchPaths.map(dir => this.getInterpretersInDirectory(dir)); + return Promise.all(promises) + // tslint:disable-next-line:underscore-consistent-invocation + .then(listOfInterpreters => _.flatten(listOfInterpreters)) + .then(interpreters => interpreters.filter(item => item.length > 0)) + .then(interpreters => Promise.all(interpreters.map(interpreter => this.getInterpreterDetails(interpreter)))); + } + private getInterpreterDetails(interpreter: string) { + return this.versionProvider.getVersion(interpreter, path.basename(interpreter)) + .then(displayName => { + return { + displayName, + path: interpreter + }; + }); + } + private getInterpretersInDirectory(dir: string) { + return fsExistsAsync(dir) + .then(exists => exists ? lookForInterpretersInDirectory(dir) : Promise.resolve([])); + } +} + +export function getKnownSearchPathsForInterpreters(): string[] { + if (IS_WINDOWS) { + return []; + } else { + const paths = ['/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin', '/usr/local/sbin']; + paths.forEach(p => { + paths.push(untildify(`~${p}`)); + }); + // Add support for paths such as /Users/xxx/anaconda/bin. + if (process.env.HOME) { + paths.push(path.join(process.env.HOME, 'anaconda', 'bin')); + paths.push(path.join(process.env.HOME, 'python', 'bin')); + } + return paths; + } +} diff --git a/src/client/interpreter/locators/services/conda.ts b/src/client/interpreter/locators/services/conda.ts new file mode 100644 index 000000000000..808a3d6ea049 --- /dev/null +++ b/src/client/interpreter/locators/services/conda.ts @@ -0,0 +1,19 @@ +import { IS_WINDOWS } from '../../../common/utils'; + +// where to find the Python binary within a conda env. +export const CONDA_RELATIVE_PY_PATH = IS_WINDOWS ? ['python.exe'] : ['bin', 'python']; +// tslint:disable-next-line:variable-name +export const AnacondaCompanyNames = ['Anaconda, Inc.', 'Continuum Analytics, Inc.']; +// tslint:disable-next-line:variable-name +export const AnacondaCompanyName = 'Anaconda, Inc.'; +// tslint:disable-next-line:variable-name +export const AnacondaDisplayName = 'Anaconda'; +// tslint:disable-next-line:variable-name +export const AnacondaIdentfiers = ['Anaconda', 'Conda', 'Continuum']; + +export type CondaInfo = { + envs?: string[]; + 'sys.version'?: string; + 'python_version'?: string; + default_prefix?: string; +}; diff --git a/src/client/interpreter/locators/services/condaEnvFileService.ts b/src/client/interpreter/locators/services/condaEnvFileService.ts new file mode 100644 index 000000000000..1383d571088b --- /dev/null +++ b/src/client/interpreter/locators/services/condaEnvFileService.ts @@ -0,0 +1,69 @@ +'use strict'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { IS_WINDOWS } from '../../../common/configSettings'; +import { IInterpreterLocatorService, PythonInterpreter } from '../../contracts'; +import { IInterpreterVersionService } from '../../interpreterVersion'; +import { AnacondaCompanyName, AnacondaCompanyNames, AnacondaDisplayName, CONDA_RELATIVE_PY_PATH } from './conda'; + +export class CondaEnvFileService implements IInterpreterLocatorService { + constructor(private condaEnvironmentFile: string, + private versionService: IInterpreterVersionService) { + } + public async getInterpreters(_?: Uri) { + return this.getSuggestionsFromConda(); + } + // tslint:disable-next-line:no-empty + public dispose() { } + private async getSuggestionsFromConda(): Promise { + return fs.pathExists(this.condaEnvironmentFile) + .then(exists => exists ? this.getEnvironmentsFromFile(this.condaEnvironmentFile) : Promise.resolve([])); + } + private async getEnvironmentsFromFile(envFile: string) { + return fs.readFile(envFile) + .then(buffer => buffer.toString().split(/\r?\n/g)) + .then(lines => lines.map(line => line.trim())) + .then(lines => lines.map(line => path.join(line, ...CONDA_RELATIVE_PY_PATH))) + .then(interpreterPaths => interpreterPaths.map(item => fs.pathExists(item).then(exists => exists ? item : ''))) + .then(promises => Promise.all(promises)) + .then(interpreterPaths => interpreterPaths.filter(item => item.length > 0)) + .then(interpreterPaths => interpreterPaths.map(item => this.getInterpreterDetails(item))) + .then(promises => Promise.all(promises)); + } + private async getInterpreterDetails(interpreter: string) { + return this.versionService.getVersion(interpreter, path.basename(interpreter)) + .then(version => { + version = this.stripCompanyName(version); + const envName = this.getEnvironmentRootDirectory(interpreter); + // tslint:disable-next-line:no-unnecessary-local-variable + const info: PythonInterpreter = { + displayName: `${AnacondaDisplayName} ${version} (${envName})`, + path: interpreter, + companyDisplayName: AnacondaCompanyName, + version: version + }; + return info; + }); + } + private stripCompanyName(content: string) { + // Strip company name from version. + const startOfCompanyName = AnacondaCompanyNames.reduce((index, companyName) => { + if (index > 0) { + return index; + } + return content.indexOf(`:: ${companyName}`); + }, -1); + + return startOfCompanyName > 0 ? content.substring(0, startOfCompanyName).trim() : content; + } + private getEnvironmentRootDirectory(interpreter: string) { + const envDir = interpreter.substring(0, interpreter.length - path.join(...CONDA_RELATIVE_PY_PATH).length); + return path.basename(envDir); + } +} + +export function getEnvironmentsFile() { + const homeDir = IS_WINDOWS ? process.env.USERPROFILE : (process.env.HOME || process.env.HOMEPATH); + return homeDir ? path.join(homeDir, '.conda', 'environments.txt') : ''; +} diff --git a/src/client/interpreter/locators/services/condaEnvService.ts b/src/client/interpreter/locators/services/condaEnvService.ts new file mode 100644 index 000000000000..c6afce8abe0a --- /dev/null +++ b/src/client/interpreter/locators/services/condaEnvService.ts @@ -0,0 +1,102 @@ +'use strict'; +import * as child_process from 'child_process'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { VersionUtils } from '../../../common/versionUtils'; +import { IInterpreterLocatorService, PythonInterpreter } from '../../contracts'; +import { AnacondaCompanyName, CONDA_RELATIVE_PY_PATH, CondaInfo } from './conda'; +import { CondaHelper } from './condaHelper'; + +export class CondaEnvService implements IInterpreterLocatorService { + private readonly condaHelper = new CondaHelper(); + constructor(private registryLookupForConda?: IInterpreterLocatorService) { + } + public async getInterpreters(resource?: Uri) { + return this.getSuggestionsFromConda(); + } + // tslint:disable-next-line:no-empty + public dispose() { } + public async getCondaFile() { + if (this.registryLookupForConda) { + return this.registryLookupForConda.getInterpreters() + .then(interpreters => interpreters.filter(this.isCondaEnvironment)) + .then(condaInterpreters => this.getLatestVersion(condaInterpreters)) + .then(condaInterpreter => { + return condaInterpreter ? path.join(path.dirname(condaInterpreter.path), 'conda.exe') : 'conda'; + }) + .then(async condaPath => { + return fs.pathExists(condaPath).then(exists => exists ? condaPath : 'conda'); + }); + } + return Promise.resolve('conda'); + } + public isCondaEnvironment(interpreter: PythonInterpreter) { + return (interpreter.displayName ? interpreter.displayName : '').toUpperCase().indexOf('ANACONDA') >= 0 || + (interpreter.companyDisplayName ? interpreter.companyDisplayName : '').toUpperCase().indexOf('CONTINUUM') >= 0; + } + public getLatestVersion(interpreters: PythonInterpreter[]) { + const sortedInterpreters = interpreters.filter(interpreter => interpreter.version && interpreter.version.length > 0); + // tslint:disable-next-line:no-non-null-assertion + sortedInterpreters.sort((a, b) => VersionUtils.compareVersion(a.version!, b.version!)); + if (sortedInterpreters.length > 0) { + return sortedInterpreters[sortedInterpreters.length - 1]; + } + } + public async parseCondaInfo(info: CondaInfo) { + const displayName = this.condaHelper.getDisplayName(info); + + // The root of the conda environment is itself a Python interpreter + // envs reported as e.g.: /Users/bob/miniconda3/envs/someEnv. + const envs = Array.isArray(info.envs) ? info.envs : []; + if (info.default_prefix && info.default_prefix.length > 0) { + envs.push(info.default_prefix); + } + + const promises = envs + .map(env => { + // If it is an environment, hence suffix with env name. + const interpreterDisplayName = env === info.default_prefix ? displayName : `${displayName} (${path.basename(env)})`; + // tslint:disable-next-line:no-unnecessary-local-variable + const interpreter: PythonInterpreter = { + path: path.join(env, ...CONDA_RELATIVE_PY_PATH), + displayName: interpreterDisplayName, + companyDisplayName: AnacondaCompanyName + }; + return interpreter; + }) + .map(async env => fs.pathExists(env.path).then(exists => exists ? env : null)); + + return Promise.all(promises) + .then(interpreters => interpreters.filter(interpreter => interpreter !== null && interpreter !== undefined)) + // tslint:disable-next-line:no-non-null-assertion + .then(interpreters => interpreters.map(interpreter => interpreter!)); + } + private async getSuggestionsFromConda(): Promise { + return this.getCondaFile() + .then(async condaFile => { + return new Promise((resolve, reject) => { + // interrogate conda (if it's on the path) to find all environments. + child_process.execFile(condaFile, ['info', '--json'], (_, stdout) => { + if (stdout.length === 0) { + resolve([]); + return; + } + + try { + // tslint:disable-next-line:prefer-type-cast + const info = JSON.parse(stdout) as CondaInfo; + resolve(this.parseCondaInfo(info)); + } catch (e) { + // Failed because either: + // 1. conda is not installed. + // 2. `conda info --json` has changed signature. + // 3. output of `conda info --json` has changed in structure. + // In all cases, we can't offer conda pythonPath suggestions. + resolve([]); + } + }); + }); + }); + } +} diff --git a/src/client/interpreter/locators/services/condaHelper.ts b/src/client/interpreter/locators/services/condaHelper.ts new file mode 100644 index 000000000000..ea8276c6392f --- /dev/null +++ b/src/client/interpreter/locators/services/condaHelper.ts @@ -0,0 +1,42 @@ +import { AnacondaDisplayName, AnacondaIdentfiers, CondaInfo } from './conda'; + +export class CondaHelper { + public getDisplayName(condaInfo: CondaInfo = {}): string { + const pythonVersion = this.getPythonVersion(condaInfo); + + // Samples. + // "3.6.1 |Anaconda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]". + // "3.6.2 |Anaconda, Inc.| (default, Sep 21 2017, 18:29:43) \n[GCC 4.2.1 Compatible Clang 4.0.1 (tags/RELEASE_401/final)]". + const sysVersion = condaInfo['sys.version']; + if (!sysVersion) { + return pythonVersion ? `Python ${pythonVersion} : ${AnacondaDisplayName}` : AnacondaDisplayName; + } + + // Take the first two parts of the sys.version. + const sysVersionParts = sysVersion.split('|').filter((_, index) => index < 2); + if (sysVersionParts.length > 0) { + if (pythonVersion && sysVersionParts[0].startsWith(pythonVersion)) { + sysVersionParts[0] = `Python ${sysVersionParts[0]}`; + } else { + // The first part is not the python version, hence remove this. + sysVersionParts.shift(); + } + } + + const displayName = sysVersionParts.map(item => item.trim()).join(' : '); + return this.isIdentifiableAsAnaconda(displayName) ? displayName : `${displayName} : ${AnacondaDisplayName}`; + } + private isIdentifiableAsAnaconda(value: string) { + const valueToSearch = value.toLowerCase(); + return AnacondaIdentfiers.some(item => valueToSearch.indexOf(item.toLowerCase()) !== -1); + } + private getPythonVersion(condaInfo: CondaInfo): string | undefined { + // Sample. + // 3.6.2.final.0 (hence just take everything untill the third period). + const pythonVersion = condaInfo.python_version; + if (!pythonVersion) { + return undefined; + } + return pythonVersion.split('.').filter((_, index) => index < 3).join('.'); + } +} diff --git a/src/client/interpreter/locators/services/currentPathService.ts b/src/client/interpreter/locators/services/currentPathService.ts new file mode 100644 index 000000000000..55b6c4728fe9 --- /dev/null +++ b/src/client/interpreter/locators/services/currentPathService.ts @@ -0,0 +1,54 @@ +'use strict'; +import * as child_process from 'child_process'; +import * as _ from 'lodash'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { PythonSettings } from '../../../common/configSettings'; +import { IInterpreterLocatorService } from '../../contracts'; +import { getFirstNonEmptyLineFromMultilineString } from '../../helpers'; +import { IInterpreterVersionService } from '../../interpreterVersion'; +import { VirtualEnvironmentManager } from '../../virtualEnvs'; + +export class CurrentPathService implements IInterpreterLocatorService { + public constructor(private virtualEnvMgr: VirtualEnvironmentManager, + private versionProvider: IInterpreterVersionService) { } + public async getInterpreters(resource?: Uri) { + return this.suggestionsFromKnownPaths(); + } + // tslint:disable-next-line:no-empty + public dispose() { } + private async suggestionsFromKnownPaths(resource?: Uri) { + const currentPythonInterpreter = this.getInterpreter(PythonSettings.getInstance(resource).pythonPath, '').then(interpreter => [interpreter]); + const python = this.getInterpreter('python', '').then(interpreter => [interpreter]); + const python2 = this.getInterpreter('python2', '').then(interpreter => [interpreter]); + const python3 = this.getInterpreter('python3', '').then(interpreter => [interpreter]); + return Promise.all([currentPythonInterpreter, python, python2, python3]) + // tslint:disable-next-line:underscore-consistent-invocation + .then(listOfInterpreters => _.flatten(listOfInterpreters)) + .then(interpreters => interpreters.filter(item => item.length > 0)) + // tslint:disable-next-line:promise-function-async + .then(interpreters => Promise.all(interpreters.map(interpreter => this.getInterpreterDetails(interpreter)))); + } + private async getInterpreterDetails(interpreter: string) { + return Promise.all([ + this.versionProvider.getVersion(interpreter, path.basename(interpreter)), + this.virtualEnvMgr.detect(interpreter) + ]) + .then(([displayName, virtualEnv]) => { + displayName += virtualEnv ? ` (${virtualEnv.name})` : ''; + return { + displayName, + path: interpreter + }; + }); + } + private async getInterpreter(pythonPath: string, defaultValue: string) { + return new Promise(resolve => { + // tslint:disable-next-line:variable-name + child_process.execFile(pythonPath, ['-c', 'import sys;print(sys.executable)'], (_err, stdout) => { + resolve(getFirstNonEmptyLineFromMultilineString(stdout)); + }); + }) + .then(value => value.length === 0 ? defaultValue : value); + } +} diff --git a/src/client/interpreter/locators/services/virtualEnvService.ts b/src/client/interpreter/locators/services/virtualEnvService.ts new file mode 100644 index 000000000000..568936d5f372 --- /dev/null +++ b/src/client/interpreter/locators/services/virtualEnvService.ts @@ -0,0 +1,88 @@ +'use strict'; +import * as _ from 'lodash'; +import * as path from 'path'; +import { Uri, workspace } from 'vscode'; +import { fsReaddirAsync, IS_WINDOWS } from '../../../common/utils'; +import { IInterpreterLocatorService, PythonInterpreter } from '../../contracts'; +import { IInterpreterVersionService } from '../../interpreterVersion'; +import { VirtualEnvironmentManager } from '../../virtualEnvs'; +import { lookForInterpretersInDirectory } from '../helpers'; +import * as settings from './../../../common/configSettings'; +// tslint:disable-next-line:no-require-imports no-var-requires +const untildify = require('untildify'); + +export class VirtualEnvService implements IInterpreterLocatorService { + public constructor(private knownSearchPaths: string[], + private virtualEnvMgr: VirtualEnvironmentManager, + private versionProvider: IInterpreterVersionService) { } + public async getInterpreters(resource?: Uri) { + return this.suggestionsFromKnownVenvs(); + } + // tslint:disable-next-line:no-empty + public dispose() { } + private async suggestionsFromKnownVenvs() { + return Promise.all(this.knownSearchPaths.map(dir => this.lookForInterpretersInVenvs(dir))) + // tslint:disable-next-line:underscore-consistent-invocation + .then(listOfInterpreters => _.flatten(listOfInterpreters)); + } + private async lookForInterpretersInVenvs(pathToCheck: string) { + return fsReaddirAsync(pathToCheck) + .then(subDirs => Promise.all(this.getProspectiveDirectoriesForLookup(subDirs))) + .then(dirs => dirs.filter(dir => dir.length > 0)) + .then(dirs => Promise.all(dirs.map(lookForInterpretersInDirectory))) + // tslint:disable-next-line:underscore-consistent-invocation + .then(pathsWithInterpreters => _.flatten(pathsWithInterpreters)) + .then(interpreters => Promise.all(interpreters.map(interpreter => this.getVirtualEnvDetails(interpreter)))); + } + private getProspectiveDirectoriesForLookup(subDirs: string[]) { + const dirToLookFor = IS_WINDOWS ? 'SCRIPTS' : 'BIN'; + return subDirs.map(subDir => fsReaddirAsync(subDir).then(dirs => { + const scriptOrBinDirs = dirs.filter(dir => { + const folderName = path.basename(dir); + return folderName.toUpperCase() === dirToLookFor; + }); + return scriptOrBinDirs.length === 1 ? scriptOrBinDirs[0] : ''; + })); + } + private async getVirtualEnvDetails(interpreter: string): Promise { + return Promise.all([ + this.versionProvider.getVersion(interpreter, path.basename(interpreter)), + this.virtualEnvMgr.detect(interpreter) + ]) + .then(([displayName, virtualEnv]) => { + const virtualEnvSuffix = virtualEnv ? virtualEnv.name : this.getVirtualEnvironmentRootDirectory(interpreter); + return { + displayName: `${displayName} (${virtualEnvSuffix})`.trim(), + path: interpreter + }; + }); + } + private getVirtualEnvironmentRootDirectory(interpreter: string) { + return path.basename(path.dirname(path.dirname(interpreter))); + } +} + +export function getKnownSearchPathsForVirtualEnvs(resource?: Uri): string[] { + const paths: string[] = []; + if (!IS_WINDOWS) { + const defaultPaths = ['/Envs', '/.virtualenvs', '/.pyenv', '/.pyenv/versions']; + defaultPaths.forEach(p => { + paths.push(untildify(`~${p}`)); + }); + } + const venvPath = settings.PythonSettings.getInstance(resource).venvPath; + if (venvPath) { + paths.push(untildify(venvPath)); + } + if (Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0) { + if (resource && workspace.workspaceFolders.length > 1) { + const wkspaceFolder = workspace.getWorkspaceFolder(resource); + if (wkspaceFolder) { + paths.push(wkspaceFolder.uri.fsPath); + } + } else { + paths.push(workspace.workspaceFolders[0].uri.fsPath); + } + } + return paths; +} diff --git a/src/client/interpreter/locators/services/windowsRegistryService.ts b/src/client/interpreter/locators/services/windowsRegistryService.ts new file mode 100644 index 000000000000..de01c5bfadce --- /dev/null +++ b/src/client/interpreter/locators/services/windowsRegistryService.ts @@ -0,0 +1,146 @@ +import * as fs from 'fs-extra'; +import * as _ from 'lodash'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { Architecture, Hive, IRegistry } from '../../../common/registry'; +import { IInterpreterLocatorService, PythonInterpreter } from '../../contracts'; + +// tslint:disable-next-line:variable-name +const DefaultPythonExecutable = 'python.exe'; +// tslint:disable-next-line:variable-name +const CompaniesToIgnore = ['PYLAUNCHER']; +// tslint:disable-next-line:variable-name +const PythonCoreCompanyDisplayName = 'Python Software Foundation'; +// tslint:disable-next-line:variable-name +const PythonCoreComany = 'PYTHONCORE'; + +type CompanyInterpreter = { + companyKey: string, + hive: Hive, + arch?: Architecture +}; + +export class WindowsRegistryService implements IInterpreterLocatorService { + constructor(private registry: IRegistry, private is64Bit: boolean) { + + } + // tslint:disable-next-line:variable-name + public getInterpreters(_resource?: Uri) { + return this.getInterpretersFromRegistry(); + } + // tslint:disable-next-line:no-empty + public dispose() { } + private async getInterpretersFromRegistry() { + // https://github.com/python/peps/blob/master/pep-0514.txt#L357 + const hkcuArch = this.is64Bit ? undefined : Architecture.x86; + const promises: Promise[] = [ + this.getCompanies(Hive.HKCU, hkcuArch), + this.getCompanies(Hive.HKLM, Architecture.x86) + ]; + // https://github.com/Microsoft/PTVS/blob/ebfc4ca8bab234d453f15ee426af3b208f3c143c/Python/Product/Cookiecutter/Shared/Interpreters/PythonRegistrySearch.cs#L44 + if (this.is64Bit) { + promises.push(this.getCompanies(Hive.HKLM, Architecture.x64)); + } + + const companies = await Promise.all(promises); + // tslint:disable-next-line:underscore-consistent-invocation + const companyInterpreters = await Promise.all(_.flatten(companies) + .filter(item => item !== undefined && item !== null) + .map(company => { + return this.getInterpretersForCompany(company.companyKey, company.hive, company.arch); + })); + + // tslint:disable-next-line:underscore-consistent-invocation + return _.flatten(companyInterpreters) + .filter(item => item !== undefined && item !== null) + // tslint:disable-next-line:no-non-null-assertion + .map(item => item!) + .reduce((prev, current) => { + if (prev.findIndex(item => item.path.toUpperCase() === current.path.toUpperCase()) === -1) { + prev.push(current); + } + return prev; + }, []); + } + private async getCompanies(hive: Hive, arch?: Architecture): Promise { + return this.registry.getKeys('\\Software\\Python', hive, arch) + .then(companyKeys => companyKeys + .filter(companyKey => CompaniesToIgnore.indexOf(path.basename(companyKey).toUpperCase()) === -1) + .map(companyKey => { + return { companyKey, hive, arch }; + })); + } + private async getInterpretersForCompany(companyKey: string, hive: Hive, arch?: Architecture) { + const tagKeys = await this.registry.getKeys(companyKey, hive, arch); + return Promise.all(tagKeys.map(tagKey => this.getInreterpreterDetailsForCompany(tagKey, companyKey, hive, arch))); + } + private getInreterpreterDetailsForCompany(tagKey: string, companyKey: string, hive: Hive, arch?: Architecture): Promise { + const key = `${tagKey}\\InstallPath`; + type InterpreterInformation = null | undefined | { + installPath: string, + executablePath?: string, + displayName?: string, + version?: string, + companyDisplayName?: string + }; + return this.registry.getValue(key, hive, arch) + .then(installPath => { + // Install path is mandatory. + if (!installPath) { + return Promise.resolve(null); + } + // Check if 'ExecutablePath' exists. + // Remember Python 2.7 doesn't have 'ExecutablePath' (there could be others). + // Treat all other values as optional. + return Promise.all([ + Promise.resolve(installPath), + this.registry.getValue(key, hive, arch, 'ExecutablePath'), + // tslint:disable-next-line:no-non-null-assertion + this.getInterpreterDisplayName(tagKey, companyKey, hive, arch), + this.registry.getValue(tagKey, hive, arch, 'Version'), + this.getCompanyDisplayName(companyKey, hive, arch) + ]) + .then(([installedPath, executablePath, displayName, version, companyDisplayName]) => { + // tslint:disable-next-line:prefer-type-cast + return { installPath: installedPath, executablePath, displayName, version, companyDisplayName } as InterpreterInformation; + }); + }) + .then((interpreterInfo?: InterpreterInformation) => { + if (!interpreterInfo) { + return; + } + + const executablePath = interpreterInfo.executablePath && interpreterInfo.executablePath.length > 0 ? interpreterInfo.executablePath : path.join(interpreterInfo.installPath, DefaultPythonExecutable); + const displayName = interpreterInfo.displayName; + const version = interpreterInfo.version ? path.basename(interpreterInfo.version) : path.basename(tagKey); + // tslint:disable-next-line:prefer-type-cast + return { + architecture: arch, + displayName, + path: executablePath, + version, + companyDisplayName: interpreterInfo.companyDisplayName + } as PythonInterpreter; + }) + .then(interpreter => interpreter ? fs.pathExists(interpreter.path).then(exists => exists ? interpreter : null) : null) + .catch(error => { + console.error(`Failed to retrieve interpreter details for company ${companyKey},tag: ${tagKey}, hive: ${hive}, arch: ${arch}`); + console.error(error); + return null; + }); + } + private async getInterpreterDisplayName(tagKey: string, companyKey: string, hive: Hive, arch?: Architecture) { + const displayName = await this.registry.getValue(tagKey, hive, arch, 'DisplayName'); + if (displayName && displayName.length > 0) { + return displayName; + } + } + private async getCompanyDisplayName(companyKey: string, hive: Hive, arch?: Architecture) { + const displayName = await this.registry.getValue(companyKey, hive, arch, 'DisplayName'); + if (displayName && displayName.length > 0) { + return displayName; + } + const company = path.basename(companyKey); + return company.toUpperCase() === PythonCoreComany ? PythonCoreCompanyDisplayName : company; + } +} diff --git a/src/client/interpreter/virtualEnvs/contracts.ts b/src/client/interpreter/virtualEnvs/contracts.ts new file mode 100644 index 000000000000..07298ffc1f1a --- /dev/null +++ b/src/client/interpreter/virtualEnvs/contracts.ts @@ -0,0 +1,4 @@ +export interface IVirtualEnvironment { + detect(pythonPath: string): Promise; + readonly name: string; +} diff --git a/src/client/interpreter/virtualEnvs/index.ts b/src/client/interpreter/virtualEnvs/index.ts new file mode 100644 index 000000000000..128881a4e043 --- /dev/null +++ b/src/client/interpreter/virtualEnvs/index.ts @@ -0,0 +1,19 @@ +import { IVirtualEnvironment } from './contracts'; + +export class VirtualEnvironmentManager { + constructor(private envs: IVirtualEnvironment[]) { + } + public detect(pythonPath: string): Promise { + const promises = this.envs + .map(item => item.detect(pythonPath) + .then(result => { + return { env: item, result }; + })); + + return Promise.all(promises) + .then(results => { + const env = results.find(items => items.result === true); + return env ? env.env : undefined; + }); + } +} diff --git a/src/client/interpreter/virtualEnvs/venv.ts b/src/client/interpreter/virtualEnvs/venv.ts new file mode 100644 index 000000000000..da65ec561a87 --- /dev/null +++ b/src/client/interpreter/virtualEnvs/venv.ts @@ -0,0 +1,15 @@ +import { IVirtualEnvironment } from "./contracts"; +import * as path from 'path'; +import { fsExistsAsync } from '../../common/utils'; + +const pyEnvCfgFileName = 'pyvenv.cfg'; + +export class VEnv implements IVirtualEnvironment { + public readonly name: string = 'venv'; + + detect(pythonPath: string): Promise { + const dir = path.dirname(pythonPath); + const pyEnvCfgPath = path.join(dir, '..', pyEnvCfgFileName); + return fsExistsAsync(pyEnvCfgPath); + } +} \ No newline at end of file diff --git a/src/client/interpreter/virtualEnvs/virtualEnv.ts b/src/client/interpreter/virtualEnvs/virtualEnv.ts new file mode 100644 index 000000000000..9000d576ec5f --- /dev/null +++ b/src/client/interpreter/virtualEnvs/virtualEnv.ts @@ -0,0 +1,15 @@ +import { IVirtualEnvironment } from "./contracts"; +import * as path from 'path'; +import { fsExistsAsync } from '../../common/utils'; + +const OrigPrefixFile = 'orig-prefix.txt'; + +export class VirtualEnv implements IVirtualEnvironment { + public readonly name: string = 'virtualenv'; + + detect(pythonPath: string): Promise { + const dir = path.dirname(pythonPath); + const origPrefixFile = path.join(dir, '..', 'lib', OrigPrefixFile); + return fsExistsAsync(origPrefixFile); + } +} \ No newline at end of file diff --git a/src/client/jedi/commands.ts b/src/client/jedi/commands.ts new file mode 100644 index 000000000000..347adbc9272f --- /dev/null +++ b/src/client/jedi/commands.ts @@ -0,0 +1,24 @@ +"use strict"; + +export class RequestCommands { + public static Exit: Buffer = new Buffer("exit"); + public static Ping: Buffer = new Buffer("ping"); + public static Arguments = new Buffer("args"); + public static Completions = new Buffer("comp"); + public static Definitions = new Buffer("defs"); + public static Hover = new Buffer("hovr"); + public static Usages = new Buffer("usag"); + public static Names = new Buffer("name"); +} + +export namespace ResponseCommands { + export const Pong = 'pong'; + export const TraceLog = 'tlog'; + export const Error = 'eror'; + export const Signature = "args"; + export const Completions = "comp"; + export const Definitions = "defs"; + export const Hover = "hovr"; + export const References = "usag"; + export const DocumentSymbols = "name"; +} \ No newline at end of file diff --git a/src/client/jedi/main.ts b/src/client/jedi/main.ts new file mode 100644 index 000000000000..6586a1e27eb4 --- /dev/null +++ b/src/client/jedi/main.ts @@ -0,0 +1,137 @@ +"use strict"; + +import * as child_process from 'child_process'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { SocketClient } from './socketClient'; +import { SocketServer } from '../common/comms/socketServer'; +import { createDeferred, Deferred } from '../common/helpers'; +import { PythonSettings } from '../common/configSettings'; +import { EventEmitter } from 'events'; +import { CancellationToken } from 'vscode'; +import { RequestCommands } from "./commands"; + +export enum Command { + Completions, + Definition, + Hover, + References, + Signature, + DocumentSymbols +} + +const commandMapping = new Map(); +commandMapping.set(Command.Completions, RequestCommands.Completions); +commandMapping.set(Command.Definition, RequestCommands.Definitions); +commandMapping.set(Command.Hover, RequestCommands.Hover); +commandMapping.set(Command.References, RequestCommands.Usages); +commandMapping.set(Command.Signature, RequestCommands.Arguments); +commandMapping.set(Command.DocumentSymbols, RequestCommands.Names); + +export class ClientAdapter extends EventEmitter { + constructor(private outputChannel: vscode.OutputChannel, private rootDir: string) { + super(); + } + public getResult(responseParser: (data: Object) => T, command: Command, token: CancellationToken, fileName: string, columnIndex?: number, lineIndex?: number, source?: string): Promise { + const cmd = commandMapping.get(command); + return this.socketClient.getResult(cmd, token, fileName, columnIndex, lineIndex, source) + .then(responseParser); + } + private process: child_process.ChildProcess; + private socketServer: SocketServer; + private socketClient: SocketClient; + + private startDef: Deferred; + + public dispose() { + try { + if (this.process) { + this.process.stdin.write('\n'); + } + } + catch (ex) { + } + try { + this.socketClient.dispose(); + } + catch (ex) { + } + try { + this.socketServer.Stop(); + } + catch (ex) { + } + this.socketClient = null; + this.process = null; + this.socketServer = null; + this.startDef = null; + } + public start(envVariables?: { [key: string]: string }): Promise { + if (this.startDef) { + return this.startDef.promise; + } + + this.startDef = createDeferred(); + const pyFile = path.join(__dirname, '..', '..', '..', '..', 'pythonFiles', 'completionServer.py'); + const newEnv = {}; + Object.assign(newEnv, envVariables); + Object.assign(newEnv, process.env); + + this.startSocketServer().then(port => { + const def = createDeferred(); + const options = { env: newEnv, cwd: this.rootDir }; + const rootDirUri = this.rootDir ? vscode.Uri.file(this.rootDir) : undefined; + this.process = child_process.spawn(PythonSettings.getInstance(rootDirUri).pythonPath, [pyFile, port.toString()], options); + this.process.stdout.setEncoding('utf8'); + this.process.stderr.setEncoding('utf8'); + + let processStarted = false; + let handshakeDone = false; + + this.process.stdout.on('data', (data: string) => { + if (!processStarted && data.split(/\r?\n/g).some(line => line === 'Started')) { + processStarted = true; + if (processStarted && handshakeDone) { + def.resolve(); + } + return; + } + this.outputChannel.append(data); + }); + this.process.stderr.on('data', (data: string) => { + this.outputChannel.append(data); + }); + + this.socketClient.on('handshake', () => { + handshakeDone = true; + if (processStarted && handshakeDone) { + def.resolve(); + } + }); + + return def.promise; + }).then(() => { + this.startDef.resolve(); + }).catch(reason => { + this.startDef.reject(reason); + }); + + return this.startDef.promise; + } + private startSocketServer(): Promise { + this.socketServer = new SocketServer(); + this.socketClient = new SocketClient(this.socketServer, this.outputChannel); + this.socketClient.on('status', status => { + this.emit('status', status); + }); + this.socketClient.on('error', error => { + this.emit('error', error); + console.error(error); + this.outputChannel.appendLine('Error received: ' + error); + }); + this.socketClient.on('commanderror', (commandError: { command: string, id: string, trace: string }) => { + this.outputChannel.appendLine(`Unhandled command Error from Autocompletion Library. '${JSON.stringify(commandError)}'`); + }); + return this.socketServer.Start(); + } +} diff --git a/src/client/jedi/parsers/CompletionParser.ts b/src/client/jedi/parsers/CompletionParser.ts new file mode 100644 index 000000000000..eedc47842730 --- /dev/null +++ b/src/client/jedi/parsers/CompletionParser.ts @@ -0,0 +1,27 @@ +import * as proxy from '../../providers/jediProxy'; +import { extractSignatureAndDocumentation } from '../../providers/jediHelpers'; +import { PythonSettings } from '../../common/configSettings'; +import { CompletionItem, SymbolKind, SnippetString, Uri } from 'vscode'; + +export class CompletionParser { + public static parse(data: proxy.ICompletionResult, resource: Uri): CompletionItem[] { + if (!data || data.items.length === 0) { + return []; + } + return data.items.map(item => { + const sigAndDocs = extractSignatureAndDocumentation(item); + let completionItem = new CompletionItem(item.text); + completionItem.kind = item.type; + completionItem.documentation = sigAndDocs[1].length === 0 ? item.description : sigAndDocs[1]; + completionItem.detail = sigAndDocs[0].split(/\r?\n/).join(''); + if (PythonSettings.getInstance(resource).autoComplete.addBrackets === true && + (item.kind === SymbolKind.Function || item.kind === SymbolKind.Method)) { + completionItem.insertText = new SnippetString(item.text).appendText("(").appendTabstop().appendText(")"); + } + + // ensure the built in memebers are at the bottom + completionItem.sortText = (completionItem.label.startsWith('__') ? 'z' : (completionItem.label.startsWith('_') ? 'y' : '__')) + completionItem.label; + return completionItem; + }); + } +} \ No newline at end of file diff --git a/src/client/jedi/parsers/DefinitionParser.ts b/src/client/jedi/parsers/DefinitionParser.ts new file mode 100644 index 000000000000..6b70545e48df --- /dev/null +++ b/src/client/jedi/parsers/DefinitionParser.ts @@ -0,0 +1,16 @@ +import { Definition, Location, Range, Uri } from 'vscode'; +import * as proxy from '../../providers/jediProxy'; +export class DefinitionParser { + public static parse(data: proxy.IDefinitionResult, possibleWord: string): Definition { + if (!data || !Array.isArray(data.definitions) || data.definitions.length === 0) { + return null; + } + const definitions = data.definitions.filter(d => d.text === possibleWord); + const definition = definitions.length > 0 ? definitions[0] : data.definitions[data.definitions.length - 1]; + const definitionResource = Uri.file(definition.fileName); + const range = new Range( + definition.range.startLine, definition.range.startColumn, + definition.range.endLine, definition.range.endColumn); + return new Location(definitionResource, range); + } +} \ No newline at end of file diff --git a/src/client/jedi/parsers/HoverParser.ts b/src/client/jedi/parsers/HoverParser.ts new file mode 100644 index 000000000000..86bbf680fdef --- /dev/null +++ b/src/client/jedi/parsers/HoverParser.ts @@ -0,0 +1,71 @@ +import { Hover, SymbolKind } from 'vscode'; +import * as proxy from '../../providers/jediProxy'; +import { highlightCode } from '../../providers/jediHelpers'; +import { EOL } from 'os'; +export class HoverParser { + public static parse(data: proxy.IHoverResult, currentWord: string): Hover { + if (!data || !Array.isArray(data.items) || data.items.length === 0) { + return new Hover([]); + } + + let results = []; + let capturedInfo: string[] = []; + data.items.forEach(item => { + let { signature } = item; + switch (item.kind) { + case SymbolKind.Constructor: + case SymbolKind.Function: + case SymbolKind.Method: { + signature = 'def ' + signature; + break; + } + case SymbolKind.Class: { + signature = 'class ' + signature; + break; + } + default: { + signature = typeof item.text === 'string' && item.text.length > 0 ? item.text : currentWord; + } + } + if (item.docstring) { + let lines = item.docstring.split(/\r?\n/); + // If the docstring starts with the signature, then remove those lines from the docstring + if (lines.length > 0 && item.signature.indexOf(lines[0]) === 0) { + lines.shift(); + let endIndex = lines.findIndex(line => item.signature.endsWith(line)); + if (endIndex >= 0) { + lines = lines.filter((line, index) => index > endIndex); + } + } + if (lines.length > 0 && item.signature.startsWith(currentWord) && lines[0].startsWith(currentWord) && lines[0].endsWith(')')) { + lines.shift(); + } + let descriptionWithHighlightedCode = highlightCode(lines.join(EOL)); + let hoverInfo = ['```python', signature, '```', descriptionWithHighlightedCode].join(EOL); + let key = signature + lines.join(''); + // Sometimes we have duplicate documentation, one with a period at the end + if (capturedInfo.indexOf(key) >= 0 || capturedInfo.indexOf(key + '.') >= 0) { + return; + } + capturedInfo.push(key); + capturedInfo.push(key + '.'); + results.push(hoverInfo); + return; + } + if (item.description) { + let descriptionWithHighlightedCode = highlightCode(item.description); + let hoverInfo = '```python' + EOL + signature + EOL + '```' + EOL + descriptionWithHighlightedCode; + let lines = item.description.split(EOL); + let key = signature + lines.join(''); + // Sometimes we have duplicate documentation, one with a period at the end + if (capturedInfo.indexOf(key) >= 0 || capturedInfo.indexOf(key + '.') >= 0) { + return; + } + capturedInfo.push(key); + capturedInfo.push(key + '.'); + results.push(hoverInfo); + } + }); + return new Hover(results); + } +} \ No newline at end of file diff --git a/src/client/jedi/parsers/LocationParser.ts b/src/client/jedi/parsers/LocationParser.ts new file mode 100644 index 000000000000..a118e77900d6 --- /dev/null +++ b/src/client/jedi/parsers/LocationParser.ts @@ -0,0 +1,23 @@ +import { Location, Range, Uri } from 'vscode'; +import * as proxy from '../../providers/jediProxy'; +export class LocationParser { + public static parse(data: proxy.IReferenceResult): Location[] { + if (!data || !Array(data.references) || data.references.length === 0) { + return []; + } + var references = data.references.filter(ref => { + if (!ref || typeof ref.columnIndex !== 'number' || typeof ref.lineIndex !== 'number' + || typeof ref.fileName !== 'string' || ref.columnIndex === -1 || ref.lineIndex === -1 || ref.fileName.length === 0) { + return false; + } + return true; + }).map(ref => { + var definitionResource = Uri.file(ref.fileName); + + var range = new Range(ref.lineIndex, ref.columnIndex, ref.lineIndex, ref.columnIndex); + return new Location(definitionResource, range); + }); + + return references; + } +} diff --git a/src/client/jedi/parsers/SignatureHelpParser.ts b/src/client/jedi/parsers/SignatureHelpParser.ts new file mode 100644 index 000000000000..f1993bf59322 --- /dev/null +++ b/src/client/jedi/parsers/SignatureHelpParser.ts @@ -0,0 +1,73 @@ +import { SignatureHelp } from 'vscode'; +import * as vscode from "vscode"; +import * as proxy from "../../providers/jediProxy"; + +const DOCSTRING_PARAM_PATTERNS = [ + "\\s*:type\\s*PARAMNAME:\\s*([^\\n, ]+)", // Sphinx + "\\s*:param\\s*(\\w?)\\s*PARAMNAME:[^\\n]+", // Sphinx param with type + "\\s*@type\\s*PARAMNAME:\\s*([^\\n, ]+)" // Epydoc +]; + +/** + * Extrct the documentation for parameters from a given docstring + * + * @param {string} paramName Name of the parameter + * @param {string} docString The docstring for the function + * @returns {string} Docstring for the parameter + */ +function extractParamDocString(paramName: string, docString: string): string { + let paramDocString = ""; + // In docstring the '*' is escaped with a backslash + paramName = paramName.replace(new RegExp("\\*", "g"), "\\\\\\*"); + + DOCSTRING_PARAM_PATTERNS.forEach(pattern => { + if (paramDocString.length > 0) { + return; + } + pattern = pattern.replace("PARAMNAME", paramName); + let regExp = new RegExp(pattern); + let matches = regExp.exec(docString); + if (matches && matches.length > 0) { + paramDocString = matches[0]; + if (paramDocString.indexOf(":") >= 0) { + paramDocString = paramDocString.substring(paramDocString.indexOf(":") + 1); + } + if (paramDocString.indexOf(":") >= 0) { + paramDocString = paramDocString.substring(paramDocString.indexOf(":") + 1); + } + } + }); + + return paramDocString.trim(); +} +export class SignatureHelpParser { + public static parse(data: proxy.IArgumentsResult): SignatureHelp { + if (!data || !Array.isArray(data.definitions) || data.definitions.length === 0) { + return new SignatureHelp(); + } + let signature = new SignatureHelp(); + signature.activeSignature = 0; + + data.definitions.forEach(def => { + signature.activeParameter = def.paramindex; + // Don't display the documentation, as vs code doesn't format the docmentation + // i.e. line feeds are not respected, long content is stripped + let sig = { + // documentation: def.docstring, + label: def.description, + parameters: [] + }; + sig.parameters = def.params.map(arg => { + if (arg.docstring.length === 0) { + arg.docstring = extractParamDocString(arg.name, def.docstring); + } + return { + documentation: arg.docstring.length > 0 ? arg.docstring : arg.description, + label: arg.description.length > 0 ? arg.description : arg.name + }; + }); + signature.signatures.push(sig); + }); + return signature; + } +} \ No newline at end of file diff --git a/src/client/jedi/parsers/SymbolInformationParser.ts b/src/client/jedi/parsers/SymbolInformationParser.ts new file mode 100644 index 000000000000..6be8ea17c35d --- /dev/null +++ b/src/client/jedi/parsers/SymbolInformationParser.ts @@ -0,0 +1,19 @@ +import { SymbolInformation, TextDocument, Range, Uri, Location } from 'vscode'; +import * as proxy from "../../providers/jediProxy"; +export class SymbolInformationParser { + public static parse(data: proxy.ISymbolResult, document: TextDocument): SymbolInformation[] { + if (!data || !Array.isArray(data.definitions) || data.definitions.length === 0) { + return []; + } + let symbols = data.definitions.filter(sym => sym.fileName === document.fileName); + return symbols.map(sym => { + const symbol = sym.kind; + const range = new Range( + sym.range.startLine, sym.range.startColumn, + sym.range.endLine, sym.range.endColumn); + const uri = Uri.file(sym.fileName); + const location = new Location(uri, range); + return new SymbolInformation(sym.text, symbol, sym.container, location); + }); + } +} \ No newline at end of file diff --git a/src/client/jedi/socketClient.ts b/src/client/jedi/socketClient.ts new file mode 100644 index 000000000000..75e08d0d5f06 --- /dev/null +++ b/src/client/jedi/socketClient.ts @@ -0,0 +1,141 @@ +"use strict"; + +import { SocketCallbackHandler } from "../common/comms/socketCallbackHandler"; +import { RequestCommands, ResponseCommands } from "./commands"; +import { SocketServer } from '../common/comms/socketServer'; +import { IdDispenser } from '../common/idDispenser'; +import { createDeferred, Deferred } from '../common/helpers'; +import { OutputChannel, CancellationToken } from 'vscode'; + +export class SocketClient extends SocketCallbackHandler { + constructor(socketServer: SocketServer, private outputChannel: OutputChannel) { + super(socketServer); + this.registerCommandHandler(ResponseCommands.Pong, this.onPong.bind(this)); + this.registerCommandHandler(ResponseCommands.Error, this.onError.bind(this)); + this.registerCommandHandler(ResponseCommands.TraceLog, this.onWriteToLog.bind(this)); + + this.registerCommandHandler(ResponseCommands.Signature, this.onResponseReceived.bind(this)); + this.registerCommandHandler(ResponseCommands.Completions, this.onResponseReceived.bind(this)); + this.registerCommandHandler(ResponseCommands.Definitions, this.onResponseReceived.bind(this)); + this.registerCommandHandler(ResponseCommands.Hover, this.onResponseReceived.bind(this)); + this.registerCommandHandler(ResponseCommands.DocumentSymbols, this.onResponseReceived.bind(this)); + this.registerCommandHandler(ResponseCommands.References, this.onResponseReceived.bind(this)); + + this.idDispenser = new IdDispenser(); + } + public getResult(command: Buffer, token: CancellationToken, fileName: string, columnIndex: number, lineIndex: number, source: string): Promise { + + const [def, id] = this.createId(token); + this.SendRawCommand(command); + this.stream.WriteString(id); + + this.stream.WriteString(fileName); + this.stream.WriteInt32(columnIndex); + this.stream.WriteInt32(lineIndex); + this.stream.WriteString(source || ''); + + return def.promise; + } + private idDispenser: IdDispenser; + private pid: number; + public dispose() { + super.dispose(); + } + protected handleHandshake(): boolean { + if (typeof this.pid !== 'number') { + this.pid = this.stream.readInt32InTransaction(); + if (typeof this.pid !== 'number') { + return false; + } + } + + this.emit('handshake'); + return true; + } + + private pendingCommands = new Map>(); + + private createId(token?: CancellationToken): [Deferred, string] { + const def = createDeferred(); + const id = this.idDispenser.Allocate().toString(); + this.pendingCommands.set(id, def); + if (token) { + token.onCancellationRequested(() => { + this.releaseId(id); + }); + } + return [def, id]; + } + private releaseId(id: string) { + this.pendingCommands.delete(id); + this.idDispenser.Free(parseInt(id)); + } + private onResponseReceived() { + const id = this.stream.readStringInTransaction(); + const responseStr = this.stream.readStringInTransaction(); + if (typeof responseStr !== 'string') { + return; + } + + if (!this.pendingCommands.has(id)) { + return; + } + const def = this.pendingCommands.get(id); + this.releaseId(id); + + let jsonResponse: {}; + try { + jsonResponse = JSON.parse(responseStr); + } + catch (ex) { + def.reject(ex); + return; + } + + def.resolve(jsonResponse); + } + private onWriteToLog() { + const message = this.stream.readStringInTransaction(); + if (typeof message !== 'string') { + return; + } + this.outputChannel.appendLine(message); + } + + public ping(message: string) { + const [def, id] = this.createId(null); + this.SendRawCommand(RequestCommands.Ping); + this.stream.WriteString(id); + this.stream.WriteString(message); + return def.promise; + } + + private onPong() { + const id = this.stream.readStringInTransaction(); + const message = this.stream.readStringInTransaction(); + if (typeof message !== 'string') { + return; + } + const def = this.pendingCommands.get(id); + this.releaseId(id); + def.resolve(message); + } + private onError() { + const cmd = this.stream.readStringInTransaction(); + const id = this.stream.readStringInTransaction(); + const trace = this.stream.readStringInTransaction(); + if (typeof trace !== 'string') { + return; + } + if (cmd === 'exit') { + return; + } + if (id.length > 0 && this.pendingCommands.has(id)) { + const def = this.pendingCommands.get(id); + this.pendingCommands.delete(id); + def.reject(new Error(`Command: ${cmd}, Id: ${id}, Python Trace: ${trace}`)); + return; + } + this.emit("commanderror", { command: cmd, id: id, trace: trace }); + } +} diff --git a/src/client/jupyter/common/cellHelper.ts b/src/client/jupyter/common/cellHelper.ts index c2fbdae7f270..88853f5119cc 100644 --- a/src/client/jupyter/common/cellHelper.ts +++ b/src/client/jupyter/common/cellHelper.ts @@ -3,7 +3,7 @@ import { TextDocument, Range } from 'vscode'; import { JupyterCodeLensProvider } from '../editorIntegration/codeLensProvider'; import * as vscode from 'vscode'; -const CellIdentifier = /^(# %%|#%%|# \|# In\[\d?\]|# In\[ \])(.*)/i; +const CellIdentifier = /^(# %%|#%%|# \|# In\[\d*?\]|# In\[ \])(.*)/i; export class CellHelper { constructor(private cellCodeLenses: JupyterCodeLensProvider) { diff --git a/src/client/jupyter/common/constants.ts b/src/client/jupyter/common/constants.ts new file mode 100644 index 000000000000..fb0e37c9e225 --- /dev/null +++ b/src/client/jupyter/common/constants.ts @@ -0,0 +1,31 @@ + +export const PythonLanguage = { language: 'python' }; + +export namespace Commands { + export namespace Jupyter { + export const Get_All_KernelSpecs_For_Language = 'jupyter.getAllKernelSpecsForLanguage'; + export const Get_All_KernelSpecs = 'jupyter.getAllKernelSpecs'; + export const Kernel_Options = 'jupyter.kernelOptions'; + export const StartKernelForKernelSpeck = 'jupyter.sartKernelForKernelSpecs'; + export const ExecuteRangeInKernel = 'jupyter.execRangeInKernel'; + export const ExecuteSelectionOrLineInKernel = 'jupyter.runSelectionLine'; + export namespace Cell { + export const ExecuteCurrentCell = 'jupyter.execCurrentCell'; + export const ExecuteCurrentCellAndAdvance = 'jupyter.execCurrentCellAndAdvance'; + export const AdcanceToCell = 'jupyter.advanceToNextCell'; + export const DisplayCellMenu = 'jupyter.displayCellMenu'; + export const GoToPreviousCell = 'jupyter.gotToPreviousCell'; + export const GoToNextCell = 'jupyter.gotToNextCell'; + } + export namespace Kernel { + export const Select = 'jupyter.selectKernel'; + export const Interrupt = 'jupyter.kernelInterrupt'; + export const Restart = 'jupyter.kernelRestart'; + export const Shutdown = 'jupyter.kernelShutDown'; + export const Details = 'jupyter.kernelDetails'; + } + export namespace Notebook { + export const ShutDown = 'jupyter.shutdown'; + } + } +} \ No newline at end of file diff --git a/src/client/jupyter/common/contracts.ts b/src/client/jupyter/common/contracts.ts new file mode 100644 index 000000000000..9195dce44c8e --- /dev/null +++ b/src/client/jupyter/common/contracts.ts @@ -0,0 +1,12 @@ + +export interface JupyterSettings { + appendResults: boolean; + pythonPath: string; + languages: JupyterLanguageSetting[]; +} + +export interface JupyterLanguageSetting { + languageId: string; + defaultKernel?: string; + startupCode?: string[]; +} \ No newline at end of file diff --git a/src/client/jupyter/common/languageProvider.ts b/src/client/jupyter/common/languageProvider.ts new file mode 100644 index 000000000000..0af20dcd11db --- /dev/null +++ b/src/client/jupyter/common/languageProvider.ts @@ -0,0 +1,89 @@ +import { Range, Position, TextDocument, workspace } from 'vscode'; +import { JupyterLanguageSetting } from './contracts'; +import { EOL } from 'os'; + +/** + * Language providers + * + * @export + * @interface LanguageProvider + */ +export interface LanguageProvider { + /** + * Returns a Regular Expression used to determine whether a line is a Cell delimiter or not + * + * @type {RegExp} + * @memberOf LanguageProvider + */ + cellIdentifier: RegExp; + + /** + * Returns the selected code + * If not implemented, then the currently active line or selected code is taken. + * Can be implemented to ensure valid blocks of code are selected. + * E.g if user selects only the If statement, code can be impelemented to ensure all code within the if statement (block) is returned + * @param {string} selectedCode The selected code as identified by this extension. + * @param {Range} [currentCell] Range of the currently active cell + * @returns {Promise} The code selected. If nothing is to be done, return the parameter value. + * + * @memberOf LanguageProvider + */ + getSelectedCode(selectedCode: string, currentCell?: Range): Promise; + + /** + * Gets the first line (position) of executable code within a range + * + * @param {TextDocument} document + * @param {number} startLine + * @param {number} endLine + * @returns {Promise} + * + * @memberOf LanguageProvider + */ + getFirstLineOfExecutableCode(document: TextDocument, range: Range): Promise; +} + +export class LanguageProviders { + private static providers: Map = new Map(); + public static registerLanguageProvider(language: string, provider: LanguageProvider) { + if (typeof language !== 'string' || language.length === 0) { + throw new Error(`Argument 'language' is invalid`); + } + if (typeof provider !== 'object' || language === null) { + throw new Error(`Argument 'provider' is invalid`); + } + LanguageProviders.providers.set(language, provider); + } + public static cellIdentifier(language: string): RegExp { + return LanguageProviders.providers.has(language) ? + LanguageProviders.providers.get(language).cellIdentifier : null; + } + public static getSelectedCode(language: string, selectedCode: string, currentCell?: Range): Promise { + return LanguageProviders.providers.has(language) ? + LanguageProviders.providers.get(language).getSelectedCode(selectedCode, currentCell) : + Promise.resolve(selectedCode); + } + public static getFirstLineOfExecutableCode(language: string, defaultRange: Range, document: TextDocument, range: Range): Promise | Promise { + return LanguageProviders.providers.has(language) ? + LanguageProviders.providers.get(language).getFirstLineOfExecutableCode(document, range) : + Promise.resolve(defaultRange); + } + private static getLanguageSetting(language: string): JupyterLanguageSetting { + let jupyterConfig = workspace.getConfiguration('jupyter'); + let langSettings = jupyterConfig.get('languages') as JupyterLanguageSetting[]; + let lowerLang = language.toLowerCase(); + return langSettings.find(setting => setting.languageId.toLowerCase() === lowerLang); + } + + public static getDefaultKernel(language: string): string { + let langSetting = LanguageProviders.getLanguageSetting(language); + return langSetting ? langSetting.defaultKernel : null; + } + public static getStartupCode(language: string): string { + let langSetting = LanguageProviders.getLanguageSetting(language); + if (!langSetting || langSetting.startupCode.length === 0) { + return null; + } + return langSetting.startupCode.join(EOL); + } +} \ No newline at end of file diff --git a/src/client/jupyter/common/utils.ts b/src/client/jupyter/common/utils.ts new file mode 100644 index 000000000000..f0482c1887ff --- /dev/null +++ b/src/client/jupyter/common/utils.ts @@ -0,0 +1,80 @@ + +'use strict'; +// TODO: Cleanup this place +// Add options for execPythonFile +import * as path from 'path'; +import * as fs from 'fs'; + +export const IS_WINDOWS = /^win/.test(process.platform); +export const PATH_VARIABLE_NAME = IS_WINDOWS ? 'Path' : 'PATH'; + +const PathValidity: Map = new Map(); +export function validatePath(filePath: string): Promise { + if (filePath.length === 0) { + return Promise.resolve(''); + } + if (PathValidity.has(filePath)) { + return Promise.resolve(PathValidity.get(filePath) ? filePath : ''); + } + return new Promise(resolve => { + fs.exists(filePath, exists => { + PathValidity.set(filePath, exists); + return resolve(exists ? filePath : ''); + }); + }); +} +export function fsExistsAsync(filePath: string): Promise { + return new Promise(resolve => { + fs.exists(filePath, exists => { + PathValidity.set(filePath, exists); + return resolve(exists); + }); + }); +} + +export function formatErrorForLogging(error: Error | string): string { + let message: string = ''; + if (typeof error === 'string') { + message = error; + } + else { + if (error.message) { + message = `Error Message: ${error.message}`; + } + if (error.name && error.message.indexOf(error.name) === -1) { + message += `, (${error.name})`; + } + const innerException = (error as any).innerException; + if (innerException && (innerException.message || innerException.name)) { + if (innerException.message) { + message += `, Inner Error Message: ${innerException.message}`; + } + if (innerException.name && innerException.message.indexOf(innerException.name) === -1) { + message += `, (${innerException.name})`; + } + } + } + return message; +} + +export function getSubDirectories(rootDir: string): Promise { + return new Promise(resolve => { + fs.readdir(rootDir, (error, files) => { + if (error) { + return resolve([]); + } + const subDirs = []; + files.forEach(name => { + const fullPath = path.join(rootDir, name); + try { + if (fs.statSync(fullPath).isDirectory()) { + subDirs.push(fullPath); + } + } + catch (ex) { + } + }); + resolve(subDirs); + }); + }); +} \ No newline at end of file diff --git a/src/client/jupyter/display/kernelStatus.ts b/src/client/jupyter/display/kernelStatus.ts index 1434ec0406a9..6dc02b588329 100644 --- a/src/client/jupyter/display/kernelStatus.ts +++ b/src/client/jupyter/display/kernelStatus.ts @@ -10,9 +10,9 @@ export class KernelStatus extends vscode.Disposable { super(() => { }); this.disposables = []; this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); - this.statusBar.command = 'jupyter:proxyKernelOptionsCmd'; + this.statusBar.command = 'jupyter.proxyKernelOptionsCmd'; this.disposables.push(this.statusBar); - this.disposables.push(vscode.commands.registerCommand('jupyter:proxyKernelOptionsCmd', () => { + this.disposables.push(vscode.commands.registerCommand('jupyter.proxyKernelOptionsCmd', () => { vscode.commands.executeCommand(Commands.Jupyter.Kernel_Options, this.activeKernalDetails); })); diff --git a/src/client/jupyter/editorIntegration/codeLensProvider.ts b/src/client/jupyter/editorIntegration/codeLensProvider.ts index 908bdd3a6d0d..a0548848aabf 100644 --- a/src/client/jupyter/editorIntegration/codeLensProvider.ts +++ b/src/client/jupyter/editorIntegration/codeLensProvider.ts @@ -1,7 +1,6 @@ 'use strict'; -import {CodeLensProvider, TextDocument, CancellationToken, CodeLens, Command} from 'vscode'; -import * as telemetryContracts from '../../common/telemetryContracts'; +import {CancellationToken, CodeLens, CodeLensProvider, Command, TextDocument} from 'vscode'; import {Commands} from '../../common/constants'; import {CellHelper} from '../common/cellHelper'; @@ -37,4 +36,4 @@ export class JupyterCodeLensProvider implements CodeLensProvider { this.cache.push({ fileName: document.fileName, documentVersion: document.version, lenses: lenses }); return Promise.resolve(lenses); } -} \ No newline at end of file +} diff --git a/src/client/jupyter/editorIntegration/symbolProvider.ts b/src/client/jupyter/editorIntegration/symbolProvider.ts index 4c9a49a6fa04..64afc98d3eb2 100644 --- a/src/client/jupyter/editorIntegration/symbolProvider.ts +++ b/src/client/jupyter/editorIntegration/symbolProvider.ts @@ -1,6 +1,5 @@ 'use strict'; -import {DocumentSymbolProvider, TextDocument, CancellationToken, SymbolInformation, SymbolKind} from 'vscode'; -import * as telemetryContracts from '../../common/telemetryContracts'; +import {CancellationToken, DocumentSymbolProvider, SymbolInformation, SymbolKind, TextDocument} from 'vscode'; import {CellHelper} from '../common/cellHelper'; export class JupyterSymbolProvider implements DocumentSymbolProvider { diff --git a/src/client/jupyter/jupyter_client/jupyterSocketClient.ts b/src/client/jupyter/jupyter_client/jupyterSocketClient.ts index 812025e3fe9b..c00e67464bcd 100644 --- a/src/client/jupyter/jupyter_client/jupyterSocketClient.ts +++ b/src/client/jupyter/jupyter_client/jupyterSocketClient.ts @@ -16,7 +16,7 @@ export class JupyterSocketClient extends SocketCallbackHandler { private isDebugging: boolean; constructor(socketServer: SocketServer, private outputChannel: OutputChannel) { super(socketServer); - this.isDebugging = process.env['DEBUG_DJAYAMANNE_IPYTHON'] === '1'; + this.isDebugging = process.env['DEBUG_EXTENSION_IPYTHON'] === '1'; this.registerCommandHandler(ResponseCommands.Pong, this.onPong.bind(this)); this.registerCommandHandler(ResponseCommands.ListKernelsSpecs, this.onKernelsListed.bind(this)); this.registerCommandHandler(ResponseCommands.Error, this.onError.bind(this)); @@ -187,7 +187,7 @@ export class JupyterSocketClient extends SocketCallbackHandler { def.resolve(); } public ping(message: string) { - const [def, id] = this.createId(); + const [def, id] = this.createId(); this.SendRawCommand(Commands.PingBytes); this.stream.WriteString(id); this.stream.WriteString(message); diff --git a/src/client/jupyter/jupyter_client/main.ts b/src/client/jupyter/jupyter_client/main.ts index aacf7067f184..f55e4dda4f89 100644 --- a/src/client/jupyter/jupyter_client/main.ts +++ b/src/client/jupyter/jupyter_client/main.ts @@ -57,7 +57,7 @@ export class JupyterClientAdapter extends EventEmitter implements IJupyterClient this.startDef = createDeferred(); const pyFile = path.join(__dirname, '..', '..', '..', '..', 'pythonFiles', 'PythonTools', 'ipythonServer.py'); const newEnv = {}; - // const newEnv = {'DEBUG_DJAYAMANNE_IPYTHON':'1'}; + // const newEnv = {'DEBUG_EXTENSION_IPYTHON':'1'}; Object.assign(newEnv, envVariables); Object.assign(newEnv, process.env); @@ -70,7 +70,7 @@ export class JupyterClientAdapter extends EventEmitter implements IJupyterClient let processStarted = false; let handshakeDone = false; - let isInTestRun = newEnv['PYTHON_DONJAYAMANNE_TEST'] === "1"; + let isInTestRun = newEnv['VSC_PYTHON_CI_TEST'] === "1"; const testDef = createDeferred(); const promiseToResolve = isInTestRun ? testDef.resolve.bind(testDef) : def.resolve.bind(def); @@ -101,7 +101,7 @@ export class JupyterClientAdapter extends EventEmitter implements IJupyterClient // Ok everything has started, now test ping const msg1 = 'Hello world from Type Script - ะคัƒะฝะบั†ะธั ะฟั€ะพะฒะตั€ะบะธ ะ˜ะะ ะธ ะšะŸะŸ - ้•ฟๅŸŽ!1'; const msg2 = 'Hello world from Type Script - ะคัƒะฝะบั†ะธั ะฟั€ะพะฒะตั€ะบะธ ะ˜ะะ ะธ ะšะŸะŸ - ้•ฟๅŸŽ!2'; - Promise.all([this.ipythonAdapter.ping(msg1), this.ipythonAdapter.ping(msg2)]).then(msgs => { + Promise.all([this.ipythonAdapter.ping(msg1), this.ipythonAdapter.ping(msg2)]).then(msgs => { if (msgs.indexOf(msg1) === -1 || msgs.indexOf(msg2) === -1) { def.reject('msg1 or msg2 not returned'); } @@ -189,4 +189,4 @@ export class JupyterClientAdapter extends EventEmitter implements IJupyterClient return subject; } -} \ No newline at end of file +} diff --git a/src/client/jupyter/jupyter_client/resultParser.ts b/src/client/jupyter/jupyter_client/resultParser.ts new file mode 100644 index 000000000000..a7fee33f66cb --- /dev/null +++ b/src/client/jupyter/jupyter_client/resultParser.ts @@ -0,0 +1,81 @@ +"use strict"; + +import { EventEmitter } from "events"; +import * as Rx from 'rx'; +import { OutputChannel } from 'vscode'; +import { ParsedIOMessage } from '../contracts'; +import { Helpers } from '../common/helpers'; +type KernelMessage = any; + + +export class MessageParser extends EventEmitter { + private isDebugging: boolean; + constructor(private outputChannel: OutputChannel) { + super(); + this.isDebugging = process.env['DEBUG_EXTENSION_IPYTHON'] === '1'; + } + private writeToDebugLog(message: string) { + if (!this.isDebugging) { + return; + } + this.outputChannel.appendLine(message); + } + public processResponse(message: KernelMessage, observer?: Rx.Observer) { + if (!message) { + return; + } + if (!Helpers.isValidMessag(message)) { + return; + } + try { + const msg_type = message.header.msg_type; + if (msg_type === 'status') { + this.writeToDebugLog(`Kernel Status = ${(message.content as any).execution_state}`); + this.emit('status', (message.content as any).execution_state); + } + const msg_id = (message.parent_header as any).msg_id; + if (!msg_id) { + return; + } + const status = (message.content as any).status; + let parsedMesage: ParsedIOMessage; + switch (status) { + case 'abort': + case 'aborted': + case 'error': { + // http://jupyter-client.readthedocs.io/en/latest/messaging.html#request-reply + if (msg_type !== 'complete_reply' && msg_type !== 'inspect_reply') { + parsedMesage = { + data: 'error', + type: 'text', + stream: 'status' + }; + } + break; + } + case 'ok': { + // http://jupyter-client.readthedocs.io/en/latest/messaging.html#request-reply + if (msg_type !== 'complete_reply' && msg_type !== 'inspect_reply') { + parsedMesage = { + data: 'ok', + type: 'text', + stream: 'status' + }; + } + } + } + this.writeToDebugLog(`Shell Result with msg_id = ${msg_id} with status = ${status}`); + if (!parsedMesage) { + parsedMesage = Helpers.parseIOMessage(message); + } + if (!parsedMesage || !observer) { + return; + } + this.writeToDebugLog(`Shell Result with msg_id = ${msg_id} has message of: '\n${JSON.stringify(message)}`); + observer.onNext(parsedMesage); + } + catch (ex) { + this.emit('shellmessagepareerror', ex, JSON.stringify(message)); + } + } +} diff --git a/src/client/jupyter/kernel-manager.ts b/src/client/jupyter/kernel-manager.ts index ffbf118520b2..cd72a8313b35 100644 --- a/src/client/jupyter/kernel-manager.ts +++ b/src/client/jupyter/kernel-manager.ts @@ -88,16 +88,16 @@ export class KernelManagerImpl extends EventEmitter { throw new Error('Start Existing Kernel not implemented'); } - public startKernel(kernelSpec: KernelspecMetadata, language: string): Promise { + public async startKernel(kernelSpec: KernelspecMetadata, language: string): Promise { this.destroyRunningKernelFor(language); - return this.jupyterClient.startKernel(kernelSpec).then((kernelInfo: [string, any, string]) => { - const kernelUUID = kernelInfo[0]; - const config = kernelInfo[1]; - const connectionFile = kernelInfo[2]; - const kernel = new JupyterClientKernel(kernelSpec, language, config, connectionFile, kernelUUID, this.jupyterClient); - this.setRunningKernelFor(language, kernel); - return this.executeStartupCode(kernel).then(() => kernel); - }); + const kernelInfo = await this.jupyterClient.startKernel(kernelSpec); + const kernelUUID = kernelInfo[0]; + const config = kernelInfo[1]; + const connectionFile = kernelInfo[2]; + const kernel = new JupyterClientKernel(kernelSpec, language, config, connectionFile, kernelUUID, this.jupyterClient); + this.setRunningKernelFor(language, kernel); + await this.executeStartupCode(kernel); + return kernel; } private executeStartupCode(kernel: Kernel): Promise { @@ -186,4 +186,4 @@ export class KernelManagerImpl extends EventEmitter { public getKernelSpecsFromJupyter(): Promise { return this.jupyterClient.getAllKernelSpecs(); } -} \ No newline at end of file +} diff --git a/src/client/jupyter/main.ts b/src/client/jupyter/main.ts index 13816623a263..c8373cf60025 100644 --- a/src/client/jupyter/main.ts +++ b/src/client/jupyter/main.ts @@ -7,9 +7,6 @@ import { Commands, PythonLanguage } from '../common/constants'; import { JupyterCodeLensProvider } from './editorIntegration/codeLensProvider'; import { JupyterSymbolProvider } from './editorIntegration/symbolProvider'; import { formatErrorForLogging } from '../common/utils'; -import { Documentation } from '../common/constants'; -import * as telemetryHelper from '../common/telemetry'; -import * as telemetryContracts from '../common/telemetryContracts'; import * as main from './jupyter_client/main'; import { KernelRestartedError, KernelShutdownError } from './common/errors'; import { PythonSettings } from '../common/configSettings'; @@ -28,6 +25,27 @@ export class Jupyter extends vscode.Disposable { private codeLensProvider: JupyterCodeLensProvider; private lastUsedPythonPath: string; private codeHelper: CodeHelper; + + private async displaySuggestion(): Promise { + return new Promise(resolve => { + let recommend = vscode.workspace.getConfiguration('python').get('promptToInstallJupyter', true); + if (!recommend) { + return resolve(); + } + vscode.window.showInformationMessage('Deprecated: Please install the new Jupyter extension. Jupyter functionality within this extension has been deprecated.', 'Do not show again') + .then(item => { + if (item !== 'Do not show again') { + return resolve(); + } + vscode.workspace.getConfiguration('python').update('promptToInstallJupyter', false) + .then(() => { + resolve(); + }, ex => { + resolve(); + }); + }); + }); + } constructor(private outputChannel: vscode.OutputChannel) { super(() => { }); this.disposables = []; @@ -107,10 +125,7 @@ export class Jupyter extends vscode.Disposable { this.status.setActiveKernel(this.kernel ? this.kernel.kernelSpec : null); } executeCode(code: string, language: string): Promise { - // const m = new main.JupyterClient(this.outputChannel); - // m.start(); - // return Promise.resolve(); - telemetryHelper.sendTelemetryEvent(telemetryContracts.Jupyter.Usage); + // telemetryHelper.sendTelemetryEvent(telemetryContracts.Jupyter.Usage); if (this.kernel && this.kernel.kernelSpec.language === language) { return this.executeAndDisplay(this.kernel, code).catch(reason => { @@ -119,7 +134,8 @@ export class Jupyter extends vscode.Disposable { this.outputChannel.appendLine(formatErrorForLogging(reason)); }); } - return this.kernelManager.startKernelFor(language) + return this.displaySuggestion() + .then(() => this.kernelManager.startKernelFor(language)) .then(kernel => { if (kernel) { this.onKernelChanged(kernel); @@ -128,10 +144,7 @@ export class Jupyter extends vscode.Disposable { }).catch(reason => { const message = typeof reason === 'string' ? reason : reason.message; this.outputChannel.appendLine(formatErrorForLogging(reason)); - vscode.window.showErrorMessage(message, 'Help', 'View Errors').then(item => { - if (item === 'Help') { - vscode.commands.executeCommand('python.displayHelp', Documentation.Jupyter.Setup); - } + vscode.window.showErrorMessage(message, 'View Errors').then(item => { if (item === 'View Errors') { this.outputChannel.show(); } @@ -211,4 +224,4 @@ export class Jupyter extends vscode.Disposable { this.onKernelChanged(); })); } -}; \ No newline at end of file +}; diff --git a/src/client/jupyter/provider.ts b/src/client/jupyter/provider.ts new file mode 100644 index 000000000000..3b4b1fbafaf9 --- /dev/null +++ b/src/client/jupyter/provider.ts @@ -0,0 +1,90 @@ +import { Range, window, TextDocument, Position } from 'vscode'; + +export class JupyterProvider { + /** + * Returns a Regular Expression used to determine whether a line is a Cell delimiter or not + * + * @type {RegExp} + * @memberOf LanguageProvider + */ + get cellIdentifier(): RegExp { + return /^(# %%|#%%|# \|# In\[\d*?\]|# In\[ \])(.*)/i; + } + + /** + * Returns the selected code + * If not implemented, then the currently active line or selected code is taken. + * Can be implemented to ensure valid blocks of code are selected. + * E.g if user selects only the If statement, code can be impelemented to ensure all code within the if statement (block) is returned + * @param {string} selectedCode The selected code as identified by this extension. + * @param {Range} [currentCell] Range of the currently active cell + * @returns {Promise} The code selected. If nothing is to be done, return the parameter value. + * + * @memberOf LanguageProvider + */ + getSelectedCode(selectedCode: string, currentCell?: Range): Promise { + if (!JupyterProvider.isCodeBlock(selectedCode)) { + return Promise.resolve(selectedCode); + } + + // ok we're in a block, look for the end of the block untill the last line in the cell (if there are any cells) + return new Promise((resolve, reject) => { + const activeEditor = window.activeTextEditor; + const endLineNumber = currentCell ? currentCell.end.line : activeEditor.document.lineCount - 1; + const startIndent = selectedCode.indexOf(selectedCode.trim()); + const nextStartLine = activeEditor.selection.start.line + 1; + + for (let lineNumber = nextStartLine; lineNumber <= endLineNumber; lineNumber++) { + const line = activeEditor.document.lineAt(lineNumber); + const nextLine = line.text; + const nextLineIndent = nextLine.indexOf(nextLine.trim()); + if (nextLine.trim().indexOf('#') === 0) { + continue; + } + if (nextLineIndent === startIndent) { + // Return code untill previous line + const endRange = activeEditor.document.lineAt(lineNumber - 1).range.end; + resolve(activeEditor.document.getText(new Range(activeEditor.selection.start, endRange))); + } + } + + resolve(activeEditor.document.getText(currentCell)); + }); + } + + /** + * Gets the first line (position) of executable code within a range + * + * @param {TextDocument} document + * @param {number} startLine + * @param {number} endLine + * @returns {Promise} + * + * @memberOf LanguageProvider + */ + getFirstLineOfExecutableCode(document: TextDocument, range: Range): Promise { + for (let lineNumber = range.start.line; lineNumber < range.end.line; lineNumber++) { + let line = document.lineAt(lineNumber); + if (line.isEmptyOrWhitespace) { + continue; + } + const lineText = line.text; + const trimmedLine = lineText.trim(); + if (trimmedLine.startsWith('#')) { + continue; + } + // Yay we have a line + // Remember, we need to set the cursor to a character other than white space + // Highlighting doesn't kick in for comments or white space + return Promise.resolve(new Position(lineNumber, lineText.indexOf(trimmedLine))); + } + + // give up + return Promise.resolve(new Position(range.start.line, 0)); + } + + private static isCodeBlock(code: string): boolean { + return code.trim().endsWith(':') && code.indexOf('#') === -1; + } + +} \ No newline at end of file diff --git a/src/client/languageServices/jediProxyFactory.ts b/src/client/languageServices/jediProxyFactory.ts new file mode 100644 index 000000000000..9af0b012d648 --- /dev/null +++ b/src/client/languageServices/jediProxyFactory.ts @@ -0,0 +1,38 @@ +import { Disposable, Uri, workspace } from 'vscode'; +import { JediProxy, JediProxyHandler, ICommandResult } from '../providers/jediProxy'; + +export class JediFactory implements Disposable { + private disposables: Disposable[]; + private jediProxyHandlers: Map>; + + constructor(private extensionRootPath: string) { + this.disposables = []; + this.jediProxyHandlers = new Map>(); + } + + public dispose() { + this.disposables.forEach(disposable => disposable.dispose()); + this.disposables = []; + } + public getJediProxyHandler(resource: Uri): JediProxyHandler { + const workspaceFolder = workspace.getWorkspaceFolder(resource); + let workspacePath = workspaceFolder ? workspaceFolder.uri.fsPath : undefined; + if (!workspacePath) { + if (Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0) { + workspacePath = workspace.workspaceFolders[0].uri.fsPath; + } + else { + workspacePath = __dirname; + } + } + + if (!this.jediProxyHandlers.has(workspacePath)) { + const jediProxy = new JediProxy(this.extensionRootPath, workspacePath); + const jediProxyHandler = new JediProxyHandler(jediProxy); + this.disposables.push(jediProxy, jediProxyHandler); + this.jediProxyHandlers.set(workspacePath, jediProxyHandler); + } + // tslint:disable-next-line:no-non-null-assertion + return this.jediProxyHandlers.get(workspacePath)! as JediProxyHandler; + } +} diff --git a/src/client/linters/baseLinter.ts b/src/client/linters/baseLinter.ts index 4242984b205a..4b4f1d61541e 100644 --- a/src/client/linters/baseLinter.ts +++ b/src/client/linters/baseLinter.ts @@ -1,11 +1,13 @@ 'use strict'; -import { execPythonFile } from './../common/utils'; -import * as settings from './../common/configSettings'; -import { OutputChannel } from 'vscode'; -import { isNotInstalledError } from '../common/helpers'; -import { Installer, Product, disableLinter } from '../common/installer'; +import * as path from 'path'; +import { OutputChannel, Uri } from 'vscode'; import * as vscode from 'vscode'; +import { IPythonSettings, PythonSettings } from '../common/configSettings'; +import { Installer, Product } from '../common/installer'; +import { execPythonFile } from './../common/utils'; +import { ErrorHandler } from './errorHandlers/main'; +// tslint:disable-next-line:variable-name let NamedRegexp = null; const REGEX = '(?\\d+),(?\\d+),(?\\w+),(?\\w\\d+):(?.*)\\r?(\\n|$)'; @@ -23,7 +25,6 @@ export interface ILintMessage { code: string; message: string; type: string; - possibleWord?: string; severity?: LintMessageSeverity; provider: string; } @@ -36,11 +37,12 @@ export enum LintMessageSeverity { export function matchNamedRegEx(data, regex): IRegexGroup { if (NamedRegexp === null) { + // tslint:disable-next-line:no-require-imports NamedRegexp = require('named-js-regexp'); } - let compiledRegexp = NamedRegexp(regex, 'g'); - let rawMatch = compiledRegexp.exec(data); + const compiledRegexp = NamedRegexp(regex, 'g'); + const rawMatch = compiledRegexp.exec(data); if (rawMatch !== null) { return rawMatch.groups(); } @@ -48,116 +50,130 @@ export function matchNamedRegEx(data, regex): IRegexGroup { return null; } +type LinterId = 'flake8' | 'mypy' | 'pep8' | 'prospector' | 'pydocstyle' | 'pylama' | 'pylint'; export abstract class BaseLinter { - public Id: string; - private installer: Installer; - protected pythonSettings: settings.IPythonSettings; - private _workspaceRootPath: string; - protected get workspaceRootPath(): string { - return typeof this._workspaceRootPath === 'string' ? this._workspaceRootPath : vscode.workspace.rootPath; + // tslint:disable-next-line:variable-name + public Id: LinterId; + // tslint:disable-next-line:variable-name + protected _columnOffset = 0; + // tslint:disable-next-line:variable-name + private _errorHandler: ErrorHandler; + // tslint:disable-next-line:variable-name + private _pythonSettings: IPythonSettings; + protected get pythonSettings(): IPythonSettings { + return this._pythonSettings; } - constructor(id: string, private product: Product, protected outputChannel: OutputChannel, workspaceRootPath: string) { + constructor(id: LinterId, public product: Product, protected outputChannel: OutputChannel) { this.Id = id; - this.installer = new Installer(); - this._workspaceRootPath = workspaceRootPath; - this.pythonSettings = settings.PythonSettings.getInstance(); + this._errorHandler = new ErrorHandler(this.Id, product, new Installer(), this.outputChannel); } - public abstract isEnabled(): Boolean; - public abstract runLinter(document: vscode.TextDocument): Promise; - - protected run(command: string, args: string[], document: vscode.TextDocument, cwd: string, regEx: string = REGEX): Promise { - let outputChannel = this.outputChannel; - - return new Promise((resolve, reject) => { - execPythonFile(command, args, cwd, true).then(data => { - outputChannel.append('#'.repeat(10) + 'Linting Output - ' + this.Id + '#'.repeat(10) + '\n'); - outputChannel.append(data); - let outputLines = data.split(/\r?\n/g); - let diagnostics: ILintMessage[] = []; - outputLines.filter((value, index) => index <= this.pythonSettings.linting.maxNumberOfProblems).forEach(line => { - let match = matchNamedRegEx(line, regEx); - if (!match) { - return; - } - - try { - match.line = Number(match.line); - match.column = Number(match.column); - - let possibleWord: string; - if (!isNaN(match.column)) { - let sourceLine = document.lineAt(match.line - 1).text; - let sourceStart = sourceLine.substring(match.column - 1); - - // try to get the first word from the startig position - let possibleProblemWords = sourceStart.match(/\w+/g); - if (possibleProblemWords != null && possibleProblemWords.length > 0 && sourceStart.startsWith(possibleProblemWords[0])) { - possibleWord = possibleProblemWords[0]; - } - } - - diagnostics.push({ - code: match.code, - message: match.message, - column: isNaN(match.column) || match.column === 0 ? 0 : match.column - 1, - line: match.line, - possibleWord: possibleWord, - type: match.type, - provider: this.Id - }); - } - catch (ex) { - // Hmm, need to handle this later - // TODO: + public isEnabled(resource: Uri) { + this._pythonSettings = PythonSettings.getInstance(resource); + const enabledSetting = `${this.Id}Enabled`; + // tslint:disable-next-line:prefer-type-cast + return this._pythonSettings.linting[enabledSetting] as boolean; + } + public linterArgs(resource: Uri) { + this._pythonSettings = PythonSettings.getInstance(resource); + const argsSetting = `${this.Id}Args`; + // tslint:disable-next-line:prefer-type-cast + return this._pythonSettings.linting[argsSetting] as string[]; + } + public isLinterExecutableSpecified(resource: Uri) { + this._pythonSettings = PythonSettings.getInstance(resource); + const argsSetting = `${this.Id}Path`; + // tslint:disable-next-line:prefer-type-cast + const executablePath = this._pythonSettings.linting[argsSetting] as string; + return path.basename(executablePath).length > 0 && path.basename(executablePath) !== executablePath; + } + public lint(document: vscode.TextDocument, cancellation: vscode.CancellationToken): Promise { + this._pythonSettings = PythonSettings.getInstance(document.uri); + return this.runLinter(document, cancellation); + } + protected getWorkspaceRootPath(document: vscode.TextDocument): string { + const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri); + const workspaceRootPath = (workspaceFolder && typeof workspaceFolder.uri.fsPath === 'string') ? workspaceFolder.uri.fsPath : undefined; + return typeof workspaceRootPath === 'string' ? workspaceRootPath : __dirname; + } + protected abstract runLinter(document: vscode.TextDocument, cancellation: vscode.CancellationToken): Promise; + // tslint:disable-next-line:no-any + protected parseMessagesSeverity(error: string, categorySeverity: any): LintMessageSeverity { + if (categorySeverity[error]) { + const severityName = categorySeverity[error]; + switch (severityName) { + case 'Error': + return LintMessageSeverity.Error; + case 'Hint': + return LintMessageSeverity.Hint; + case 'Information': + return LintMessageSeverity.Information; + case 'Warning': + return LintMessageSeverity.Warning; + default: { + if (LintMessageSeverity[severityName]) { + // tslint:disable-next-line:no-any + return LintMessageSeverity[severityName]; } - }); + } + } + } - resolve(diagnostics); - }).catch(error => { - this.handleError(this.Id, command, error); - resolve([]); - }); + return LintMessageSeverity.Information; + } + protected run(command: string, args: string[], document: vscode.TextDocument, cwd: string, cancellation: vscode.CancellationToken, regEx: string = REGEX): Promise { + return execPythonFile(document.uri, command, args, cwd, true, null, cancellation).then(data => { + if (!data) { + data = ''; + } + this.displayLinterResultHeader(data); + const outputLines = data.split(/\r?\n/g); + return this.parseLines(outputLines, regEx); + }).catch(error => { + this.handleError(this.Id, command, error, document.uri); + return []; }); } + protected handleError(expectedFileName: string, fileName: string, error: Error, resource: Uri) { + this._errorHandler.handleError(expectedFileName, fileName, error, resource); + } - protected handleError(expectedFileName: string, fileName: string, error: Error) { - let customError = `Linting with ${this.Id} failed.`; - - if (isNotInstalledError(error)) { - // Check if we have some custom arguments such as "pylint --load-plugins pylint_django" - // Such settings are no longer supported - let stuffAfterFileName = fileName.substring(fileName.toUpperCase().lastIndexOf(expectedFileName) + expectedFileName.length); - - // Ok if we have a space after the file name, this means we have some arguments defined and this isn't supported - if (stuffAfterFileName.trim().indexOf(' ') > 0) { - customError = `Linting failed, custom arguments in the 'python.linting.${this.Id}Path' is not supported.\n` + - `Custom arguments to the linters can be defined in 'python.linting.${this.Id}Args' setting of settings.json.\n` + - 'For further details, please see https://github.com/DonJayamanne/pythonVSCode/wiki/Troubleshooting-Linting#2-linting-with-xxx-failed-'; - vscode.window.showErrorMessage(`Unsupported configuration for '${this.Id}'`, 'View Errors').then(item => { - if (item === 'View Errors') { - this.outputChannel.show(); - } - }); - } - else { - customError += `\nYou could either install the '${this.Id}' linter or turn it off in setings.json via "python.linting.${this.Id}Enabled = false".`; - this.installer.promptToInstall(this.product); - } + private parseLine(line: string, regEx: string) { + const match = matchNamedRegEx(line, regEx); + if (!match) { + return; } - else { - vscode.window.showErrorMessage(`There was an error in running the linter '${this.Id}'`, 'Disable linter', 'View Errors').then(item => { - switch (item) { - case 'Disable linter': { - disableLinter(this.product); - break; - } - case 'View Errors': { - this.outputChannel.show(); - break; - } + + // tslint:disable-next-line:no-any + match.line = Number(match.line); + // tslint:disable-next-line:no-any + match.column = Number(match.column); + + return { + code: match.code, + message: match.message, + column: isNaN(match.column) || match.column === 0 ? 0 : match.column - this._columnOffset, + line: match.line, + type: match.type, + provider: this.Id + }; + } + private parseLines(outputLines: string[], regEx: string) { + const diagnostics: ILintMessage[] = []; + outputLines.filter((value, index) => index <= this.pythonSettings.linting.maxNumberOfProblems).forEach(line => { + try { + const msg = this.parseLine(line, regEx); + if (msg) { + diagnostics.push(msg); } - }); - } - this.outputChannel.appendLine(`\n${customError}\n${error + ''}`); + } catch (ex) { + // Hmm, need to handle this later + // TODO: + } + }); + return diagnostics; + } + private displayLinterResultHeader(data: string) { + this.outputChannel.append(`${'#'.repeat(10)}Linting Output - ${this.Id}${'#'.repeat(10)}\n`); + this.outputChannel.append(data); } } diff --git a/src/client/linters/errorHandlers/invalidArgs.ts b/src/client/linters/errorHandlers/invalidArgs.ts new file mode 100644 index 000000000000..b2d4b155cadb --- /dev/null +++ b/src/client/linters/errorHandlers/invalidArgs.ts @@ -0,0 +1,39 @@ +'use strict'; +import { isNotInstalledError } from '../../common/helpers'; +import { Uri, window } from 'vscode'; +import { StandardErrorHandler } from './standard'; + +export class InvalidArgumentsErrorHandler extends StandardErrorHandler { + private hasInvalidArgs(expectedFileName: string, fileName: string): boolean { + // Check if we have some custom arguments such as "pylint --load-plugins pylint_django" + // Such settings are no longer supported + let stuffAfterFileName = fileName.substring(fileName.toUpperCase().lastIndexOf(expectedFileName) + expectedFileName.length); + return stuffAfterFileName.trim().indexOf(' ') > 0; + } + + private displayInvalidArgsError() { + // Ok if we have a space after the file name, this means we have some arguments defined and this isn't supported + window.showErrorMessage(`Unsupported configuration for '${this.id}'`, 'View Errors').then(item => { + if (item === 'View Errors') { + this.outputChannel.show(); + } + }); + } + + public handleError(expectedFileName: string, fileName: string, error: Error, resource?: Uri): boolean { + if (!isNotInstalledError(error)) { + return false; + } + if (!this.hasInvalidArgs(expectedFileName, fileName)) { + return false; + } + + const customError = `Linting failed, custom arguments in the 'python.linting.${this.id}Path' is not supported.\n` + + `Custom arguments to the linters can be defined in 'python.linting.${this.id}Args' setting of settings.json.\n` + + 'For further details, please see https://github.com/DonJayamanne/pythonVSCode/wiki/Troubleshooting-Linting#2-linting-with-xxx-failed-'; + + this.outputChannel.appendLine(`\n${customError}\n${error + ''}`); + this.displayInvalidArgsError(); + return true; + } +} diff --git a/src/client/linters/errorHandlers/main.ts b/src/client/linters/errorHandlers/main.ts new file mode 100644 index 000000000000..e8b9c4d82b35 --- /dev/null +++ b/src/client/linters/errorHandlers/main.ts @@ -0,0 +1,22 @@ +'use strict'; +import { OutputChannel, Uri } from 'vscode'; +import { Installer, Product } from '../../common/installer'; +import { InvalidArgumentsErrorHandler } from './invalidArgs'; +import { NotInstalledErrorHandler } from './notInstalled'; +import { StandardErrorHandler } from './standard'; + +export class ErrorHandler { + // tslint:disable-next-line:variable-name + private _errorHandlers: StandardErrorHandler[] = []; + constructor(protected id: string, protected product: Product, protected installer: Installer, protected outputChannel: OutputChannel) { + this._errorHandlers = [ + new InvalidArgumentsErrorHandler(this.id, this.product, this.installer, this.outputChannel), + new NotInstalledErrorHandler(this.id, this.product, this.installer, this.outputChannel), + new StandardErrorHandler(this.id, this.product, this.installer, this.outputChannel) + ]; + } + + public handleError(expectedFileName: string, fileName: string, error: Error, resource: Uri) { + this._errorHandlers.some(handler => handler.handleError(expectedFileName, fileName, error, resource)); + } +} diff --git a/src/client/linters/errorHandlers/notInstalled.ts b/src/client/linters/errorHandlers/notInstalled.ts new file mode 100644 index 000000000000..f875997b184f --- /dev/null +++ b/src/client/linters/errorHandlers/notInstalled.ts @@ -0,0 +1,17 @@ +'use strict'; +import { Uri } from 'vscode'; +import { isNotInstalledError } from '../../common/helpers'; +import { StandardErrorHandler } from './standard'; + +export class NotInstalledErrorHandler extends StandardErrorHandler { + public handleError(expectedFileName: string, fileName: string, error: Error, resource?: Uri): boolean { + if (!isNotInstalledError(error)) { + return false; + } + + this.installer.promptToInstall(this.product, resource); + const customError = `Linting with ${this.id} failed.\nYou could either install the '${this.id}' linter or turn it off in setings.json via "python.linting.${this.id}Enabled = false".`; + this.outputChannel.appendLine(`\n${customError}\n${error + ''}`); + return true; + } +} diff --git a/src/client/linters/errorHandlers/standard.ts b/src/client/linters/errorHandlers/standard.ts new file mode 100644 index 000000000000..966f8afa41a9 --- /dev/null +++ b/src/client/linters/errorHandlers/standard.ts @@ -0,0 +1,36 @@ +'use strict'; +import { OutputChannel, Uri, window } from 'vscode'; +import { Installer, Product } from '../../common/installer'; + +export class StandardErrorHandler { + constructor(protected id: string, protected product: Product, protected installer: Installer, protected outputChannel: OutputChannel) { + + } + private displayLinterError(resource: Uri) { + const message = `There was an error in running the linter '${this.id}'`; + window.showErrorMessage(message, 'Disable linter', 'View Errors').then(item => { + switch (item) { + case 'Disable linter': { + this.installer.disableLinter(this.product, resource); + break; + } + case 'View Errors': { + this.outputChannel.show(); + break; + } + } + }); + } + + public handleError(expectedFileName: string, fileName: string, error: Error, resource: Uri): boolean { + if (typeof error === 'string' && (error as string).indexOf("OSError: [Errno 2] No such file or directory: '/") > 0) { + return false; + } + console.error('There was an error in running the linter'); + console.error(error); + + this.outputChannel.appendLine(`Linting with ${this.id} failed.\n${error + ''}`); + this.displayLinterError(resource); + return true; + } +} diff --git a/src/client/linters/flake8.ts b/src/client/linters/flake8.ts index bd2e226ae619..00bca18c28d4 100644 --- a/src/client/linters/flake8.ts +++ b/src/client/linters/flake8.ts @@ -2,29 +2,33 @@ import * as baseLinter from './baseLinter'; import { OutputChannel } from 'vscode'; -import { Product } from '../common/installer'; -import { TextDocument } from 'vscode'; +import { Product, ProductExecutableAndArgs } from '../common/installer'; +import { TextDocument, CancellationToken } from 'vscode'; export class Linter extends baseLinter.BaseLinter { - constructor(outputChannel: OutputChannel, workspaceRootPath?: string) { - super('flake8', Product.flake8, outputChannel, workspaceRootPath); - } + _columnOffset = 1; - public isEnabled(): Boolean { - return this.pythonSettings.linting.flake8Enabled; + constructor(outputChannel: OutputChannel) { + super('flake8', Product.flake8, outputChannel); } - public runLinter(document: TextDocument): Promise { + + protected runLinter(document: TextDocument, cancellation: CancellationToken): Promise { if (!this.pythonSettings.linting.flake8Enabled) { return Promise.resolve([]); } let flake8Path = this.pythonSettings.linting.flake8Path; let flake8Args = Array.isArray(this.pythonSettings.linting.flake8Args) ? this.pythonSettings.linting.flake8Args : []; + + if (flake8Args.length === 0 && ProductExecutableAndArgs.has(Product.flake8) && flake8Path.toLocaleLowerCase() === 'flake8') { + flake8Path = ProductExecutableAndArgs.get(Product.flake8).executable; + flake8Args = ProductExecutableAndArgs.get(Product.flake8).args; + } + return new Promise((resolve, reject) => { - this.run(flake8Path, flake8Args.concat(['--format=%(row)d,%(col)d,%(code)s,%(code)s:%(text)s', document.uri.fsPath]), document, this.workspaceRootPath).then(messages => { - // All messages in pep8 are treated as warnings for now + this.run(flake8Path, flake8Args.concat(['--format=%(row)d,%(col)d,%(code).1s,%(code)s:%(text)s', document.uri.fsPath]), document, this.getWorkspaceRootPath(document), cancellation).then(messages => { messages.forEach(msg => { - msg.severity = baseLinter.LintMessageSeverity.Information; + msg.severity = this.parseMessagesSeverity(msg.type, this.pythonSettings.linting.flake8CategorySeverity); }); resolve(messages); diff --git a/src/client/linters/main.ts b/src/client/linters/main.ts new file mode 100644 index 000000000000..a54d75fc7df9 --- /dev/null +++ b/src/client/linters/main.ts @@ -0,0 +1,43 @@ +import { OutputChannel } from 'vscode'; +import { workspace } from 'vscode'; + +import { Product } from '../common/installer'; +import { BaseLinter } from './baseLinter'; +import * as prospector from './../linters/prospector'; +import * as pylint from './../linters/pylint'; +import * as pep8 from './../linters/pep8Linter'; +import * as pylama from './../linters/pylama'; +import * as flake8 from './../linters/flake8'; +import * as pydocstyle from './../linters/pydocstyle'; +import * as mypy from './../linters/mypy'; + +export class LinterFactor { + public static createLinter(product: Product, outputChannel: OutputChannel): BaseLinter { + switch (product) { + case Product.flake8: { + return new flake8.Linter(outputChannel); + } + case Product.mypy: { + return new mypy.Linter(outputChannel); + } + case Product.pep8: { + return new pep8.Linter(outputChannel); + } + case Product.prospector: { + return new prospector.Linter(outputChannel); + } + case Product.pydocstyle: { + return new pydocstyle.Linter(outputChannel); + } + case Product.pylama: { + return new pylama.Linter(outputChannel); + } + case Product.pylint: { + return new pylint.Linter(outputChannel); + } + default: { + throw new Error(`Invalid Linter '${Product[product]}''`); + } + } + } +} diff --git a/src/client/linters/mypy.ts b/src/client/linters/mypy.ts index e897d79c6b81..8696d4db5a78 100644 --- a/src/client/linters/mypy.ts +++ b/src/client/linters/mypy.ts @@ -2,43 +2,33 @@ import * as baseLinter from './baseLinter'; import { OutputChannel } from 'vscode'; -import { Product } from '../common/installer'; -import { TextDocument } from 'vscode'; +import { Product, ProductExecutableAndArgs } from '../common/installer'; +import { TextDocument, CancellationToken } from 'vscode'; const REGEX = '(?.py):(?\\d+): (?\\w+): (?.*)\\r?(\\n|$)'; export class Linter extends baseLinter.BaseLinter { - constructor(outputChannel: OutputChannel, workspaceRootPath?: string) { - super('mypy', Product.mypy, outputChannel, workspaceRootPath); - } - private parseMessagesSeverity(category: string): baseLinter.LintMessageSeverity { - switch (category) { - case 'error': { - return baseLinter.LintMessageSeverity.Error; - } - case 'note': { - return baseLinter.LintMessageSeverity.Hint; - } - default: { - return baseLinter.LintMessageSeverity.Information; - } - } + constructor(outputChannel: OutputChannel) { + super('mypy', Product.mypy, outputChannel); } - public isEnabled(): Boolean { - return this.pythonSettings.linting.mypyEnabled; - } - public runLinter(document: TextDocument): Promise { + protected runLinter(document: TextDocument, cancellation: CancellationToken): Promise { if (!this.pythonSettings.linting.mypyEnabled) { return Promise.resolve([]); } let mypyPath = this.pythonSettings.linting.mypyPath; let mypyArgs = Array.isArray(this.pythonSettings.linting.mypyArgs) ? this.pythonSettings.linting.mypyArgs : []; + + if (mypyArgs.length === 0 && ProductExecutableAndArgs.has(Product.mypy) && mypyPath.toLocaleLowerCase() === 'mypy'){ + mypyPath = ProductExecutableAndArgs.get(Product.mypy).executable; + mypyArgs = ProductExecutableAndArgs.get(Product.mypy).args; + } + return new Promise((resolve, reject) => { - this.run(mypyPath, mypyArgs.concat([document.uri.fsPath]), document, this.workspaceRootPath, REGEX).then(messages => { + this.run(mypyPath, mypyArgs.concat([document.uri.fsPath]), document, this.getWorkspaceRootPath(document), cancellation, REGEX).then(messages => { messages.forEach(msg => { - msg.severity = this.parseMessagesSeverity(msg.type); + msg.severity = this.parseMessagesSeverity(msg.type, this.pythonSettings.linting.mypyCategorySeverity); msg.code = msg.type; }); @@ -46,4 +36,4 @@ export class Linter extends baseLinter.BaseLinter { }, reject); }); } -} \ No newline at end of file +} diff --git a/src/client/linters/pep8Linter.ts b/src/client/linters/pep8Linter.ts index b9e332fe9da5..18f77e4bce54 100644 --- a/src/client/linters/pep8Linter.ts +++ b/src/client/linters/pep8Linter.ts @@ -1,30 +1,34 @@ 'use strict'; import * as baseLinter from './baseLinter'; -import {OutputChannel} from 'vscode'; -import { Product } from '../common/installer'; -import { TextDocument } from 'vscode'; +import { OutputChannel } from 'vscode'; +import { Product, ProductExecutableAndArgs } from '../common/installer'; +import { TextDocument, CancellationToken } from 'vscode'; export class Linter extends baseLinter.BaseLinter { - constructor(outputChannel: OutputChannel, workspaceRootPath?: string) { - super('pep8', Product.pep8, outputChannel, workspaceRootPath); - } + _columnOffset = 1; - public isEnabled(): Boolean { - return this.pythonSettings.linting.pep8Enabled; + constructor(outputChannel: OutputChannel) { + super('pep8', Product.pep8, outputChannel); } - public runLinter(document:TextDocument): Promise { + + protected runLinter(document: TextDocument, cancellation: CancellationToken): Promise { if (!this.pythonSettings.linting.pep8Enabled) { return Promise.resolve([]); } let pep8Path = this.pythonSettings.linting.pep8Path; let pep8Args = Array.isArray(this.pythonSettings.linting.pep8Args) ? this.pythonSettings.linting.pep8Args : []; + + if (pep8Args.length === 0 && ProductExecutableAndArgs.has(Product.pep8) && pep8Path.toLocaleLowerCase() === 'pep8') { + pep8Path = ProductExecutableAndArgs.get(Product.pep8).executable; + pep8Args = ProductExecutableAndArgs.get(Product.pep8).args; + } + return new Promise(resolve => { - this.run(pep8Path, pep8Args.concat(['--format=%(row)d,%(col)d,%(code)s,%(code)s:%(text)s', document.uri.fsPath]), document, this.workspaceRootPath).then(messages => { - // All messages in pep8 are treated as warnings for now + this.run(pep8Path, pep8Args.concat(['--format=%(row)d,%(col)d,%(code).1s,%(code)s:%(text)s', document.uri.fsPath]), document, this.getWorkspaceRootPath(document), cancellation).then(messages => { messages.forEach(msg => { - msg.severity = baseLinter.LintMessageSeverity.Information; + msg.severity = this.parseMessagesSeverity(msg.type, this.pythonSettings.linting.pep8CategorySeverity); }); resolve(messages); diff --git a/src/client/linters/prospector.ts b/src/client/linters/prospector.ts index 0e4b7c783dab..2fb44b7d9449 100644 --- a/src/client/linters/prospector.ts +++ b/src/client/linters/prospector.ts @@ -1,10 +1,10 @@ 'use strict'; import * as baseLinter from './baseLinter'; -import {OutputChannel} from 'vscode'; -import {execPythonFile} from './../common/utils'; -import { Product } from '../common/installer'; -import { TextDocument } from 'vscode'; +import { OutputChannel } from 'vscode'; +import { execPythonFile } from './../common/utils'; +import { Product, ProductExecutableAndArgs } from '../common/installer'; +import { TextDocument, CancellationToken } from 'vscode'; interface IProspectorResponse { messages: IProspectorMessage[]; @@ -24,14 +24,11 @@ interface IProspectorLocation { } export class Linter extends baseLinter.BaseLinter { - constructor(outputChannel: OutputChannel, workspaceRootPath?: string) { - super('prospector', Product.prospector, outputChannel, workspaceRootPath); + constructor(outputChannel: OutputChannel) { + super('prospector', Product.prospector, outputChannel); } - public isEnabled(): Boolean { - return this.pythonSettings.linting.prospectorEnabled; - } - public runLinter(document:TextDocument): Promise { + protected runLinter(document: TextDocument, cancellation: CancellationToken): Promise { if (!this.pythonSettings.linting.prospectorEnabled) { return Promise.resolve([]); } @@ -39,8 +36,14 @@ export class Linter extends baseLinter.BaseLinter { let prospectorPath = this.pythonSettings.linting.prospectorPath; let outputChannel = this.outputChannel; let prospectorArgs = Array.isArray(this.pythonSettings.linting.prospectorArgs) ? this.pythonSettings.linting.prospectorArgs : []; + + if (prospectorArgs.length === 0 && ProductExecutableAndArgs.has(Product.prospector) && prospectorPath.toLocaleLowerCase() === 'prospector') { + prospectorPath = ProductExecutableAndArgs.get(Product.prospector).executable; + prospectorArgs = ProductExecutableAndArgs.get(Product.prospector).args; + } + return new Promise((resolve, reject) => { - execPythonFile(prospectorPath, prospectorArgs.concat(['--absolute-paths', '--output-format=json', document.uri.fsPath]), this.workspaceRootPath, false).then(data => { + execPythonFile(document.uri, prospectorPath, prospectorArgs.concat(['--absolute-paths', '--output-format=json', document.uri.fsPath]), this.getWorkspaceRootPath(document), false, null, cancellation).then(data => { let parsedData: IProspectorResponse; try { parsedData = JSON.parse(data); @@ -53,23 +56,13 @@ export class Linter extends baseLinter.BaseLinter { let diagnostics: baseLinter.ILintMessage[] = []; parsedData.messages.filter((value, index) => index <= this.pythonSettings.linting.maxNumberOfProblems).forEach(msg => { - let lineNumber = msg.location.line === null || isNaN(msg.location.line) ? 1 : msg.location.line; - let sourceLine = document.lineAt(lineNumber - 1).text; - let sourceStart = sourceLine.substring(msg.location.character); - - // try to get the first word from the starting position - let possibleProblemWords = sourceStart.match(/\w+/g); - let possibleWord: string; - if (possibleProblemWords != null && possibleProblemWords.length > 0 && sourceStart.startsWith(possibleProblemWords[0])) { - possibleWord = possibleProblemWords[0]; - } + let lineNumber = msg.location.line === null || isNaN(msg.location.line) ? 1 : msg.location.line; diagnostics.push({ code: msg.code, message: msg.message, column: msg.location.character, line: lineNumber, - possibleWord: possibleWord, type: msg.code, provider: `${this.Id} - ${msg.source}` }); @@ -77,7 +70,7 @@ export class Linter extends baseLinter.BaseLinter { resolve(diagnostics); }).catch(error => { - this.handleError(this.Id, prospectorPath, error); + this.handleError(this.Id, prospectorPath, error, document.uri); resolve([]); }); }); diff --git a/src/client/linters/pydocstyle.ts b/src/client/linters/pydocstyle.ts index 7a281cb5426a..81f2cdbd029f 100644 --- a/src/client/linters/pydocstyle.ts +++ b/src/client/linters/pydocstyle.ts @@ -5,26 +5,29 @@ import * as baseLinter from './baseLinter'; import { ILintMessage } from './baseLinter'; import { OutputChannel } from 'vscode'; import { execPythonFile, IS_WINDOWS } from './../common/utils'; -import { Product } from '../common/installer'; -import { TextDocument } from 'vscode'; +import { Product, ProductExecutableAndArgs } from '../common/installer'; +import { TextDocument, CancellationToken } from 'vscode'; export class Linter extends baseLinter.BaseLinter { - constructor(outputChannel: OutputChannel, workspaceRootPath?: string) { - super('pydocstyle', Product.pydocstyle, outputChannel, workspaceRootPath); + constructor(outputChannel: OutputChannel) { + super('pydocstyle', Product.pydocstyle, outputChannel); } - public isEnabled(): Boolean { - return this.pythonSettings.linting.pydocstyleEnabled; - } - public runLinter(document:TextDocument): Promise { + protected runLinter(document: TextDocument, cancellation: CancellationToken): Promise { if (!this.pythonSettings.linting.pydocstyleEnabled) { return Promise.resolve([]); } let pydocstylePath = this.pythonSettings.linting.pydocstylePath; let pydocstyleArgs = Array.isArray(this.pythonSettings.linting.pydocstyleArgs) ? this.pythonSettings.linting.pydocstyleArgs : []; + + if (pydocstyleArgs.length === 0 && ProductExecutableAndArgs.has(Product.pydocstyle) && pydocstylePath.toLocaleLowerCase() === 'pydocstyle') { + pydocstylePath = ProductExecutableAndArgs.get(Product.pydocstyle).executable; + pydocstyleArgs = ProductExecutableAndArgs.get(Product.pydocstyle).args; + } + return new Promise(resolve => { - this.run(pydocstylePath, pydocstyleArgs.concat([document.uri.fsPath]), document).then(messages => { + this.run(pydocstylePath, pydocstyleArgs.concat([document.uri.fsPath]), document, null, cancellation).then(messages => { // All messages in pep8 are treated as warnings for now messages.forEach(msg => { msg.severity = baseLinter.LintMessageSeverity.Information; @@ -35,11 +38,11 @@ export class Linter extends baseLinter.BaseLinter { }); } - protected run(commandLine: string, args: string[], document:TextDocument): Promise { + protected run(commandLine: string, args: string[], document: TextDocument, cwd: any, cancellation: CancellationToken): Promise { let outputChannel = this.outputChannel; return new Promise((resolve, reject) => { - execPythonFile(commandLine, args, this.workspaceRootPath, true).then(data => { + execPythonFile(document.uri, commandLine, args, this.getWorkspaceRootPath(document), true, null, cancellation).then(data => { outputChannel.append('#'.repeat(10) + 'Linting Output - ' + this.Id + '#'.repeat(10) + '\n'); outputChannel.append(data); let outputLines = data.split(/\r?\n/g); @@ -96,7 +99,7 @@ export class Linter extends baseLinter.BaseLinter { }); resolve(diagnostics); }, error => { - this.handleError(this.Id, commandLine, error); + this.handleError(this.Id, commandLine, error, document.uri); resolve([]); }); }); diff --git a/src/client/linters/pylama.ts b/src/client/linters/pylama.ts index e4e153ac60ef..f0251ee728ea 100644 --- a/src/client/linters/pylama.ts +++ b/src/client/linters/pylama.ts @@ -1,29 +1,34 @@ 'use strict'; import * as baseLinter from './baseLinter'; -import {OutputChannel} from 'vscode'; -import { Product } from '../common/installer'; -import { TextDocument } from 'vscode'; +import { OutputChannel } from 'vscode'; +import { Product, ProductExecutableAndArgs } from '../common/installer'; +import { TextDocument, CancellationToken } from 'vscode'; const REGEX = '(?.py):(?\\d+):(?\\d+): \\[(?\\w+)\\] (?\\w\\d+):? (?.*)\\r?(\\n|$)'; export class Linter extends baseLinter.BaseLinter { - constructor(outputChannel: OutputChannel, workspaceRootPath?: string) { - super('pylama', Product.pylama, outputChannel, workspaceRootPath); - } + _columnOffset = 1; - public isEnabled(): Boolean { - return this.pythonSettings.linting.pylamaEnabled; + constructor(outputChannel: OutputChannel) { + super('pylama', Product.pylama, outputChannel); } - public runLinter(document:TextDocument): Promise { + + protected runLinter(document: TextDocument, cancellation: CancellationToken): Promise { if (!this.pythonSettings.linting.pylamaEnabled) { return Promise.resolve([]); } let pylamaPath = this.pythonSettings.linting.pylamaPath; let pylamaArgs = Array.isArray(this.pythonSettings.linting.pylamaArgs) ? this.pythonSettings.linting.pylamaArgs : []; + + if (pylamaArgs.length === 0 && ProductExecutableAndArgs.has(Product.pylama) && pylamaPath.toLocaleLowerCase() === 'pylama') { + pylamaPath = ProductExecutableAndArgs.get(Product.pylama).executable; + pylamaArgs = ProductExecutableAndArgs.get(Product.pylama).args; + } + return new Promise(resolve => { - this.run(pylamaPath, pylamaArgs.concat(['--format=parsable', document.uri.fsPath]), document, this.workspaceRootPath, REGEX).then(messages => { + this.run(pylamaPath, pylamaArgs.concat(['--format=parsable', document.uri.fsPath]), document, this.getWorkspaceRootPath(document), cancellation, REGEX).then(messages => { // All messages in pylama are treated as warnings for now messages.forEach(msg => { msg.severity = baseLinter.LintMessageSeverity.Information; diff --git a/src/client/linters/pylint.ts b/src/client/linters/pylint.ts index 7e3ed3049d16..5649d079f008 100644 --- a/src/client/linters/pylint.ts +++ b/src/client/linters/pylint.ts @@ -2,55 +2,35 @@ import * as baseLinter from './baseLinter'; import { OutputChannel } from 'vscode'; -import { Product } from '../common/installer'; -import { TextDocument } from 'vscode'; +import { Product, ProductExecutableAndArgs } from '../common/installer'; +import { TextDocument, CancellationToken } from 'vscode'; export class Linter extends baseLinter.BaseLinter { - constructor(outputChannel: OutputChannel, workspaceRootPath?: string) { - super('pylint', Product.pylint, outputChannel, workspaceRootPath); + constructor(outputChannel: OutputChannel) { + super('pylint', Product.pylint, outputChannel); } - private parseMessagesSeverity(category: string): baseLinter.LintMessageSeverity { - if (this.pythonSettings.linting.pylintCategorySeverity[category]) { - let severityName = this.pythonSettings.linting.pylintCategorySeverity[category]; - switch (severityName) { - case 'Error': - return baseLinter.LintMessageSeverity.Error; - case 'Hint': - return baseLinter.LintMessageSeverity.Hint; - case 'Information': - return baseLinter.LintMessageSeverity.Information; - case 'Warning': - return baseLinter.LintMessageSeverity.Warning; - default: { - if (baseLinter.LintMessageSeverity[severityName]) { - return baseLinter.LintMessageSeverity[severityName]; - } - } - } - } - - return baseLinter.LintMessageSeverity.Information; - } - - public isEnabled(): Boolean { - return this.pythonSettings.linting.pylintEnabled; - } - public runLinter(document: TextDocument): Promise { + protected runLinter(document: TextDocument, cancellation: CancellationToken): Promise { if (!this.pythonSettings.linting.pylintEnabled) { return Promise.resolve([]); } let pylintPath = this.pythonSettings.linting.pylintPath; let pylintArgs = Array.isArray(this.pythonSettings.linting.pylintArgs) ? this.pythonSettings.linting.pylintArgs : []; + + if (pylintArgs.length === 0 && ProductExecutableAndArgs.has(Product.pylint) && pylintPath === 'pylint') { + pylintPath = ProductExecutableAndArgs.get(Product.pylint).executable; + pylintArgs = ProductExecutableAndArgs.get(Product.pylint).args; + } + return new Promise((resolve, reject) => { - this.run(pylintPath, pylintArgs.concat(['--msg-template=\'{line},{column},{category},{msg_id}:{msg}\'', '--reports=n', '--output-format=text', document.uri.fsPath]), document, this.workspaceRootPath).then(messages => { + this.run(pylintPath, pylintArgs.concat(['--msg-template=\'{line},{column},{category},{msg_id}:{msg}\'', '--reports=n', '--output-format=text', document.uri.fsPath]), document, this.getWorkspaceRootPath(document), cancellation).then(messages => { messages.forEach(msg => { - msg.severity = this.parseMessagesSeverity(msg.type); + msg.severity = this.parseMessagesSeverity(msg.type, this.pythonSettings.linting.pylintCategorySeverity); }); resolve(messages); }, reject); }); } -} \ No newline at end of file +} diff --git a/src/client/providers/codeActionProvider.ts b/src/client/providers/codeActionProvider.ts deleted file mode 100644 index 14e03c9a4b9a..000000000000 --- a/src/client/providers/codeActionProvider.ts +++ /dev/null @@ -1,40 +0,0 @@ -'use strict'; - -import * as vscode from 'vscode'; -import {TextDocument, Range, CodeActionContext, CancellationToken, Command} from 'vscode'; - -export class PythonCodeActionsProvider implements vscode.CodeActionProvider { - public constructor(context: vscode.ExtensionContext) { - } - provideCodeActions(document: TextDocument, range: Range, context: CodeActionContext, token: CancellationToken): Thenable { - return new Promise((resolve, reject) => { - let commands: Command[] = [ - { - command: 'python.sortImports', - title: 'Sort Imports' - } - ]; - - if (vscode.window.activeTextEditor.document === document && !vscode.window.activeTextEditor.selection.isEmpty) { - let wordRange = document.getWordRangeAtPosition(range.start); - // If no word has been selected by the user, then don't display rename - // If something has been selected, then ensure we have selected a word (i.e. end and start matches the word range) - if (wordRange && !wordRange.isEmpty && wordRange.isEqual(vscode.window.activeTextEditor.selection)) { - let word = document.getText(wordRange).trim(); - if (word.length > 0) { - commands.push({ command: 'editor.action.rename', title: 'Rename Symbol' }); - } - } - } - - if (!range.isEmpty) { - let word = document.getText(range).trim(); - if (word.trim().length > 0) { - commands.push({ command: 'python.refactorExtractVariable', title: 'Extract Variable', arguments: [range] }); - commands.push({ command: 'python.refactorExtractMethod', title: 'Extract Method', arguments: [range] }); - } - } - resolve(commands); - }); - } -} diff --git a/src/client/providers/completionProvider.ts b/src/client/providers/completionProvider.ts index 5ae5e000301d..b453d19a7f19 100644 --- a/src/client/providers/completionProvider.ts +++ b/src/client/providers/completionProvider.ts @@ -1,31 +1,28 @@ 'use strict'; import * as vscode from 'vscode'; -import * as proxy from './jediProxy'; -import * as telemetryContracts from '../common/telemetryContracts'; -import { extractSignatureAndDocumentation } from './jediHelpers'; -import { EOL } from 'os'; +import { ProviderResult, SnippetString, Uri } from 'vscode'; import { PythonSettings } from '../common/configSettings'; - -const pythonSettings = PythonSettings.getInstance(); +import { JediFactory } from '../languageServices/jediProxyFactory'; +import { captureTelemetry } from '../telemetry'; +import { COMPLETION } from '../telemetry/constants'; +import { extractSignatureAndDocumentation } from './jediHelpers'; +import * as proxy from './jediProxy'; export class PythonCompletionItemProvider implements vscode.CompletionItemProvider { - private jediProxyHandler: proxy.JediProxyHandler; - public constructor(context: vscode.ExtensionContext, jediProxy: proxy.JediProxy = null) { - this.jediProxyHandler = new proxy.JediProxyHandler(context, jediProxy); - } - private static parseData(data: proxy.ICompletionResult): vscode.CompletionItem[] { + public constructor(private jediFactory: JediFactory) { } + private static parseData(data: proxy.ICompletionResult, resource: Uri): vscode.CompletionItem[] { if (data && data.items.length > 0) { return data.items.map(item => { const sigAndDocs = extractSignatureAndDocumentation(item); - let completionItem = new vscode.CompletionItem(item.text); + const completionItem = new vscode.CompletionItem(item.text); completionItem.kind = item.type; completionItem.documentation = sigAndDocs[1].length === 0 ? item.description : sigAndDocs[1]; - completionItem.detail = sigAndDocs[0].split(EOL).join(''); - if (pythonSettings.autoComplete.addBrackets === true && + completionItem.detail = sigAndDocs[0].split(/\r?\n/).join(''); + if (PythonSettings.getInstance(resource).autoComplete.addBrackets === true && (item.kind === vscode.SymbolKind.Function || item.kind === vscode.SymbolKind.Method)) { - completionItem.insertText = item.text + '({{}})'; + completionItem.insertText = new SnippetString(item.text).appendText('(').appendTabstop().appendText(')'); } // ensure the built in memebers are at the bottom @@ -35,15 +32,24 @@ export class PythonCompletionItemProvider implements vscode.CompletionItemProvid } return []; } - public provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Thenable { + @captureTelemetry(COMPLETION) + public provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): ProviderResult { + if (position.character <= 0) { + return Promise.resolve([]); + } const filename = document.fileName; - if (document.lineAt(position.line).text.match(/^\s*\/\//)) { + const lineText = document.lineAt(position.line).text; + if (lineText.match(/^\s*\/\//)) { return Promise.resolve([]); } - if (position.character <= 0) { + // If starts with a comment, then return + if (lineText.trim().startsWith('#')) { + return Promise.resolve([]); + } + // If starts with a """ (possible doc string), then return + if (lineText.trim().startsWith('"""')) { return Promise.resolve([]); } - const type = proxy.CommandType.Completions; const columnIndex = position.character; @@ -56,8 +62,8 @@ export class PythonCompletionItemProvider implements vscode.CompletionItemProvid source: source }; - return this.jediProxyHandler.sendCommand(cmd, token).then(data => { - return PythonCompletionItemProvider.parseData(data); + return this.jediFactory.getJediProxyHandler(document.uri).sendCommand(cmd, token).then(data => { + return PythonCompletionItemProvider.parseData(data, document.uri); }); } -} \ No newline at end of file +} diff --git a/src/client/providers/definitionProvider.ts b/src/client/providers/definitionProvider.ts index 739de9cb49e0..9274ef5900a1 100644 --- a/src/client/providers/definitionProvider.ts +++ b/src/client/providers/definitionProvider.ts @@ -1,39 +1,38 @@ 'use strict'; import * as vscode from 'vscode'; +import { JediFactory } from '../languageServices/jediProxyFactory'; +import { captureTelemetry } from '../telemetry'; +import { DEFINITION } from '../telemetry/constants'; import * as proxy from './jediProxy'; -import * as telemetryContracts from "../common/telemetryContracts"; export class PythonDefinitionProvider implements vscode.DefinitionProvider { - private jediProxyHandler: proxy.JediProxyHandler; - public get JediProxy(): proxy.JediProxy { - return this.jediProxyHandler.JediProxy; - } - - public constructor(context: vscode.ExtensionContext) { - this.jediProxyHandler = new proxy.JediProxyHandler(context); - } - private static parseData(data: proxy.IDefinitionResult): vscode.Definition { - if (data && data.definition) { - var definitionResource = vscode.Uri.file(data.definition.fileName); - var range = new vscode.Range(data.definition.lineIndex, data.definition.columnIndex, data.definition.lineIndex, data.definition.columnIndex); - + public constructor(private jediFactory: JediFactory) { } + private static parseData(data: proxy.IDefinitionResult, possibleWord: string): vscode.Definition { + if (data && Array.isArray(data.definitions) && data.definitions.length > 0) { + const definitions = data.definitions.filter(d => d.text === possibleWord); + const definition = definitions.length > 0 ? definitions[0] : data.definitions[data.definitions.length - 1]; + const definitionResource = vscode.Uri.file(definition.fileName); + const range = new vscode.Range( + definition.range.startLine, definition.range.startColumn, + definition.range.endLine, definition.range.endColumn); return new vscode.Location(definitionResource, range); } return null; } + @captureTelemetry(DEFINITION) public provideDefinition(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Thenable { - var filename = document.fileName; + const filename = document.fileName; if (document.lineAt(position.line).text.match(/^\s*\/\//)) { - return Promise.resolve(); + return Promise.resolve(null); } if (position.character <= 0) { - return Promise.resolve(); + return Promise.resolve(null); } - var range = document.getWordRangeAtPosition(position); - var columnIndex = range.isEmpty ? position.character : range.end.character; - var cmd: proxy.ICommand = { + const range = document.getWordRangeAtPosition(position); + const columnIndex = range.isEmpty ? position.character : range.end.character; + const cmd: proxy.ICommand = { command: proxy.CommandType.Definitions, fileName: filename, columnIndex: columnIndex, @@ -42,8 +41,9 @@ export class PythonDefinitionProvider implements vscode.DefinitionProvider { if (document.isDirty) { cmd.source = document.getText(); } - return this.jediProxyHandler.sendCommand(cmd, token).then(data => { - return PythonDefinitionProvider.parseData(data); + const possibleWord = document.getText(range); + return this.jediFactory.getJediProxyHandler(document.uri).sendCommand(cmd, token).then(data => { + return PythonDefinitionProvider.parseData(data, possibleWord); }); } } diff --git a/src/client/providers/execInTerminalProvider.ts b/src/client/providers/execInTerminalProvider.ts index 50c1bb49bcfe..73045bc32904 100644 --- a/src/client/providers/execInTerminalProvider.ts +++ b/src/client/providers/execInTerminalProvider.ts @@ -1,29 +1,54 @@ 'use strict'; +import * as fs from 'fs-extra'; +import { EOL } from 'os'; +import * as path from 'path'; import * as vscode from 'vscode'; +import { Disposable, workspace } from 'vscode'; import * as settings from '../common/configSettings'; import { Commands, PythonLanguage } from '../common/constants'; -let path = require('path'); -let terminal: vscode.Terminal; +import { ContextKey } from '../common/contextKey'; import { IS_WINDOWS } from '../common/utils'; +import { sendTelemetryEvent } from '../telemetry'; +import { EXECUTION_CODE, EXECUTION_DJANGO } from '../telemetry/constants'; +let terminal: vscode.Terminal; export function activateExecInTerminalProvider(): vscode.Disposable[] { const disposables: vscode.Disposable[] = []; disposables.push(vscode.commands.registerCommand(Commands.Exec_In_Terminal, execInTerminal)); disposables.push(vscode.commands.registerCommand(Commands.Exec_Selection_In_Terminal, execSelectionInTerminal)); + disposables.push(vscode.commands.registerCommand(Commands.Exec_Selection_In_Django_Shell, execSelectionInDjangoShell)); disposables.push(vscode.window.onDidCloseTerminal((closedTermina: vscode.Terminal) => { if (terminal === closedTermina) { terminal = null; } })); + disposables.push(new DjangoContextInitializer()); return disposables; } +function removeBlankLines(code: string): string { + const codeLines = code.split(/\r?\n/g); + const codeLinesWithoutEmptyLines = codeLines.filter(line => line.trim().length > 0); + const lastLineIsEmpty = codeLines.length > 0 && codeLines[codeLines.length - 1].trim().length === 0; + if (lastLineIsEmpty) { + codeLinesWithoutEmptyLines.unshift(''); + } + return codeLinesWithoutEmptyLines.join(EOL); +} function execInTerminal(fileUri?: vscode.Uri) { - let pythonSettings = settings.PythonSettings.getInstance(); - const currentPythonPath = pythonSettings.pythonPath; + const terminalShellSettings = vscode.workspace.getConfiguration('terminal.integrated.shell'); + // tslint:disable-next-line:no-backbone-get-set-outside-model + const IS_POWERSHELL = /powershell/.test(terminalShellSettings.get('windows')); + + const pythonSettings = settings.PythonSettings.getInstance(fileUri); let filePath: string; - if (fileUri === undefined || typeof fileUri.fsPath !== 'string') { + let currentPythonPath = pythonSettings.pythonPath; + if (currentPythonPath.indexOf(' ') > 0) { + currentPythonPath = `"${currentPythonPath}"`; + } + + if (fileUri === undefined || fileUri === null || typeof fileUri.fsPath !== 'string') { const activeEditor = vscode.window.activeTextEditor; if (activeEditor !== undefined) { if (!activeEditor.document.isUntitled) { @@ -48,48 +73,194 @@ function execInTerminal(fileUri?: vscode.Uri) { if (filePath.indexOf(' ') > 0) { filePath = `"${filePath}"`; } - terminal = terminal ? terminal : vscode.window.createTerminal(`Python`); + terminal = terminal ? terminal : vscode.window.createTerminal('Python'); if (pythonSettings.terminal && pythonSettings.terminal.executeInFileDir) { const fileDirPath = path.dirname(filePath); - if (fileDirPath !== vscode.workspace.rootPath && fileDirPath.substring(1) !== vscode.workspace.rootPath) { + const wkspace = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(filePath)); + if (wkspace && fileDirPath !== wkspace.uri.fsPath && fileDirPath.substring(1) !== wkspace.uri.fsPath) { terminal.sendText(`cd "${fileDirPath}"`); } } - const launchArgs = settings.PythonSettings.getInstance().terminal.launchArgs; - const launchArgsString = launchArgs.length > 0 ? " ".concat(launchArgs.join(" ")) : ""; + const launchArgs = settings.PythonSettings.getInstance(fileUri).terminal.launchArgs; + const launchArgsString = launchArgs.length > 0 ? ' '.concat(launchArgs.join(' ')) : ''; + const command = `${currentPythonPath}${launchArgsString} ${filePath}`; if (IS_WINDOWS) { - const cmd = `"${currentPythonPath}"${launchArgsString} ${filePath}`; - terminal.sendText(cmd.replace(/\\/g, "/")); - } - else { - terminal.sendText(`${currentPythonPath}${launchArgsString} ${filePath}`); + const commandWin = command.replace(/\\/g, '/'); + if (IS_POWERSHELL) { + terminal.sendText(`& ${commandWin}`); + } else { + terminal.sendText(commandWin); + } + } else { + terminal.sendText(command); } terminal.show(); + sendTelemetryEvent(EXECUTION_CODE, undefined, { scope: 'file' }); } function execSelectionInTerminal() { - const currentPythonPath = settings.PythonSettings.getInstance().pythonPath; const activeEditor = vscode.window.activeTextEditor; if (!activeEditor) { return; } + const terminalShellSettings = vscode.workspace.getConfiguration('terminal.integrated.shell'); + // tslint:disable-next-line:no-backbone-get-set-outside-model + const IS_POWERSHELL = /powershell/.test(terminalShellSettings.get('windows')); + + let currentPythonPath = settings.PythonSettings.getInstance(activeEditor.document.uri).pythonPath; + if (currentPythonPath.indexOf(' ') > 0) { + currentPythonPath = `"${currentPythonPath}"`; + } + const selection = vscode.window.activeTextEditor.selection; + let code: string; if (selection.isEmpty) { + code = vscode.window.activeTextEditor.document.lineAt(selection.start.line).text; + } else { + const textRange = new vscode.Range(selection.start, selection.end); + code = vscode.window.activeTextEditor.document.getText(textRange); + } + if (code.length === 0) { return; } - const code = vscode.window.activeTextEditor.document.getText(new vscode.Range(selection.start, selection.end)); - terminal = terminal ? terminal : vscode.window.createTerminal(`Python`); - const launchArgs = settings.PythonSettings.getInstance().terminal.launchArgs; - const launchArgsString = launchArgs.length > 0 ? " ".concat(launchArgs.join(" ")) : ""; + code = removeBlankLines(code); + const launchArgs = settings.PythonSettings.getInstance(activeEditor.document.uri).terminal.launchArgs; + const launchArgsString = launchArgs.length > 0 ? ' '.concat(launchArgs.join(' ')) : ''; + const command = `${currentPythonPath}${launchArgsString}`; + if (!terminal) { + terminal = vscode.window.createTerminal('Python'); + if (IS_WINDOWS) { + const commandWin = command.replace(/\\/g, '/'); + if (IS_POWERSHELL) { + terminal.sendText(`& ${commandWin}`); + } else { + terminal.sendText(commandWin); + } + } else { + terminal.sendText(command); + } + } + // tslint:disable-next-line:variable-name + const unix_code = code.replace(/\r\n/g, '\n'); if (IS_WINDOWS) { - // Multi line commands don't work the same way on windows terminals as it does on other OS - // So just start the Python REPL, then send the commands - terminal.sendText(`"${currentPythonPath}"${launchArgsString}`); - terminal.sendText(code); + terminal.sendText(unix_code.replace(/\n/g, '\r\n')); + } else { + terminal.sendText(unix_code); + } + terminal.show(); + sendTelemetryEvent(EXECUTION_CODE, undefined, { scope: 'selection' }); +} + +function execSelectionInDjangoShell() { + const activeEditor = vscode.window.activeTextEditor; + if (!activeEditor) { + return; + } + + const terminalShellSettings = vscode.workspace.getConfiguration('terminal.integrated.shell'); + // tslint:disable-next-line:no-backbone-get-set-outside-model + const IS_POWERSHELL = /powershell/.test(terminalShellSettings.get('windows')); + + let currentPythonPath = settings.PythonSettings.getInstance(activeEditor.document.uri).pythonPath; + if (currentPythonPath.indexOf(' ') > 0) { + currentPythonPath = `"${currentPythonPath}"`; + } + + const workspaceUri = vscode.workspace.getWorkspaceFolder(activeEditor.document.uri); + const defaultWorkspace = Array.isArray(vscode.workspace.workspaceFolders) && vscode.workspace.workspaceFolders.length > 0 ? vscode.workspace.workspaceFolders[0].uri.fsPath : ''; + const workspaceRoot = workspaceUri ? workspaceUri.uri.fsPath : defaultWorkspace; + const djangoShellCmd = `"${path.join(workspaceRoot, 'manage.py')}" shell`; + const selection = vscode.window.activeTextEditor.selection; + let code: string; + if (selection.isEmpty) { + code = vscode.window.activeTextEditor.document.lineAt(selection.start.line).text; + } else { + const textRange = new vscode.Range(selection.start, selection.end); + code = vscode.window.activeTextEditor.document.getText(textRange); } - else { - terminal.sendText(`${currentPythonPath}${launchArgsString} -c "${code}"`); + if (code.length === 0) { + return; + } + const launchArgs = settings.PythonSettings.getInstance(activeEditor.document.uri).terminal.launchArgs; + const launchArgsString = launchArgs.length > 0 ? ' '.concat(launchArgs.join(' ')) : ''; + const command = `${currentPythonPath}${launchArgsString} ${djangoShellCmd}`; + if (!terminal) { + terminal = vscode.window.createTerminal('Django Shell'); + if (IS_WINDOWS) { + const commandWin = command.replace(/\\/g, '/'); + if (IS_POWERSHELL) { + terminal.sendText(`& ${commandWin}`); + } else { + terminal.sendText(commandWin); + } + } else { + terminal.sendText(command); + } + } + // tslint:disable-next-line:variable-name + const unix_code = code.replace(/\r\n/g, '\n'); + if (IS_WINDOWS) { + terminal.sendText(unix_code.replace(/\n/g, '\r\n')); + } else { + terminal.sendText(unix_code); } terminal.show(); -} \ No newline at end of file + sendTelemetryEvent(EXECUTION_DJANGO); +} + +class DjangoContextInitializer implements vscode.Disposable { + private isDjangoProject: ContextKey; + private monitoringActiveTextEditor: boolean; + private workspaceContextKeyValues = new Map(); + private lastCheckedWorkspace: string; + private disposables: Disposable[] = []; + constructor() { + this.isDjangoProject = new ContextKey('python.isDjangoProject'); + this.ensureState(); + this.disposables.push(vscode.workspace.onDidChangeWorkspaceFolders(() => this.updateContextKeyBasedOnActiveWorkspace())); + } + + public dispose() { + this.isDjangoProject = null; + this.disposables.forEach(disposable => disposable.dispose()); + } + private updateContextKeyBasedOnActiveWorkspace() { + if (this.monitoringActiveTextEditor) { + return; + } + this.monitoringActiveTextEditor = true; + this.disposables.push(vscode.window.onDidChangeActiveTextEditor(() => this.ensureState())); + } + private getActiveWorkspace(): string | undefined { + if (!Array.isArray(workspace.workspaceFolders || workspace.workspaceFolders.length === 0)) { + return undefined; + } + if (workspace.workspaceFolders.length === 1) { + return workspace.workspaceFolders[0].uri.fsPath; + } + const activeEditor = vscode.window.activeTextEditor; + if (!activeEditor) { + return undefined; + } + const workspaceFolder = vscode.workspace.getWorkspaceFolder(activeEditor.document.uri); + return workspaceFolder ? workspaceFolder.uri.fsPath : undefined; + } + private async ensureState(): Promise { + const activeWorkspace = this.getActiveWorkspace(); + if (!activeWorkspace) { + return await this.isDjangoProject.set(false); + } + if (this.lastCheckedWorkspace === activeWorkspace) { + return; + } + if (this.workspaceContextKeyValues.has(activeWorkspace)) { + await this.isDjangoProject.set(this.workspaceContextKeyValues.get(activeWorkspace)); + } else { + const exists = await fs.pathExists(path.join(activeWorkspace, 'manage.py')); + await this.isDjangoProject.set(exists); + this.workspaceContextKeyValues.set(activeWorkspace, exists); + this.lastCheckedWorkspace = activeWorkspace; + } + } +} diff --git a/src/client/providers/formatOnSaveProvider.ts b/src/client/providers/formatOnSaveProvider.ts index abf17ac8248f..1b550f0d6cc4 100644 --- a/src/client/providers/formatOnSaveProvider.ts +++ b/src/client/providers/formatOnSaveProvider.ts @@ -6,32 +6,31 @@ import * as vscode from "vscode"; import { BaseFormatter } from "./../formatters/baseFormatter"; import { YapfFormatter } from "./../formatters/yapfFormatter"; import { AutoPep8Formatter } from "./../formatters/autoPep8Formatter"; -import * as settings from "./../common/configSettings"; -import * as telemetryHelper from "../common/telemetry"; -import * as telemetryContracts from "../common/telemetryContracts"; +import { DummyFormatter } from "./../formatters/dummyFormatter"; +import { PythonSettings } from "./../common/configSettings"; -export function activateFormatOnSaveProvider(languageFilter: vscode.DocumentFilter, settings: settings.IPythonSettings, outputChannel: vscode.OutputChannel, workspaceRootPath?: string): vscode.Disposable { - let formatters = new Map(); - let pythonSettings = settings; - - let yapfFormatter = new YapfFormatter(outputChannel, settings, workspaceRootPath); - let autoPep8 = new AutoPep8Formatter(outputChannel, settings, workspaceRootPath); +export function activateFormatOnSaveProvider(languageFilter: vscode.DocumentFilter, outputChannel: vscode.OutputChannel): vscode.Disposable { + const formatters = new Map(); + const yapfFormatter = new YapfFormatter(outputChannel); + const autoPep8 = new AutoPep8Formatter(outputChannel); + const dummyFormatter = new DummyFormatter(outputChannel); formatters.set(yapfFormatter.Id, yapfFormatter); formatters.set(autoPep8.Id, autoPep8); + formatters.set(dummyFormatter.Id, dummyFormatter); return vscode.workspace.onWillSaveTextDocument(e => { const document = e.document; if (document.languageId !== languageFilter.language) { return; } - let textEditor = vscode.window.activeTextEditor; - let editorConfig = vscode.workspace.getConfiguration('editor'); + const textEditor = vscode.window.activeTextEditor; + const editorConfig = vscode.workspace.getConfiguration('editor'); const globalEditorFormatOnSave = editorConfig && editorConfig.has('formatOnSave') && editorConfig.get('formatOnSave') === true; - if ((pythonSettings.formatting.formatOnSave || globalEditorFormatOnSave) && textEditor.document === document) { - let formatter = formatters.get(pythonSettings.formatting.provider); - telemetryHelper.sendTelemetryEvent(telemetryContracts.IDE.Format, { Format_Provider: formatter.Id, Format_OnSave: "true" }); + const settings = PythonSettings.getInstance(document.uri); + if ((settings.formatting.formatOnSave || globalEditorFormatOnSave) && textEditor.document === document) { + const formatter = formatters.get(settings.formatting.provider); e.waitUntil(formatter.formatDocument(document, null, null)); } }, null, null); -} \ No newline at end of file +} diff --git a/src/client/providers/formatProvider.ts b/src/client/providers/formatProvider.ts index 710fd3424d0c..528e3d8f69d7 100644 --- a/src/client/providers/formatProvider.ts +++ b/src/client/providers/formatProvider.ts @@ -1,22 +1,22 @@ 'use strict'; import * as vscode from 'vscode'; -import * as path from 'path'; -import {BaseFormatter} from './../formatters/baseFormatter'; -import {YapfFormatter} from './../formatters/yapfFormatter'; -import {AutoPep8Formatter} from './../formatters/autoPep8Formatter'; -import * as settings from './../common/configSettings'; -import * as telemetryHelper from '../common/telemetry'; -import * as telemetryContracts from '../common/telemetryContracts'; +import { PythonSettings } from './../common/configSettings'; +import { AutoPep8Formatter } from './../formatters/autoPep8Formatter'; +import { BaseFormatter } from './../formatters/baseFormatter'; +import { DummyFormatter } from './../formatters/dummyFormatter'; +import { YapfFormatter } from './../formatters/yapfFormatter'; export class PythonFormattingEditProvider implements vscode.DocumentFormattingEditProvider, vscode.DocumentRangeFormattingEditProvider { private formatters = new Map(); - public constructor(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel, private settings: settings.IPythonSettings) { - let yapfFormatter = new YapfFormatter(outputChannel, settings); - let autoPep8 = new AutoPep8Formatter(outputChannel, settings); + public constructor(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel) { + const yapfFormatter = new YapfFormatter(outputChannel); + const autoPep8 = new AutoPep8Formatter(outputChannel); + const dummy = new DummyFormatter(outputChannel); this.formatters.set(yapfFormatter.Id, yapfFormatter); this.formatters.set(autoPep8.Id, autoPep8); + this.formatters.set(dummy.Id, dummy); } public provideDocumentFormattingEdits(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken): Thenable { @@ -24,13 +24,9 @@ export class PythonFormattingEditProvider implements vscode.DocumentFormattingEd } public provideDocumentRangeFormattingEdits(document: vscode.TextDocument, range: vscode.Range, options: vscode.FormattingOptions, token: vscode.CancellationToken): Thenable { - let formatter = this.formatters.get(this.settings.formatting.provider); - let delays = new telemetryHelper.Delays(); - return formatter.formatDocument(document, options, token, range).then(edits => { - delays.stop(); - telemetryHelper.sendTelemetryEvent(telemetryContracts.IDE.Format, { Format_Provider: formatter.Id }, delays.toMeasures()); - return edits; - }); + const settings = PythonSettings.getInstance(document.uri); + const formatter = this.formatters.get(settings.formatting.provider); + return formatter.formatDocument(document, options, token, range); } } diff --git a/src/client/providers/hoverProvider.ts b/src/client/providers/hoverProvider.ts index ffec18438207..8c50b336fcfa 100644 --- a/src/client/providers/hoverProvider.ts +++ b/src/client/providers/hoverProvider.ts @@ -1,60 +1,107 @@ 'use strict'; +import { EOL } from 'os'; import * as vscode from 'vscode'; +import { JediFactory } from '../languageServices/jediProxyFactory'; +import { captureTelemetry } from '../telemetry'; +import { HOVER_DEFINITION } from '../telemetry/constants'; +import { highlightCode } from './jediHelpers'; import * as proxy from './jediProxy'; -import * as telemetryContracts from "../common/telemetryContracts"; -import { extractHoverInfo} from './jediHelpers'; export class PythonHoverProvider implements vscode.HoverProvider { - private jediProxyHandler: proxy.JediProxyHandler; - - public constructor(context: vscode.ExtensionContext, jediProxy: proxy.JediProxy = null) { - this.jediProxyHandler = new proxy.JediProxyHandler(context, jediProxy); + public constructor(private jediFactory: JediFactory) { } + private static parseData(data: proxy.IHoverResult, currentWord: string): vscode.Hover { + const results = []; + const capturedInfo: string[] = []; + data.items.forEach(item => { + let { signature } = item; + switch (item.kind) { + case vscode.SymbolKind.Constructor: + case vscode.SymbolKind.Function: + case vscode.SymbolKind.Method: { + signature = `def ${signature}`; + break; + } + case vscode.SymbolKind.Class: { + signature = `class ${signature}`; + break; + } + default: { + signature = typeof item.text === 'string' && item.text.length > 0 ? item.text : currentWord; + } + } + if (item.docstring) { + let lines = item.docstring.split(/\r?\n/); + // If the docstring starts with the signature, then remove those lines from the docstring. + if (lines.length > 0 && item.signature.indexOf(lines[0]) === 0) { + lines.shift(); + const endIndex = lines.findIndex(line => item.signature.endsWith(line)); + if (endIndex >= 0) { + lines = lines.filter((line, index) => index > endIndex); + } + } + if (lines.length > 0 && item.signature.startsWith(currentWord) && lines[0].startsWith(currentWord) && lines[0].endsWith(')')) { + lines.shift(); + } + const descriptionWithHighlightedCode = highlightCode(lines.join(EOL)); + const hoverInfo = ['```python', signature, '```', descriptionWithHighlightedCode].join(EOL); + const key = signature + lines.join(''); + // Sometimes we have duplicate documentation, one with a period at the end. + if (capturedInfo.indexOf(key) >= 0 || capturedInfo.indexOf(`${key}.`) >= 0) { + return; + } + capturedInfo.push(key); + capturedInfo.push(`${key}.`); + results.push(hoverInfo); + return; + } + if (item.description) { + const descriptionWithHighlightedCode = highlightCode(item.description); + // tslint:disable-next-line:prefer-template + const hoverInfo = '```python' + EOL + signature + EOL + '```' + EOL + descriptionWithHighlightedCode; + const lines = item.description.split(EOL); + const key = signature + lines.join(''); + // Sometimes we have duplicate documentation, one with a period at the end. + if (capturedInfo.indexOf(key) >= 0 || capturedInfo.indexOf(`${key}.`) >= 0) { + return; + } + capturedInfo.push(key); + capturedInfo.push(`${key}.`); + results.push(hoverInfo); + } + }); + return new vscode.Hover(results); } - public provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Thenable { - var filename = document.fileName; + @captureTelemetry(HOVER_DEFINITION) + public async provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { + const filename = document.fileName; if (document.lineAt(position.line).text.match(/^\s*\/\//)) { - return Promise.resolve(); + return null; } if (position.character <= 0) { - return Promise.resolve(); + return null; } - var range = document.getWordRangeAtPosition(position); + const range = document.getWordRangeAtPosition(position); if (!range || range.isEmpty) { - return Promise.resolve(); + return null; } - var columnIndex = range.start.character < range.end.character ? range.start.character + 2 : range.end.character; - var cmd: proxy.ICommand = { - command: proxy.CommandType.Completions, + const word = document.getText(range); + const cmd: proxy.ICommand = { + command: proxy.CommandType.Hover, fileName: filename, - columnIndex: columnIndex, + columnIndex: range.end.character, lineIndex: position.line }; if (document.isDirty) { cmd.source = document.getText(); } - return this.jediProxyHandler.sendCommand(cmd, token).then(data => { - if (!data || !Array.isArray(data.items) || data.items.length === 0) { - return; - } - // Find the right items - const wordUnderCursor = document.getText(range); - const completionItem = data.items.filter(item => item.text === wordUnderCursor); - if (completionItem.length === 0) { - return; - } - var definition = completionItem[0]; - var txt = definition.description || definition.text; - if (typeof txt !== 'string' || txt.length === 0) { - return; - } - if (wordUnderCursor === txt) { - return; - } + const data = await this.jediFactory.getJediProxyHandler(document.uri).sendCommand(cmd, token); + if (!data || !data.items.length) { + return; + } - return extractHoverInfo(definition); - }); + return PythonHoverProvider.parseData(data, word); } } diff --git a/src/client/providers/importSortProvider.ts b/src/client/providers/importSortProvider.ts index a72e56a85342..99c8961b672b 100644 --- a/src/client/providers/importSortProvider.ts +++ b/src/client/providers/importSortProvider.ts @@ -1,41 +1,57 @@ -"use strict"; - -import * as vscode from "vscode"; -import * as path from "path"; -import * as fs from "fs"; -import * as child_process from "child_process"; -import * as settings from '../common/configSettings'; -import {getTextEditsFromPatch, getTempFileWithDocumentContents} from "../common/editor"; +'use strict'; +import * as child_process from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { PythonSettings } from '../common/configSettings'; +import { getTempFileWithDocumentContents, getTextEditsFromPatch } from '../common/editor'; +import { captureTelemetry } from '../telemetry'; +import { FORMAT_SORT_IMPORTS } from '../telemetry/constants'; +// tslint:disable-next-line:completed-docs export class PythonImportSortProvider { - public sortImports(extensionDir: string, document: vscode.TextDocument): Promise { + @captureTelemetry(FORMAT_SORT_IMPORTS) + public async sortImports(extensionDir: string, document: vscode.TextDocument): Promise { if (document.lineCount === 1) { - return Promise.resolve([]); + return []; } - return new Promise((resolve, reject) => { - // isort does have the ability to read from the process input stream and return the formatted code out of the output stream - // However they don't support returning the diff of the formatted text when reading data from the input stream - // Yes getting text formatted that way avoids having to create a temporary file, however the diffing will have - // to be done here in node (extension), i.e. extension cpu, i.e. les responsive solution - let importScript = path.join(extensionDir, "pythonFiles", "sortImports.py"); - let tmpFileCreated = document.isDirty; - let filePromise = tmpFileCreated ? getTempFileWithDocumentContents(document) : Promise.resolve(document.fileName); - filePromise.then(filePath => { - const pythonPath = settings.PythonSettings.getInstance().pythonPath; - const args = settings.PythonSettings.getInstance().sortImports.args.join(' '); - child_process.exec(`${pythonPath} "${importScript}" "${filePath}" --diff ${args}`, (error, stdout, stderr) => { - if (tmpFileCreated) { - fs.unlink(filePath); - } - if (error || (stderr && stderr.length > 0)) { - return reject(error ? error : stderr); - } - - let formattedText = stdout; - let edits = getTextEditsFromPatch(document.getText(), stdout); - resolve(edits); - }); - }).catch(reject); + // isort does have the ability to read from the process input stream and return the formatted code out of the output stream. + // However they don't support returning the diff of the formatted text when reading data from the input stream. + // Yes getting text formatted that way avoids having to create a temporary file, however the diffing will have + // to be done here in node (extension), i.e. extension cpu, i.e. less responsive solution. + const importScript = path.join(extensionDir, 'pythonFiles', 'sortImports.py'); + const tmpFileCreated = document.isDirty; + const filePath = tmpFileCreated ? await getTempFileWithDocumentContents(document) : document.fileName; + const settings = PythonSettings.getInstance(document.uri); + const pythonPath = settings.pythonPath; + const isort = settings.sortImports.path; + const args = settings.sortImports.args.join(' '); + let isortCmd = ''; + if (typeof isort === 'string' && isort.length > 0) { + if (isort.indexOf(' ') > 0) { + isortCmd = `"${isort}" "${filePath}" --diff ${args}`; + } else { + isortCmd = `${isort} "${filePath}" --diff ${args}`; + } + } else { + if (pythonPath.indexOf(' ') > 0) { + isortCmd = `"${pythonPath}" "${importScript}" "${filePath}" --diff ${args}`; + } else { + isortCmd = `${pythonPath} "${importScript}" "${filePath}" --diff ${args}`; + } + } + // tslint:disable-next-line:promise-must-complete + return await new Promise((resolve, reject) => { + child_process.exec(isortCmd, (error, stdout, stderr) => { + if (tmpFileCreated) { + fs.unlink(filePath); + } + if (error || (stderr && stderr.length > 0)) { + reject(error ? error : stderr); + } else { + resolve(getTextEditsFromPatch(document.getText(), stdout)); + } + }); }); } } diff --git a/src/client/providers/jediHelpers.ts b/src/client/providers/jediHelpers.ts index 42d59d42b137..9ff87f966b84 100644 --- a/src/client/providers/jediHelpers.ts +++ b/src/client/providers/jediHelpers.ts @@ -16,8 +16,8 @@ export function extractSignatureAndDocumentation(definition: proxy.IAutoComplete /// ``` const txt = definition.description || definition.text; const rawDocString = typeof definition.raw_docstring === 'string' ? definition.raw_docstring.trim() : ''; - const firstLineOfRawDocString = rawDocString.length > 0 ? rawDocString.split(EOL)[0] : ''; - let lines = txt.split(EOL); + const firstLineOfRawDocString = rawDocString.length > 0 ? rawDocString.split(/\r?\n/)[0] : ''; + let lines = txt.split(/\r?\n/); const startIndexOfDocString = firstLineOfRawDocString === '' ? -1 : lines.findIndex(line => line.indexOf(firstLineOfRawDocString) === 0); let signatureLines = startIndexOfDocString === -1 ? [lines.shift()] : lines.splice(0, startIndexOfDocString); @@ -48,11 +48,59 @@ export function extractSignatureAndDocumentation(definition: proxy.IAutoComplete return [signature, lines.join(EOL).trim().replace(/^\s+|\s+$/g, '').trim()]; } -export function extractHoverInfo(definition: proxy.IAutoCompleteItem): vscode.Hover { - const parts = extractSignatureAndDocumentation(definition, true); - const hoverInfo: vscode.MarkedString[] = parts[0].length === 0 ? [] : [{ language: 'python', value: parts[0] }]; - if (parts[1].length > 0) { - hoverInfo.push(parts[1]); +export function highlightCode(docstring: string): string { + /********** + * + * Magic. Do not touch. [What is the best comment in source code](https://stackoverflow.com/a/185106) + * + * This method uses several regexs to 'translate' reStructruedText syntax (Python doc syntax) to Markdown syntax. + * + * Let's just keep it unchanged unless a better solution becomes possible. + * + **********/ + // Add 2 line break before and after docstring (used to match a blank line) + docstring = EOL + EOL + docstring.trim() + EOL + EOL; + // Section title -> heading level 2 + docstring = docstring.replace(/(.+\r?\n)[-=]+\r?\n/g, '## $1' + EOL); + // Directives: '.. directive::' -> '**directive**' + docstring = docstring.replace(/\.\. (.*)::/g, '**$1**'); + // Pattern of 'var : description' + let paramLinePattern = '[\\*\\w_]+ ?:[^:\r\n]+'; + // Add new line after and before param line + docstring = docstring.replace(new RegExp(`(${EOL + paramLinePattern})`, 'g'), `$1${EOL}`); + docstring = docstring.replace(new RegExp(`(${EOL + paramLinePattern + EOL})`, 'g'), `${EOL}$1`); + // 'var : description' -> '`var` description' + docstring = docstring.replace(/\r?\n([\*\w]+) ?: ?([^:\r\n]+\r?\n)/g, `${EOL}\`$1\` $2`); + // Doctest blocks: begin with `>>>` and end with blank line + docstring = docstring.replace(/(>>>[\w\W]+?\r?\n)\r?\n/g, `${'```python' + EOL}$1${'```' + EOL + EOL}`); + // Literal blocks: begin with `::` (literal blocks are indented or quoted; for simplicity, we end literal blocks with blank line) + docstring = docstring.replace(/(\r?\n[^\.]*)::\r?\n\r?\n([\w\W]+?\r?\n)\r?\n/g, `$1${EOL + '```' + EOL}$2${'```' + EOL + EOL}`); + // Remove indentation in Field lists and Literal blocks + let inCodeBlock = false; + let codeIndentation = 0; + let lines = docstring.split(/\r?\n/); + for (let i = 0; i < lines.length; i++) { + let line = lines[i]; + if (line.startsWith('```')) { + inCodeBlock = !inCodeBlock; + if (inCodeBlock) { + codeIndentation = lines[i + 1].match(/^ */)[0].length; + } + continue; + } + if (!inCodeBlock) { + lines[i] = line.replace(/^ {4,8}/, ''); + // Field lists: ':field:' -> '**field**' + lines[i] = lines[i].replace(/:(.+?):/g, '**$1** '); + } else { + if (codeIndentation !== 0) { + lines[i] = line.substring(codeIndentation); + } + } } - return new vscode.Hover(hoverInfo); -} \ No newline at end of file + docstring = lines.join(EOL); + // Grid Tables + docstring = docstring.replace(/\r?\n[\+-]+\r?\n/g, EOL); + docstring = docstring.replace(/\r?\n[\+=]+\r?\n/g, s => s.replace(/\+/g, '|').replace(/=/g, '-')); + return docstring.trim(); +} diff --git a/src/client/providers/jediProxy.ts b/src/client/providers/jediProxy.ts index 7f1d9af05cc9..ae1a03b9858c 100644 --- a/src/client/providers/jediProxy.ts +++ b/src/client/providers/jediProxy.ts @@ -5,13 +5,14 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as settings from './../common/configSettings'; import * as logger from './../common/logger'; -import * as telemetryHelper from "../common/telemetry"; -import { execPythonFile, validatePath } from "../common/utils"; +import * as telemetryHelper from '../telemetry'; +import { execPythonFile, getCustomEnvVarsSync, validatePath } from '../common/utils'; import { createDeferred, Deferred } from '../common/helpers'; +import { getCustomEnvVars } from '../common/utils'; +import { mergeEnvVariables } from '../common/envFileParser'; +import { IPythonSettings, PythonSettings } from '../common/configSettings'; const IS_WINDOWS = /^win/.test(process.platform); -var proc: child_process.ChildProcess; -var pythonSettings = settings.PythonSettings.getInstance(); const pythonVSCodeTypeMappings = new Map(); pythonVSCodeTypeMappings.set('none', vscode.CompletionItemKind.Value); @@ -106,6 +107,7 @@ function getMappedVSCodeSymbol(pythonType: string): vscode.SymbolKind { export enum CommandType { Arguments, Completions, + Hover, Usages, Definitions, Symbols @@ -115,418 +117,465 @@ var commandNames = new Map(); commandNames.set(CommandType.Arguments, "arguments"); commandNames.set(CommandType.Completions, "completions"); commandNames.set(CommandType.Definitions, "definitions"); +commandNames.set(CommandType.Hover, "tooltip"); commandNames.set(CommandType.Usages, "usages"); commandNames.set(CommandType.Symbols, "names"); -export class JediProxy extends vscode.Disposable { - public constructor(context: vscode.ExtensionContext) { - super(killProcess); - - context.subscriptions.push(this); - initialize(context.asAbsolutePath(".")); +export class JediProxy implements vscode.Disposable { + private proc: child_process.ChildProcess; + private pythonSettings: PythonSettings; + + public constructor(private extensionRootDir: string, private workspacePath: string) { + this.pythonSettings = PythonSettings.getInstance(vscode.Uri.file(workspacePath)); + this.lastKnownPythonInterpreter = this.pythonSettings.pythonPath + this.pythonSettings.on('change', this.onPythonSettingsChanged.bind(this)); + vscode.workspace.onDidChangeConfiguration(this.onConfigChanged.bind(this)); + this.onConfigChanged(); + this.initialize(extensionRootDir); + } + public dispose() { + this.killProcess(); } - private cmdId: number = 0; public getNextCommandId(): number { return this.cmdId++; } - public sendCommand(cmd: ICommand): Promise { - return sendCommand(cmd); - } -} - -// keep track of the directory so we can re-spawn the process -let pythonProcessCWD = ""; -function initialize(dir: string) { - pythonProcessCWD = dir; - spawnProcess(path.join(dir, "pythonFiles")); -} - -// Check if settings changes -let lastKnownPythonInterpreter = pythonSettings.pythonPath; -pythonSettings.on('change', onPythonSettingsChanged); -function onPythonSettingsChanged() { - if (lastKnownPythonInterpreter === pythonSettings.pythonPath) { - return; + // keep track of the directory so we can re-spawn the process + private pythonProcessCWD = ""; + private initialize(dir: string) { + this.pythonProcessCWD = dir; + this.spawnProcess(path.join(dir, "pythonFiles")); } - killProcess(); - clearPendingRequests(); - initialize(pythonProcessCWD); -} -function clearPendingRequests() { - commandQueue = []; - commands.forEach(item => { - item.deferred.resolve(); - }); - commands.clear(); -} -var previousData = ""; -var commands = new Map>(); -var commandQueue: number[] = []; - -function killProcess() { - try { - if (proc) { - proc.kill(); + // Check if settings changes + private lastKnownPythonInterpreter: string; + private onPythonSettingsChanged() { + if (this.lastKnownPythonInterpreter === this.pythonSettings.pythonPath) { + return; } + this.killProcess(); + this.clearPendingRequests(); + this.initialize(this.pythonProcessCWD); } - catch (ex) { } - proc = null; -} -function handleError(source: string, errorMessage: string) { - logger.error(source + ' jediProxy', `Error (${source}) ${errorMessage}`); -} + private clearPendingRequests() { + this.commandQueue = []; + this.commands.forEach(item => { + item.deferred.resolve(); + }); + this.commands.clear(); + } + private previousData = ""; + private commands = new Map>(); + private commandQueue: number[] = []; -let spawnRetryAttempts = 0;; -function spawnProcess(dir: string) { - try { - let environmentVariables = { 'PYTHONUNBUFFERED': '1' }; - for (let setting in process.env) { - if (!environmentVariables[setting]) { - environmentVariables[setting] = process.env[setting]; + private killProcess() { + try { + if (this.proc) { + this.proc.kill(); } } + catch (ex) { } + this.proc = null; + } + + private handleError(source: string, errorMessage: string) { + logger.error(source + ' jediProxy', `Error (${source}) ${errorMessage}`); + } - logger.log('child_process.spawn in jediProxy', 'Value of pythonSettings.pythonPath is :' + pythonSettings.pythonPath); - const args = ["completion.py"]; - if (typeof pythonSettings.jediPath !== 'string' || pythonSettings.jediPath.length === 0) { - if (Array.isArray(pythonSettings.devOptions) && - pythonSettings.devOptions.some(item => item.toUpperCase().trim() === 'USERELEASEAUTOCOMP')) { - // Use standard version of jedi library - args.push('std'); + private spawnRetryAttempts = 0; + private spawnProcess(dir: string) { + try { + let environmentVariables = { 'PYTHONUNBUFFERED': '1' }; + let customEnvironmentVars = getCustomEnvVarsSync(vscode.Uri.file(dir)); + if (customEnvironmentVars) { + environmentVariables = mergeEnvVariables(environmentVariables, customEnvironmentVars); + } + environmentVariables = mergeEnvVariables(environmentVariables); + + logger.log('child_process.spawn in jediProxy', 'Value of pythonSettings.pythonPath is :' + this.pythonSettings.pythonPath); + const args = ["completion.py"]; + if (typeof this.pythonSettings.jediPath !== 'string' || this.pythonSettings.jediPath.length === 0) { + if (Array.isArray(this.pythonSettings.devOptions) && + this.pythonSettings.devOptions.some(item => item.toUpperCase().trim() === 'USERELEASEAUTOCOMP')) { + // Use standard version of jedi library + args.push('std'); + } + else { + // Use preview version of jedi library + args.push('preview'); + } } else { - // Use preview version of jedi library - args.push('preview'); + args.push('custom'); + args.push(this.pythonSettings.jediPath); + } + if (Array.isArray(this.pythonSettings.autoComplete.preloadModules) && + this.pythonSettings.autoComplete.preloadModules.length > 0) { + var modules = this.pythonSettings.autoComplete.preloadModules.filter(m => m.trim().length > 0).join(','); + args.push(modules); } + this.proc = child_process.spawn(this.pythonSettings.pythonPath, args, { + cwd: dir, + env: environmentVariables + }); } - else { - args.push('custom'); - args.push(pythonSettings.jediPath); + catch (ex) { + return this.handleError("spawnProcess", ex.message); } - proc = child_process.spawn(pythonSettings.pythonPath, args, { - cwd: dir, - env: environmentVariables + this.proc.stderr.setEncoding('utf8'); + this.proc.stderr.on("data", (data: string) => { + this.handleError("stderr", data); }); - } - catch (ex) { - return handleError("spawnProcess", ex.message); - } - proc.stderr.setEncoding('utf8'); - proc.stderr.on("data", (data: string) => { - handleError("stderr", data); - }); - proc.on("end", (end) => { - logger.error('spawnProcess.end', "End - " + end); - }); - proc.on("error", error => { - handleError("error", error + ''); - spawnRetryAttempts++; - if (spawnRetryAttempts < 10 && error && error.message && - error.message.indexOf('This socket has been ended by the other party') >= 0) { - spawnProcess(dir); - } - }); - proc.stdout.setEncoding('utf8'); - proc.stdout.on("data", (data: string) => { - //Possible there was an exception in parsing the data returned - //So append the data then parse it - var dataStr = previousData = previousData + data + ""; - var responses: any[]; - try { - responses = dataStr.split(/\r?\n/g).filter(line => line.length > 0).map(resp => JSON.parse(resp)); - previousData = ""; - } - catch (ex) { - // Possible we've only received part of the data, hence don't clear previousData - // Don't log errors when we haven't received the entire response - if (ex.message.indexOf('Unexpected end of input') === -1 && - ex.message.indexOf('Unexpected end of JSON input') === -1 && - ex.message.indexOf('Unexpected token') === -1) { - handleError("stdout", ex.message); + this.proc.on("end", (end) => { + logger.error('spawnProcess.end', "End - " + end); + }); + this.proc.on("error", error => { + this.handleError("error", error + ''); + this.spawnRetryAttempts++; + if (this.spawnRetryAttempts < 10 && error && error.message && + error.message.indexOf('This socket has been ended by the other party') >= 0) { + this.spawnProcess(dir); } - return; - } - - responses.forEach((response) => { - // What's this, can't remember, - // Great example of poorly written code (this whole file is a mess) - // I think this needs to be removed, because this is misspelt, it is argments, 'U' is missing - // And that case is handled further down - // case CommandType.Arguments: { - // Rewrite this mess to use stratergy.. - if (response["argments"]) { - var index = commandQueue.indexOf(cmd.id); - commandQueue.splice(index, 1); - return; + }); + this.proc.stdout.setEncoding('utf8'); + this.proc.stdout.on("data", (data: string) => { + //Possible there was an exception in parsing the data returned + //So append the data then parse it + var dataStr = this.previousData = this.previousData + data + ""; + var responses: any[]; + try { + responses = dataStr.split(/\r?\n/g).filter(line => line.length > 0).map(resp => JSON.parse(resp)); + this.previousData = ""; } - var responseId = response["id"]; - - var cmd = >commands.get(responseId); - if (typeof cmd === "object" && cmd !== null) { - commands.delete(responseId); - var index = commandQueue.indexOf(cmd.id); - commandQueue.splice(index, 1); - - if (cmd.delays && typeof cmd.telemetryEvent === 'string') { - cmd.delays.stop(); - telemetryHelper.sendTelemetryEvent(cmd.telemetryEvent, null, cmd.delays.toMeasures()); + catch (ex) { + // Possible we've only received part of the data, hence don't clear previousData + // Don't log errors when we haven't received the entire response + if (ex.message.indexOf('Unexpected end of input') === -1 && + ex.message.indexOf('Unexpected end of JSON input') === -1 && + ex.message.indexOf('Unexpected token') === -1) { + this.handleError("stdout", ex.message); } + return; + } - // Check if this command has expired - if (cmd.token.isCancellationRequested) { - cmd.deferred.resolve(); + responses.forEach((response) => { + // What's this, can't remember, + // Great example of poorly written code (this whole file is a mess) + // I think this needs to be removed, because this is misspelt, it is argments, 'U' is missing + // And that case is handled further down + // case CommandType.Arguments: { + // Rewrite this mess to use stratergy.. + if (response["argments"]) { + var index = this.commandQueue.indexOf(cmd.id); + this.commandQueue.splice(index, 1); return; } + var responseId = response["id"]; + + var cmd = >this.commands.get(responseId); + if (typeof cmd === "object" && cmd !== null) { + this.commands.delete(responseId); + var index = this.commandQueue.indexOf(cmd.id); + this.commandQueue.splice(index, 1); + + if (cmd.delay && typeof cmd.telemetryEvent === 'string') { + // cmd.delays.stop(); + // telemetryHelper.sendTelemetryEvent(cmd.telemetryEvent, null, cmd.delays.toMeasures()); + } - switch (cmd.command) { - case CommandType.Completions: { - let results = response['results']; - results = Array.isArray(results) ? results : []; - results.forEach(item => { - const originalType = item.type; - item.type = getMappedVSCodeType(originalType); - item.kind = getMappedVSCodeSymbol(originalType); - item.raw_type = getMappedVSCodeType(originalType); - }); - - let completionResult: ICompletionResult = { - items: results, - requestId: cmd.id - }; - cmd.deferred.resolve(completionResult); - break; + // Check if this command has expired + if (cmd.token.isCancellationRequested) { + cmd.deferred.resolve(); + return; } - case CommandType.Definitions: { - let defs = response['results']; - let defResult: IDefinitionResult = { - requestId: cmd.id, - definition: null - }; - if (defs.length > 0) { - let def = defs[0]; - const originalType = def.type as string; - defResult.definition = { - columnIndex: Number(def.column), - fileName: def.fileName, - lineIndex: Number(def.line), - text: def.text, - type: getMappedVSCodeType(originalType), - kind: getMappedVSCodeSymbol(originalType) + + switch (cmd.command) { + case CommandType.Completions: { + let results = response['results']; + results = Array.isArray(results) ? results : []; + results.forEach(item => { + const originalType = item.type; + item.type = getMappedVSCodeType(originalType); + item.kind = getMappedVSCodeSymbol(originalType); + item.rawType = getMappedVSCodeType(originalType); + }); + + let completionResult: ICompletionResult = { + items: results, + requestId: cmd.id }; + cmd.deferred.resolve(completionResult); + break; } + case CommandType.Definitions: { + let defs = response['results']; + let defResult: IDefinitionResult = { + requestId: cmd.id, + definitions: [] + }; + if (defs.length > 0) { + defResult.definitions = defs.map(def => { + const originalType = def.type as string; + return { + fileName: def.fileName, + text: def.text, + rawType: originalType, + type: getMappedVSCodeType(originalType), + kind: getMappedVSCodeSymbol(originalType), + container: def.container, + range: { + startLine: def.range.start_line, + startColumn: def.range.start_column, + endLine: def.range.end_line, + endColumn: def.range.end_column + } + }; + }); + } - cmd.deferred.resolve(defResult); - break; - } - case CommandType.Symbols: { - var defs = response['results']; - defs = Array.isArray(defs) ? defs : []; - var defResults: ISymbolResult = { - requestId: cmd.id, - definitions: [] - }; - defResults.definitions = defs.map(def => { - const originalType = def.type as string; - return { - columnIndex: def.column, - fileName: def.fileName, - lineIndex: def.line, - text: def.text, - type: getMappedVSCodeType(originalType), - kind: getMappedVSCodeSymbol(originalType) + cmd.deferred.resolve(defResult); + break; + } + case CommandType.Hover: { + let defs = response['results']; + var defResult: IHoverResult = { + requestId: cmd.id, + items: defs.map(def => { + return { + kind: getMappedVSCodeSymbol(def.type), + description: def.description, + signature: def.signature, + docstring: def.docstring, + text: def.text + }; + }) }; - }); - cmd.deferred.resolve(defResults); - break; - } - case CommandType.Usages: { - let defs = response['results']; - defs = Array.isArray(defs) ? defs : []; - var refResult: IReferenceResult = { - requestId: cmd.id, - references: defs.map(item => { + cmd.deferred.resolve(defResult); + break; + } + case CommandType.Symbols: { + let defs = response['results']; + defs = Array.isArray(defs) ? defs : []; + var defResults: ISymbolResult = { + requestId: cmd.id, + definitions: [] + }; + defResults.definitions = defs.map(def => { + const originalType = def.type as string; return { - columnIndex: item.column, - fileName: item.fileName, - lineIndex: item.line - 1, - moduleName: item.moduleName, - name: item.name + fileName: def.fileName, + text: def.text, + rawType: originalType, + type: getMappedVSCodeType(originalType), + kind: getMappedVSCodeSymbol(originalType), + container: def.container, + range: { + startLine: def.range.start_line, + startColumn: def.range.start_column, + endLine: def.range.end_line, + endColumn: def.range.end_column + } }; - } - ) - }; + }); - cmd.deferred.resolve(refResult); - break; - } - case CommandType.Arguments: { - let defs = response["results"]; - cmd.deferred.resolve({ - requestId: cmd.id, - definitions: defs - }); - break; - } - } - } + cmd.deferred.resolve(defResults); + break; + } + case CommandType.Usages: { + let defs = response['results']; + defs = Array.isArray(defs) ? defs : []; + var refResult: IReferenceResult = { + requestId: cmd.id, + references: defs.map(item => { + return { + columnIndex: item.column, + fileName: item.fileName, + lineIndex: item.line - 1, + moduleName: item.moduleName, + name: item.name + }; + } + ) + }; - //Ok, check if too many pending requets - if (commandQueue.length > 10) { - var items = commandQueue.splice(0, commandQueue.length - 10); - items.forEach(id => { - if (commands.has(id)) { - const cmd = commands.get(id); - try { - cmd.deferred.resolve(null); + cmd.deferred.resolve(refResult); + break; } - catch (ex) { + case CommandType.Arguments: { + let defs = response["results"]; + cmd.deferred.resolve({ + requestId: cmd.id, + definitions: defs + }); + break; } - commands.delete(id); } - }); - } - }); - }); -} + } -function sendCommand(cmd: ICommand): Promise { - if (!proc) { - return Promise.reject(new Error("Python proc not initialized")); - } - var executionCmd = >cmd; - var payload = createPayload(executionCmd); - executionCmd.deferred = createDeferred(); - if (typeof executionCmd.telemetryEvent === 'string') { - executionCmd.delays = new telemetryHelper.Delays(); - } - try { - proc.stdin.write(JSON.stringify(payload) + "\n"); - commands.set(executionCmd.id, executionCmd); - commandQueue.push(executionCmd.id); + //Ok, check if too many pending requets + if (this.commandQueue.length > 10) { + var items = this.commandQueue.splice(0, this.commandQueue.length - 10); + items.forEach(id => { + if (this.commands.has(id)) { + const cmd = this.commands.get(id); + try { + cmd.deferred.resolve(null); + } + catch (ex) { + } + this.commands.delete(id); + } + }); + } + }); + }); } - catch (ex) { - console.error(ex); - //If 'This socket is closed.' that means process didn't start at all (at least not properly) - if (ex.message === "This socket is closed.") { - killProcess(); + public sendCommand(cmd: ICommand): Promise { + if (!this.proc) { + return Promise.reject(new Error("Python proc not initialized")); } - else { - handleError("sendCommand", ex.message); + var executionCmd = >cmd; + var payload = this.createPayload(executionCmd); + executionCmd.deferred = createDeferred(); + // if (typeof executionCmd.telemetryEvent === 'string') { + // executionCmd.delays = new telemetryHelper.Delays(); + // } + try { + this.proc.stdin.write(JSON.stringify(payload) + "\n"); + this.commands.set(executionCmd.id, executionCmd); + this.commandQueue.push(executionCmd.id); } - return Promise.reject(ex); - } - return executionCmd.deferred.promise; -} + catch (ex) { + console.error(ex); + //If 'This socket is closed.' that means process didn't start at all (at least not properly) + if (ex.message === "This socket is closed.") { -function createPayload(cmd: IExecutionCommand): any { - var payload = { - id: cmd.id, - prefix: "", - lookup: commandNames.get(cmd.command), - path: cmd.fileName, - source: cmd.source, - line: cmd.lineIndex, - column: cmd.columnIndex, - config: getConfig() - }; - - if (cmd.command === CommandType.Symbols) { - delete payload.column; - delete payload.line; + this.killProcess(); + } + else { + this.handleError("sendCommand", ex.message); + } + return Promise.reject(ex); + } + return executionCmd.deferred.promise; } - return payload; -} - -let lastKnownPythonPath: string = null; -let additionalAutoCopletePaths: string[] = []; -function getPathFromPythonCommand(args: string[]): Promise { - return execPythonFile(pythonSettings.pythonPath, args, vscode.workspace.rootPath).then(stdout => { - if (stdout.length === 0) { - return ""; + private createPayload(cmd: IExecutionCommand): any { + var payload = { + id: cmd.id, + prefix: "", + lookup: commandNames.get(cmd.command), + path: cmd.fileName, + source: cmd.source, + line: cmd.lineIndex, + column: cmd.columnIndex, + config: this.getConfig() + }; + + if (cmd.command === CommandType.Symbols) { + delete payload.column; + delete payload.line; } - let lines = stdout.split(/\r?\n/g).filter(line => line.length > 0); - return validatePath(lines[0]); - }).catch(() => { - return ""; - }); -} -vscode.workspace.onDidChangeConfiguration(onConfigChanged); -onConfigChanged(); -function onConfigChanged() { - // We're only interested in changes to the python path - if (lastKnownPythonPath === pythonSettings.pythonPath) { - return; + + return payload; } - lastKnownPythonPath = pythonSettings.pythonPath; - let filePaths = [ - // Sysprefix - getPathFromPythonCommand(["-c", "import sys;print(sys.prefix)"]), - // exeucutable path - getPathFromPythonCommand(["-c", "import sys;print(sys.executable)"]), - // Python specific site packages - getPathFromPythonCommand(["-c", "from distutils.sysconfig import get_python_lib; print(get_python_lib())"]), - // Python global site packages, as a fallback in case user hasn't installed them in custom environment - getPathFromPythonCommand(["-m", "site", "--user-site"]), - ]; - - const pythonPath: string = process.env['PYTHONPATH']; - if (typeof pythonPath === 'string' && pythonPath.length > 0) { - filePaths.push(Promise.resolve(pythonPath.trim())); + private lastKnownPythonPath: string = null; + private additionalAutoCopletePaths: string[] = []; + private getPathFromPythonCommand(args: string[]): Promise { + return execPythonFile(this.workspacePath, this.pythonSettings.pythonPath, args, this.workspacePath).then(stdout => { + if (stdout.length === 0) { + return ""; + } + let lines = stdout.split(/\r?\n/g).filter(line => line.length > 0); + return validatePath(lines[0]); + }).catch(() => { + return ""; + }); } - Promise.all(filePaths).then(paths => { - // Last item return a path, we need only the folder - if (paths[1].length > 0) { - paths[1] = path.dirname(paths[1]); + private onConfigChanged() { + // We're only interested in changes to the python path + if (this.lastKnownPythonPath === this.pythonSettings.pythonPath) { + return; } - // On windows we also need the libs path (second item will return c:\xxx\lib\site-packages) - // This is returned by "from distutils.sysconfig import get_python_lib; print(get_python_lib())" - if (IS_WINDOWS && paths[2].length > 0) { - paths.splice(3, 0, path.join(paths[2], "..")); + this.lastKnownPythonPath = this.pythonSettings.pythonPath; + let filePaths = [ + // Sysprefix + this.getPathFromPythonCommand(["-c", "import sys;print(sys.prefix)"]), + // exeucutable path + this.getPathFromPythonCommand(["-c", "import sys;print(sys.executable)"]), + // Python specific site packages + this.getPathFromPythonCommand(["-c", "from distutils.sysconfig import get_python_lib; print(get_python_lib())"]), + // Python global site packages, as a fallback in case user hasn't installed them in custom environment + this.getPathFromPythonCommand(["-m", "site", "--user-site"]), + ]; + + let PYTHONPATH: string = process.env['PYTHONPATH']; + if (typeof PYTHONPATH !== 'string') { + PYTHONPATH = ''; } - additionalAutoCopletePaths = paths.filter(p => p.length > 0); - }); -} - -function getConfig() { - // Add support for paths relative to workspace - let extraPaths = pythonSettings.autoComplete.extraPaths.map(extraPath => { - if (path.isAbsolute(extraPath)) { - return extraPath; + let customEnvironmentVars = getCustomEnvVarsSync(vscode.Uri.file(this.pythonProcessCWD)); + if (customEnvironmentVars && customEnvironmentVars['PYTHONPATH']) { + let PYTHONPATHFromEnvFile = customEnvironmentVars['PYTHONPATH'] as string; + if (!path.isAbsolute(PYTHONPATHFromEnvFile) && this.workspacePath === 'string') { + PYTHONPATHFromEnvFile = path.resolve(this.workspacePath, PYTHONPATHFromEnvFile); + } + PYTHONPATH += (PYTHONPATH.length > 0 ? + path.delimiter : '') + PYTHONPATHFromEnvFile; } - if (typeof vscode.workspace.rootPath !== 'string') { - return ''; + if (typeof PYTHONPATH === 'string' && PYTHONPATH.length > 0) { + filePaths.push(Promise.resolve(PYTHONPATH.trim())); } - return path.join(vscode.workspace.rootPath, extraPath); - }); + Promise.all(filePaths).then(paths => { + // Last item return a path, we need only the folder + if (paths[1].length > 0) { + paths[1] = path.dirname(paths[1]); + } - // Always add workspace path into extra paths - if (typeof vscode.workspace.rootPath === 'string') { - extraPaths.unshift(vscode.workspace.rootPath); + // On windows we also need the libs path (second item will return c:\xxx\lib\site-packages) + // This is returned by "from distutils.sysconfig import get_python_lib; print(get_python_lib())" + if (IS_WINDOWS && paths[2].length > 0) { + paths.splice(3, 0, path.join(paths[2], "..")); + } + this.additionalAutoCopletePaths = paths.filter(p => p.length > 0); + }); } - let distinctExtraPaths = extraPaths.concat(additionalAutoCopletePaths) - .filter(value => value.length > 0) - .filter((value, index, self) => self.indexOf(value) === index); - - return { - extraPaths: distinctExtraPaths, - useSnippets: false, - caseInsensitiveCompletion: true, - showDescriptions: true, - fuzzyMatcher: true - }; -} + private getConfig() { + // Add support for paths relative to workspace + let extraPaths = this.pythonSettings.autoComplete.extraPaths.map(extraPath => { + if (path.isAbsolute(extraPath)) { + return extraPath; + } + if (typeof this.workspacePath !== 'string') { + return ''; + } + return path.join(this.workspacePath, extraPath); + }); + + // Always add workspace path into extra paths + if (typeof this.workspacePath === 'string') { + extraPaths.unshift(this.workspacePath); + } + let distinctExtraPaths = extraPaths.concat(this.additionalAutoCopletePaths) + .filter(value => value.length > 0) + .filter((value, index, self) => self.indexOf(value) === index); + + return { + extraPaths: distinctExtraPaths, + useSnippets: false, + caseInsensitiveCompletion: true, + showDescriptions: true, + fuzzyMatcher: true + }; + } +} export interface ICommand { telemetryEvent?: string; command: CommandType; @@ -540,7 +589,7 @@ interface IExecutionCommand extends ICommand { id?: number; deferred?: Deferred; token: vscode.CancellationToken; - delays?: telemetryHelper.Delays; + delay?: number; } export interface ICommandError { @@ -553,8 +602,11 @@ export interface ICommandResult { export interface ICompletionResult extends ICommandResult { items: IAutoCompleteItem[]; } +export interface IHoverResult extends ICommandResult { + items: IHoverItem[]; +} export interface IDefinitionResult extends ICommandResult { - definition: IDefinition; + definitions: IDefinition[]; } export interface IReferenceResult extends ICommandResult { references: IReference[]; @@ -590,67 +642,79 @@ export interface IReference { export interface IAutoCompleteItem { type: vscode.CompletionItemKind; - raw_type: vscode.CompletionItemKind; + rawType: vscode.CompletionItemKind; kind: vscode.SymbolKind; text: string; description: string; raw_docstring: string; rightLabel: string; } +interface IDefinitionRange { + startLine: number; + startColumn: number; + endLine: number; + endColumn: number; +} export interface IDefinition { + rawType: string; type: vscode.CompletionItemKind; kind: vscode.SymbolKind; text: string; fileName: string; - columnIndex: number; - lineIndex: number; + container: string; + range: IDefinitionRange; +} + +export interface IHoverItem { + kind: vscode.SymbolKind; + text: string; + description: string; + docstring: string; + signature: string; } -export class JediProxyHandler { - private jediProxy: JediProxy; - private lastToken: vscode.CancellationToken; - private lastCommandId: number; - private cancellationTokenSource: vscode.CancellationTokenSource; +export class JediProxyHandler implements vscode.Disposable { + private commandCancellationTokenSources: Map; public get JediProxy(): JediProxy { return this.jediProxy; } - public constructor(context: vscode.ExtensionContext, jediProxy: JediProxy = null) { - this.jediProxy = jediProxy ? jediProxy : new JediProxy(context); + public constructor(private jediProxy: JediProxy = null) { + this.commandCancellationTokenSources = new Map(); + } + public dispose() { + this.jediProxy.dispose(); } - public sendCommand(cmd: ICommand, token?: vscode.CancellationToken): Promise { var executionCmd = >cmd; - const def = createDeferred(); executionCmd.id = executionCmd.id || this.jediProxy.getNextCommandId(); - if (this.cancellationTokenSource) { - try { - this.cancellationTokenSource.cancel(); - } - catch (ex) { } + if (this.commandCancellationTokenSources.has(cmd.command)) { + const cancellation = this.commandCancellationTokenSources.get(cmd.command); + cancellation.cancel(); } - this.cancellationTokenSource = new vscode.CancellationTokenSource(); - executionCmd.token = this.cancellationTokenSource.token; - this.lastToken = token; - this.lastCommandId = executionCmd.id; + const cancellation = new vscode.CancellationTokenSource(); + this.commandCancellationTokenSources.set(cmd.command, cancellation); + executionCmd.token = cancellation.token; - this.jediProxy.sendCommand(executionCmd).then(data => { - if (this.lastToken.isCancellationRequested || !data || data.requestId !== this.lastCommandId) { - def.resolve(); - } - if (data) { - def.resolve(data); - } - else { - def.resolve(); - } - }).catch(reason => { - console.error(reason); - def.resolve(); - }); - return def.promise; + return this.jediProxy.sendCommand(executionCmd) + .catch(reason => { + console.error(reason); + return undefined; + }); + } + + public sendCommandNonCancellableCommand(cmd: ICommand, token?: vscode.CancellationToken): Promise { + var executionCmd = >cmd; + executionCmd.id = executionCmd.id || this.jediProxy.getNextCommandId(); + executionCmd.token = token; + + return this.jediProxy.sendCommand(executionCmd) + .catch(reason => { + console.error(reason); + return undefined; + }); } } diff --git a/src/client/providers/lintProvider.ts b/src/client/providers/lintProvider.ts index 86de3bb13ac8..b734800bfa83 100644 --- a/src/client/providers/lintProvider.ts +++ b/src/client/providers/lintProvider.ts @@ -1,22 +1,28 @@ 'use strict'; -import * as vscode from 'vscode'; +import * as fs from 'fs'; import * as path from 'path'; +import * as vscode from 'vscode'; +import { ConfigurationTarget, Uri, workspace } from 'vscode'; +import { ConfigSettingMonitor } from '../common/configSettingMonitor'; +import { PythonSettings } from '../common/configSettings'; +import { LinterErrors } from '../common/constants'; +import { PythonLanguage } from '../jupyter/common/constants'; import * as linter from '../linters/baseLinter'; -import * as prospector from './../linters/prospector'; -import * as pylint from './../linters/pylint'; -import * as pep8 from './../linters/pep8Linter'; -import * as pylama from './../linters/pylama'; +import { sendTelemetryWhenDone } from '../telemetry'; +import { LINTING } from '../telemetry/constants'; +import { StopWatch } from '../telemetry/stopWatch'; import * as flake8 from './../linters/flake8'; -import * as pydocstyle from './../linters/pydocstyle'; import * as mypy from './../linters/mypy'; -import * as settings from '../common/configSettings'; -import * as telemetryHelper from '../common/telemetry'; -import * as telemetryContracts from '../common/telemetryContracts'; -import * as fs from 'fs'; -import { LinterErrors } from '../common/constants'; -const Minimatch = require("minimatch").Minimatch; +import * as pep8 from './../linters/pep8Linter'; +import * as prospector from './../linters/prospector'; +import * as pydocstyle from './../linters/pydocstyle'; +import * as pylama from './../linters/pylama'; +import * as pylint from './../linters/pylint'; +// tslint:disable-next-line:no-require-imports no-var-requires +const Minimatch = require('minimatch').Minimatch; +const uriSchemesToIgnore = ['git', 'showModifications']; const lintSeverityToVSSeverity = new Map(); lintSeverityToVSSeverity.set(linter.LintMessageSeverity.Error, vscode.DiagnosticSeverity.Error); lintSeverityToVSSeverity.set(linter.LintMessageSeverity.Hint, vscode.DiagnosticSeverity.Hint); @@ -24,59 +30,44 @@ lintSeverityToVSSeverity.set(linter.LintMessageSeverity.Information, vscode.Diag lintSeverityToVSSeverity.set(linter.LintMessageSeverity.Warning, vscode.DiagnosticSeverity.Warning); function createDiagnostics(message: linter.ILintMessage, document: vscode.TextDocument): vscode.Diagnostic { - let endCol = document.lineAt(message.line - 1).text.length; + const position = new vscode.Position(message.line - 1, message.column); + const range = new vscode.Range(position, position); - // try to get the first word from the startig position - if (message.possibleWord === 'string' && message.possibleWord.length > 0) { - endCol = message.column + message.possibleWord.length; - } - - let range = new vscode.Range(new vscode.Position(message.line - 1, message.column), new vscode.Position(message.line - 1, endCol)); - - let severity = lintSeverityToVSSeverity.get(message.severity); - let diagnostic = new vscode.Diagnostic(range, message.code + ':' + message.message, severity); + const severity = lintSeverityToVSSeverity.get(message.severity); + const diagnostic = new vscode.Diagnostic(range, `${message.code}:${message.message}`, severity); diagnostic.code = message.code; diagnostic.source = message.provider; return diagnostic; } +// tslint:disable-next-line:interface-name interface DocumentHasJupyterCodeCells { + // tslint:disable-next-line:callable-types (doc: vscode.TextDocument, token: vscode.CancellationToken): Promise; } -export class LintProvider extends vscode.Disposable { - private settings: settings.IPythonSettings; +export class LintProvider implements vscode.Disposable { private diagnosticCollection: vscode.DiagnosticCollection; private linters: linter.BaseLinter[] = []; private pendingLintings = new Map(); private outputChannel: vscode.OutputChannel; private context: vscode.ExtensionContext; private disposables: vscode.Disposable[]; - private ignoreMinmatches: { match: (fname: string) => boolean }[]; + private configMonitor: ConfigSettingMonitor; public constructor(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel, - private documentHasJupyterCodeCells: DocumentHasJupyterCodeCells) { - super(() => { }); + public documentHasJupyterCodeCells: DocumentHasJupyterCodeCells) { this.outputChannel = outputChannel; this.context = context; - this.settings = settings.PythonSettings.getInstance(); this.disposables = []; - this.ignoreMinmatches = []; this.initialize(); - - this.disposables.push(vscode.workspace.onDidChangeConfiguration(this.onConfigChanged.bind(this))); + this.configMonitor = new ConfigSettingMonitor('linting'); + this.configMonitor.on('change', this.lintSettingsChangedHandler.bind(this)); } - dispose() { + public dispose() { this.disposables.forEach(d => d.dispose()); - } - private onConfigChanged() { - this.initializeGlobs(); - } - private initializeGlobs() { - this.ignoreMinmatches = settings.PythonSettings.getInstance().linting.ignorePatterns.map(pattern => { - return new Minimatch(pattern); - }); + this.configMonitor.dispose(); } private isDocumentOpen(uri: vscode.Uri): boolean { - return vscode.window.visibleTextEditors.some(editor => editor.document && editor.document.uri.fsPath === uri.fsPath); + return vscode.workspace.textDocuments.some(document => document.uri.fsPath === uri.fsPath); } private initialize() { @@ -91,21 +82,27 @@ export class LintProvider extends vscode.Disposable { this.linters.push(new mypy.Linter(this.outputChannel)); let disposable = vscode.workspace.onDidSaveTextDocument((e) => { - if (e.languageId !== 'python' || !this.settings.linting.enabled || !this.settings.linting.lintOnSave) { + const settings = PythonSettings.getInstance(e.uri); + if (e.languageId !== 'python' || !settings.linting.enabled || !settings.linting.lintOnSave) { return; } - this.lintDocument(e, 100); + this.lintDocument(e, 100, 'save'); }); this.context.subscriptions.push(disposable); vscode.workspace.onDidOpenTextDocument((e) => { - if (e.languageId !== 'python' || !this.settings.linting.enabled) { + const settings = PythonSettings.getInstance(e.uri); + if (e.languageId !== 'python' || !settings.linting.enabled) { + return; + } + // Exclude files opened by vscode when showing a diff view + if (uriSchemesToIgnore.indexOf(e.uri.scheme) >= 0) { return; } if (!e.uri.path || (path.basename(e.uri.path) === e.uri.path && !fs.existsSync(e.uri.path))) { return; } - this.lintDocument(e, 100); + this.lintDocument(e, 100, 'auto'); }, this.context.subscriptions); disposable = vscode.workspace.onDidCloseTextDocument(textDocument => { @@ -114,16 +111,38 @@ export class LintProvider extends vscode.Disposable { } // Check if this document is still open as a duplicate editor - if (this.isDocumentOpen(textDocument.uri) && this.diagnosticCollection.has(textDocument.uri)) { + if (!this.isDocumentOpen(textDocument.uri) && this.diagnosticCollection.has(textDocument.uri)) { this.diagnosticCollection.set(textDocument.uri, []); } }); this.context.subscriptions.push(disposable); - this.initializeGlobs(); + this.lintOpenPythonFiles(); } + private lintOpenPythonFiles() { + workspace.textDocuments.forEach(document => { + if (document.languageId === PythonLanguage.language) { + this.onLintDocument(document, 'auto'); + } + }); + } + private lintSettingsChangedHandler(configTarget: ConfigurationTarget, wkspaceOrFolder: Uri) { + if (configTarget === ConfigurationTarget.Workspace) { + this.lintOpenPythonFiles(); + return; + } + // Look for python files that belong to the specified workspace folder. + workspace.textDocuments.forEach(document => { + const wkspaceFolder = workspace.getWorkspaceFolder(document.uri); + if (wkspaceFolder && wkspaceFolder.uri.fsPath === wkspaceOrFolder.fsPath) { + this.onLintDocument(document, 'auto'); + } + }); + } + + // tslint:disable-next-line:member-ordering private lastTimeout: number; - private lintDocument(document: vscode.TextDocument, delay: number): void { + private lintDocument(document: vscode.TextDocument, delay: number, trigger: 'auto' | 'save'): void { // Since this is a hack, lets wait for 2 seconds before linting // Give user to continue typing before we waste CPU time if (this.lastTimeout) { @@ -132,14 +151,20 @@ export class LintProvider extends vscode.Disposable { } this.lastTimeout = setTimeout(() => { - this.onLintDocument(document); + this.onLintDocument(document, trigger); }, delay); } - - private onLintDocument(document: vscode.TextDocument): void { + private onLintDocument(document: vscode.TextDocument, trigger: 'auto' | 'save'): void { // Check if we need to lint this document - const relativeFileName = typeof vscode.workspace.rootPath === 'string' ? path.relative(vscode.workspace.rootPath, document.fileName) : document.fileName; - if (this.ignoreMinmatches.some(matcher => matcher.match(document.fileName) || matcher.match(relativeFileName))) { + const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri); + const workspaceRootPath = (workspaceFolder && typeof workspaceFolder.uri.fsPath === 'string') ? workspaceFolder.uri.fsPath : undefined; + const relativeFileName = typeof workspaceRootPath === 'string' ? path.relative(workspaceRootPath, document.fileName) : document.fileName; + const settings = PythonSettings.getInstance(document.uri); + const ignoreMinmatches = settings.linting.ignorePatterns.map(pattern => { + return new Minimatch(pattern); + }); + + if (ignoreMinmatches.some(matcher => matcher.match(document.fileName) || matcher.match(relativeFileName))) { return; } if (this.pendingLintings.has(document.uri.fsPath)) { @@ -147,7 +172,7 @@ export class LintProvider extends vscode.Disposable { this.pendingLintings.delete(document.uri.fsPath); } - let cancelToken = new vscode.CancellationTokenSource(); + const cancelToken = new vscode.CancellationTokenSource(); cancelToken.token.onCancellationRequested(() => { if (this.pendingLintings.has(document.uri.fsPath)) { this.pendingLintings.delete(document.uri.fsPath); @@ -156,21 +181,22 @@ export class LintProvider extends vscode.Disposable { this.pendingLintings.set(document.uri.fsPath, cancelToken); this.outputChannel.clear(); - let promises: Promise[] = this.linters.map(linter => { - if (!linter.isEnabled()) { - return Promise.resolve([]); - } - // turn off telemetry for linters (at least for now) - //let delays = new telemetryHelper.Delays(); - return linter.runLinter(document).then(results => { - //delays.stop(); - //telemetryHelper.sendTelemetryEvent(telemetryContracts.IDE.Lint, { Lint_Provider: linter.Id }, delays.toMeasures()); - return results; + const promises: Promise[] = this.linters + .filter(item => item.isEnabled(document.uri)) + .map(item => { + if (typeof workspaceRootPath !== 'string' && !settings.linting.enabledWithoutWorkspace) { + return Promise.resolve([]); + } + const stopWatch = new StopWatch(); + const promise = item.lint(document, cancelToken.token); + const hasCustomArgs = item.linterArgs(document.uri).length > 0; + const executableSpecified = item.isLinterExecutableSpecified(document.uri); + sendTelemetryWhenDone(LINTING, promise, stopWatch, { tool: item.Id, hasCustomArgs, trigger, executableSpecified }); + return promise; }); - }); this.documentHasJupyterCodeCells(document, cancelToken.token).then(hasJupyterCodeCells => { - // linters will resolve asynchronously - keep a track of all - // diagnostics reported as them come in + // linters will resolve asynchronously - keep a track of all + // diagnostics reported as them come in. let diagnostics: vscode.Diagnostic[] = []; promises.forEach(p => { @@ -192,7 +218,7 @@ export class LintProvider extends vscode.Disposable { }); // Limit the number of messages to the max value - diagnostics = diagnostics.filter((value, index) => index <= this.settings.linting.maxNumberOfProblems); + diagnostics = diagnostics.filter((value, index) => index <= settings.linting.maxNumberOfProblems); if (!this.isDocumentOpen(document.uri)) { diagnostics = []; @@ -203,4 +229,4 @@ export class LintProvider extends vscode.Disposable { }); }); } -} \ No newline at end of file +} diff --git a/src/client/providers/objectDefinitionProvider.ts b/src/client/providers/objectDefinitionProvider.ts new file mode 100644 index 000000000000..6742d2de9787 --- /dev/null +++ b/src/client/providers/objectDefinitionProvider.ts @@ -0,0 +1,95 @@ +'use strict'; + +import * as vscode from 'vscode'; +import { JediFactory } from '../languageServices/jediProxyFactory'; +import { captureTelemetry } from '../telemetry'; +import { GO_TO_OBJECT_DEFINITION } from '../telemetry/constants'; +import * as defProvider from './definitionProvider'; + +export function activateGoToObjectDefinitionProvider(jediFactory: JediFactory): vscode.Disposable[] { + const def = new PythonObjectDefinitionProvider(jediFactory); + const commandRegistration = vscode.commands.registerCommand("python.goToPythonObject", () => def.goToObjectDefinition()); + return [def, commandRegistration] as vscode.Disposable[]; +} + +export class PythonObjectDefinitionProvider { + private readonly _defProvider: defProvider.PythonDefinitionProvider; + public constructor(jediFactory: JediFactory) { + this._defProvider = new defProvider.PythonDefinitionProvider(jediFactory); + } + + @captureTelemetry(GO_TO_OBJECT_DEFINITION) + public async goToObjectDefinition() { + let pathDef = await this.getObjectDefinition(); + if (typeof pathDef !== 'string' || pathDef.length === 0) { + return; + } + + let parts = pathDef.split('.'); + let source = ''; + let startColumn = 0; + if (parts.length === 1) { + source = `import ${parts[0]}`; + startColumn = 'import '.length; + } + else { + let mod = parts.shift(); + source = `from ${mod} import ${parts.join('.')}`; + startColumn = `from ${mod} import `.length; + } + const range = new vscode.Range(0, startColumn, 0, source.length - 1); + let doc = { + fileName: 'test.py', + lineAt: (line: number) => { + return { text: source }; + }, + getWordRangeAtPosition: (position: vscode.Position) => range, + isDirty: true, + getText: () => source + }; + + let tokenSource = new vscode.CancellationTokenSource(); + let defs = await this._defProvider.provideDefinition(doc, range.start, tokenSource.token); + + if (defs === null) { + await vscode.window.showInformationMessage(`Definition not found for '${pathDef}'`); + return; + } + + let uri: vscode.Uri; + let lineNumber: number; + if (Array.isArray(defs) && defs.length > 0) { + uri = defs[0].uri; + lineNumber = defs[0].range.start.line; + } + if (!Array.isArray(defs) && defs.uri) { + uri = defs.uri; + lineNumber = defs.range.start.line; + } + + if (uri) { + let doc = await vscode.workspace.openTextDocument(uri); + await vscode.window.showTextDocument(doc); + await vscode.commands.executeCommand('revealLine', { lineNumber: lineNumber, 'at': 'top' }); + } + else { + await vscode.window.showInformationMessage(`Definition not found for '${pathDef}'`); + } + } + + private intputValidation(value: string): string | undefined | null { + if (typeof value !== 'string') { + return ''; + } + value = value.trim(); + if (value.length === 0) { + return ''; + } + + return null; + } + private async getObjectDefinition(): Promise { + let value = await vscode.window.showInputBox({ prompt: "Enter Object Path", validateInput: this.intputValidation }); + return value; + } +} diff --git a/src/client/providers/referenceProvider.ts b/src/client/providers/referenceProvider.ts index cde21431a400..71521ef64c23 100644 --- a/src/client/providers/referenceProvider.ts +++ b/src/client/providers/referenceProvider.ts @@ -1,27 +1,25 @@ 'use strict'; import * as vscode from 'vscode'; +import { JediFactory } from '../languageServices/jediProxyFactory'; +import { captureTelemetry } from '../telemetry'; +import { REFERENCE } from '../telemetry/constants'; import * as proxy from './jediProxy'; -import * as telemetryContracts from "../common/telemetryContracts"; - export class PythonReferenceProvider implements vscode.ReferenceProvider { - private jediProxyHandler: proxy.JediProxyHandler; - - public constructor(context: vscode.ExtensionContext, jediProxy: proxy.JediProxy = null) { - this.jediProxyHandler = new proxy.JediProxyHandler(context, jediProxy); - } + public constructor(private jediFactory: JediFactory) { } private static parseData(data: proxy.IReferenceResult): vscode.Location[] { if (data && data.references.length > 0) { - var references = data.references.filter(ref => { + // tslint:disable-next-line:no-unnecessary-local-variable + const references = data.references.filter(ref => { if (!ref || typeof ref.columnIndex !== 'number' || typeof ref.lineIndex !== 'number' || typeof ref.fileName !== 'string' || ref.columnIndex === -1 || ref.lineIndex === -1 || ref.fileName.length === 0) { return false; } return true; }).map(ref => { - var definitionResource = vscode.Uri.file(ref.fileName); - var range = new vscode.Range(ref.lineIndex, ref.columnIndex, ref.lineIndex, ref.columnIndex); + const definitionResource = vscode.Uri.file(ref.fileName); + const range = new vscode.Range(ref.lineIndex, ref.columnIndex, ref.lineIndex, ref.columnIndex); return new vscode.Location(definitionResource, range); }); @@ -31,18 +29,19 @@ export class PythonReferenceProvider implements vscode.ReferenceProvider { return []; } + @captureTelemetry(REFERENCE) public provideReferences(document: vscode.TextDocument, position: vscode.Position, context: vscode.ReferenceContext, token: vscode.CancellationToken): Thenable { - var filename = document.fileName; + const filename = document.fileName; if (document.lineAt(position.line).text.match(/^\s*\/\//)) { - return Promise.resolve(); + return Promise.resolve(null); } if (position.character <= 0) { - return Promise.resolve(); + return Promise.resolve(null); } - var range = document.getWordRangeAtPosition(position); - var columnIndex = range.isEmpty ? position.character : range.end.character; - var cmd: proxy.ICommand = { + const range = document.getWordRangeAtPosition(position); + const columnIndex = range.isEmpty ? position.character : range.end.character; + const cmd: proxy.ICommand = { command: proxy.CommandType.Usages, fileName: filename, columnIndex: columnIndex, @@ -53,7 +52,7 @@ export class PythonReferenceProvider implements vscode.ReferenceProvider { cmd.source = document.getText(); } - return this.jediProxyHandler.sendCommand(cmd, token).then(data => { + return this.jediFactory.getJediProxyHandler(document.uri).sendCommand(cmd, token).then(data => { return PythonReferenceProvider.parseData(data); }); } diff --git a/src/client/providers/renameProvider.ts b/src/client/providers/renameProvider.ts index 274c41bdca58..b1cdad8dce50 100644 --- a/src/client/providers/renameProvider.ts +++ b/src/client/providers/renameProvider.ts @@ -1,20 +1,25 @@ 'use strict'; -import * as vscode from 'vscode'; -import { RefactorProxy } from '../refactor/proxy'; -import { getWorkspaceEditsFromPatch } from '../common/editor'; import * as path from 'path'; +import * as vscode from 'vscode'; import { PythonSettings } from '../common/configSettings'; +import { getWorkspaceEditsFromPatch } from '../common/editor'; +import { Installer, Product } from '../common/installer'; +import { RefactorProxy } from '../refactor/proxy'; +import { captureTelemetry } from '../telemetry'; +import { REFACTOR_RENAME } from '../telemetry/constants'; -const pythonSettings = PythonSettings.getInstance(); const EXTENSION_DIR = path.join(__dirname, '..', '..', '..'); interface RenameResponse { results: [{ diff: string }]; } export class PythonRenameProvider implements vscode.RenameProvider { + private installer: Installer; constructor(private outputChannel: vscode.OutputChannel) { + this.installer = new Installer(outputChannel); } + @captureTelemetry(REFACTOR_RENAME) public provideRenameEdits(document: vscode.TextDocument, position: vscode.Position, newName: string, token: vscode.CancellationToken): Thenable { return vscode.workspace.saveAll(false).then(() => { return this.doRename(document, position, newName, token); @@ -29,7 +34,7 @@ export class PythonRenameProvider implements vscode.RenameProvider { return; } - var range = document.getWordRangeAtPosition(position); + const range = document.getWordRangeAtPosition(position); if (!range || range.isEmpty) { return; } @@ -38,15 +43,26 @@ export class PythonRenameProvider implements vscode.RenameProvider { return; } - let proxy = new RefactorProxy(EXTENSION_DIR, pythonSettings, vscode.workspace.rootPath); + let workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri); + if (!workspaceFolder && Array.isArray(vscode.workspace.workspaceFolders) && vscode.workspace.workspaceFolders.length > 0) { + workspaceFolder = vscode.workspace.workspaceFolders[0]; + } + const workspaceRoot = workspaceFolder ? workspaceFolder.uri.fsPath : __dirname; + const pythonSettings = PythonSettings.getInstance(workspaceFolder ? workspaceFolder.uri : undefined); + + const proxy = new RefactorProxy(EXTENSION_DIR, pythonSettings, workspaceRoot); return proxy.rename(document, newName, document.uri.fsPath, range).then(response => { - //return response.results[0].diff; - const workspaceEdit = getWorkspaceEditsFromPatch(response.results.map(fileChanges => fileChanges.diff)); - return workspaceEdit; + const fileDiffs = response.results.map(fileChanges => fileChanges.diff); + return getWorkspaceEditsFromPatch(fileDiffs, workspaceRoot); }).catch(reason => { - vscode.window.showErrorMessage(reason); - this.outputChannel.appendLine(reason); - return; + if (reason === 'Not installed') { + this.installer.promptToInstall(Product.rope, document.uri); + return Promise.reject(''); + } else { + vscode.window.showErrorMessage(reason); + this.outputChannel.appendLine(reason); + } + return Promise.reject(reason); }); } } diff --git a/src/client/providers/replProvider.ts b/src/client/providers/replProvider.ts new file mode 100644 index 000000000000..753c261744ba --- /dev/null +++ b/src/client/providers/replProvider.ts @@ -0,0 +1,49 @@ +import { commands, Disposable, window, workspace } from 'vscode'; +import { PythonSettings } from '../common/configSettings'; +import { Commands } from '../common/constants'; +import { getPathFromPythonCommand } from '../common/utils'; +import { captureTelemetry } from '../telemetry'; +import { REPL } from '../telemetry/constants'; + +export class ReplProvider implements Disposable { + private readonly disposables: Disposable[] = []; + constructor() { + this.registerCommand(); + } + public dispose() { + this.disposables.forEach(disposable => disposable.dispose()); + } + private registerCommand() { + const disposable = commands.registerCommand(Commands.Start_REPL, this.commandHandler, this); + this.disposables.push(disposable); + } + @captureTelemetry(REPL) + private async commandHandler() { + const pythonPath = await this.getPythonPath(); + if (!pythonPath) { + return; + } + let pythonInterpreterPath: string; + try { + pythonInterpreterPath = await getPathFromPythonCommand(pythonPath).catch(() => pythonPath); + // tslint:disable-next-line:variable-name + } catch (_ex) { + pythonInterpreterPath = pythonPath; + } + const term = window.createTerminal('Python', pythonInterpreterPath); + term.show(); + this.disposables.push(term); + } + private async getPythonPath(): Promise { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + return PythonSettings.getInstance().pythonPath; + } + if (workspace.workspaceFolders.length === 1) { + return PythonSettings.getInstance(workspace.workspaceFolders[0].uri).pythonPath; + } + + // tslint:disable-next-line:no-any prefer-type-cast + const workspaceFolder = await (window as any).showWorkspaceFolderPick({ placeHolder: 'Select a workspace' }); + return workspace ? PythonSettings.getInstance(workspaceFolder.uri).pythonPath : undefined; + } +} diff --git a/src/client/providers/setInterpreterProvider.ts b/src/client/providers/setInterpreterProvider.ts deleted file mode 100644 index 3377130bcdc5..000000000000 --- a/src/client/providers/setInterpreterProvider.ts +++ /dev/null @@ -1,248 +0,0 @@ -"use strict"; -import * as child_process from 'child_process'; -import * as path from "path"; -import * as fs from "fs"; -import * as vscode from "vscode"; -import * as settings from "./../common/configSettings"; -import * as utils from "../common/utils"; -import { createDeferred } from '../common/helpers'; - -// where to find the Python binary within a conda env -const CONDA_RELATIVE_PY_PATH = utils.IS_WINDOWS ? ['python'] : ['bin', 'python']; -const CHECK_PYTHON_INTERPRETER_REGEXP = utils.IS_WINDOWS ? /^python(\d+(.\d+)?)?\.exe$/ : /^python(\d+(.\d+)?)?$/; - -interface PythonPathSuggestion { - label: string; // myenvname - path: string; // /full/path/to/bin/python - type: string; // conda -} - -interface PythonPathQuickPickItem extends vscode.QuickPickItem { - path: string; -} - -function getSearchPaths(): Promise { - if (utils.IS_WINDOWS) { - const localAppData = process.env['LOCALAPPDATA']; - const appData = process.env['APPDATA']; - const lookupParentDirectories = [process.env['PROGRAMFILES'], process.env['PROGRAMFILES(X86)'], - localAppData, appData, - process.env['SystemDrive']]; - if (appData) { - lookupParentDirectories.push(path.join(localAppData, 'Programs')) - } - if (localAppData) { - lookupParentDirectories.push(path.join(appData, 'Programs')) - } - const dirPromises = lookupParentDirectories.map(rootDir => { - if (!rootDir) { - return Promise.resolve([]); - } - const def = createDeferred(); - fs.readdir(rootDir, (error, files) => { - if (error) { - return def.resolve([]); - } - const possiblePythonDirs = []; - files.forEach(name => { - const fullPath = path.join(rootDir, name); - try { - if ((name.toUpperCase().indexOf('PYTHON') >= 0 || name.toUpperCase().indexOf('ANACONDA') >= 0) && - fs.statSync(fullPath).isDirectory()) { - possiblePythonDirs.push(fullPath); - } - } - catch (ex) { - } - }); - def.resolve(possiblePythonDirs); - }); - return def.promise; - }); - - return Promise.all(dirPromises).then(validPathsCollection => { - return validPathsCollection.reduce((previousValue, currentValue) => previousValue.concat(currentValue), []); - }); - } else { - const paths = ['/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin', '/usr/local/sbin']; - // Add support for paths such as /Users/xxx/anaconda/bin - if (process.env['HOME']) { - paths.push(path.join(process.env['HOME'], 'anaconda', 'bin')); - paths.push(path.join(process.env['HOME'], 'python', 'bin')); - } - return Promise.resolve(paths); - } -} - -export function activateSetInterpreterProvider(): vscode.Disposable { - return vscode.commands.registerCommand("python.setInterpreter", setInterpreter); -} - -function lookForInterpretersInPath(pathToCheck: string): Promise { - return new Promise(resolve => { - // Now look for Interpreters in this directory - fs.readdir(pathToCheck, (err, subDirs) => { - if (err) { - return resolve([]); - } - const interpreters = subDirs - .filter(subDir => CHECK_PYTHON_INTERPRETER_REGEXP.test(subDir)) - .map(subDir => path.join(pathToCheck, subDir)); - resolve(interpreters); - }); - }); -} -function lookForInterpretersInVirtualEnvs(pathToCheck: string): Promise { - return new Promise(resolve => { - // Now look for Interpreters in this directory - fs.readdir(pathToCheck, (err, subDirs) => { - if (err) { - return resolve([]); - } - const envsInterpreters = []; - const promises = subDirs.map(subDir => { - subDir = path.join(pathToCheck, subDir); - const interpreterFolder = utils.IS_WINDOWS ? path.join(subDir, 'scripts') : path.join(subDir, 'bin'); - return lookForInterpretersInPath(interpreterFolder); - }); - Promise.all(promises).then(pathsWithInterpreters => { - pathsWithInterpreters.forEach(interpreters => { - interpreters.map(interpter => { - envsInterpreters.push({ - label: path.basename(interpter), path: interpter, type: '' - }); - }); - }); - - resolve(envsInterpreters); - }); - }); - }); -} -function suggestionsFromKnownPaths(): Promise { - return getSearchPaths().then(paths => { - const promises = paths.map(p => { - return utils.validatePath(p).then(validatedPath => { - if (validatedPath.length === 0) { - return Promise.resolve([]); - } - - return lookForInterpretersInPath(validatedPath); - }); - }); - return Promise.all(promises).then(listOfInterpreters => { - const suggestions: PythonPathSuggestion[] = []; - const interpreters = listOfInterpreters.reduce((previous, current) => previous.concat(current), []); - interpreters.filter(interpter => interpter.length > 0).map(interpter => { - suggestions.push({ - label: path.basename(interpter), path: interpter, type: '' - }); - }); - return suggestions; - }); - }); -} -function suggestionsFromConda(): Promise { - return new Promise((resolve, reject) => { - // interrogate conda (if it's on the path) to find all environments - child_process.execFile('conda', ['info', '--json'], (error, stdout, stderr) => { - try { - const info = JSON.parse(stdout); - - // envs reported as e.g.: /Users/bob/miniconda3/envs/someEnv - const envs = info['envs']; - - // The root of the conda environment is itself a Python interpreter - envs.push(info["default_prefix"]); - - const suggestions = envs.map(env => ({ - label: path.basename(env), // e.g. someEnv, miniconda3 - path: path.join(env, ...CONDA_RELATIVE_PY_PATH), - type: 'conda', - })); - resolve(suggestions); - } catch (e) { - // Failed because either: - // 1. conda is not installed - // 2. `conda info --json` has changed signature - // 3. output of `conda info --json` has changed in structure - // In all cases, we can't offer conda pythonPath suggestions. - return resolve([]); - } - }); - }); -} - -function suggestionToQuickPickItem(suggestion: PythonPathSuggestion): PythonPathQuickPickItem { - let detail = suggestion.path; - if (suggestion.path.startsWith(vscode.workspace.rootPath)) { - detail = `.${path.sep}` + path.relative(vscode.workspace.rootPath, suggestion.path); - } - detail = utils.IS_WINDOWS ? detail.replace(/\\/g, "/") : detail; - return { - label: suggestion.label, - description: suggestion.type, - detail: detail, - path: utils.IS_WINDOWS ? suggestion.path.replace(/\\/g, "/") : suggestion.path - }; -} - -function suggestPythonPaths(): Promise { - // For now we only interrogate conda for suggestions. - const condaSuggestions = suggestionsFromConda(); - const knownPathSuggestions = suggestionsFromKnownPaths(); - const virtualEnvSuggestions = lookForInterpretersInVirtualEnvs(vscode.workspace.rootPath); - - // Here we could also look for virtualenvs/default install locations... - - return Promise.all([condaSuggestions, knownPathSuggestions, virtualEnvSuggestions]).then(suggestions => { - const quickPicks: PythonPathQuickPickItem[] = []; - suggestions.forEach(list => { - quickPicks.push(...list.map(suggestionToQuickPickItem)); - }); - - return quickPicks; - }); -} - -function setPythonPath(pythonPath: string, created: boolean = false) { - if (pythonPath.startsWith(vscode.workspace.rootPath)) { - pythonPath = path.join('${workspaceRoot}', path.relative(vscode.workspace.rootPath, pythonPath)); - } - const pythonConfig = vscode.workspace.getConfiguration('python'); - pythonConfig.update('pythonPath', pythonPath).then(() => { - //Done - }, reason => { - vscode.window.showErrorMessage(`Failed to set 'pythonPath'. Error: ${reason.message}`); - console.error(reason); - }); -} - -function presentQuickPickOfSuggestedPythonPaths() { - let currentPythonPath = settings.PythonSettings.getInstance().pythonPath; - if (currentPythonPath.startsWith(vscode.workspace.rootPath)) { - currentPythonPath = `.${path.sep}` + path.relative(vscode.workspace.rootPath, currentPythonPath); - } - const quickPickOptions: vscode.QuickPickOptions = { - matchOnDetail: true, - matchOnDescription: false, - placeHolder: `current: ${currentPythonPath}` - }; - - suggestPythonPaths().then(suggestions => { - suggestions = suggestions.sort((a, b) => a.path > b.path ? 1 : -1); - vscode.window.showQuickPick(suggestions, quickPickOptions).then( - value => { - if (value !== undefined) { - setPythonPath(value.path); - } - }); - }); -} - -function setInterpreter() { - if (typeof vscode.workspace.rootPath !== 'string'){ - return vscode.window.showErrorMessage('Please open a workspace to select the Python Interpreter'); - } - presentQuickPickOfSuggestedPythonPaths(); -} \ No newline at end of file diff --git a/src/client/providers/signatureProvider.ts b/src/client/providers/signatureProvider.ts index 853541b2a8f4..bf6480a4d3a2 100644 --- a/src/client/providers/signatureProvider.ts +++ b/src/client/providers/signatureProvider.ts @@ -1,42 +1,43 @@ -"use strict"; +'use strict'; -import * as vscode from "vscode"; -import { TextDocument, Position, CancellationToken, SignatureHelp, ExtensionContext } from "vscode"; -import * as proxy from "./jediProxy"; -import * as telemetryContracts from "../common/telemetryContracts"; +import * as vscode from 'vscode'; +import { CancellationToken, Position, SignatureHelp, TextDocument } from 'vscode'; +import { JediFactory } from '../languageServices/jediProxyFactory'; +import { captureTelemetry } from '../telemetry'; +import { SIGNATURE } from '../telemetry/constants'; +import * as proxy from './jediProxy'; const DOCSTRING_PARAM_PATTERNS = [ - "\\s*:type\\s*PARAMNAME:\\s*([^\\n, ]+)", // Sphinx - "\\s*:param\\s*(\\w?)\\s*PARAMNAME:[^\\n]+", // Sphinx param with type - "\\s*@type\\s*PARAMNAME:\\s*([^\\n, ]+)" // Epydoc + '\\s*:type\\s*PARAMNAME:\\s*([^\\n, ]+)', // Sphinx + '\\s*:param\\s*(\\w?)\\s*PARAMNAME:[^\\n]+', // Sphinx param with type + '\\s*@type\\s*PARAMNAME:\\s*([^\\n, ]+)' // Epydoc ]; /** - * Extrct the documentation for parameters from a given docstring - * + * Extract the documentation for parameters from a given docstring. * @param {string} paramName Name of the parameter * @param {string} docString The docstring for the function * @returns {string} Docstring for the parameter */ function extractParamDocString(paramName: string, docString: string): string { - let paramDocString = ""; + let paramDocString = ''; // In docstring the '*' is escaped with a backslash - paramName = paramName.replace(new RegExp("\\*", "g"), "\\\\\\*"); + paramName = paramName.replace(new RegExp('\\*', 'g'), '\\\\\\*'); DOCSTRING_PARAM_PATTERNS.forEach(pattern => { if (paramDocString.length > 0) { return; } - pattern = pattern.replace("PARAMNAME", paramName); - let regExp = new RegExp(pattern); - let matches = regExp.exec(docString); + pattern = pattern.replace('PARAMNAME', paramName); + const regExp = new RegExp(pattern); + const matches = regExp.exec(docString); if (matches && matches.length > 0) { paramDocString = matches[0]; - if (paramDocString.indexOf(":") >= 0) { - paramDocString = paramDocString.substring(paramDocString.indexOf(":") + 1); + if (paramDocString.indexOf(':') >= 0) { + paramDocString = paramDocString.substring(paramDocString.indexOf(':') + 1); } - if (paramDocString.indexOf(":") >= 0) { - paramDocString = paramDocString.substring(paramDocString.indexOf(":") + 1); + if (paramDocString.indexOf(':') >= 0) { + paramDocString = paramDocString.substring(paramDocString.indexOf(':') + 1); } } }); @@ -44,22 +45,17 @@ function extractParamDocString(paramName: string, docString: string): string { return paramDocString.trim(); } export class PythonSignatureProvider implements vscode.SignatureHelpProvider { - private jediProxyHandler: proxy.JediProxyHandler; - - public constructor(context: vscode.ExtensionContext, jediProxy: proxy.JediProxy = null) { - this.jediProxyHandler = new proxy.JediProxyHandler(context, jediProxy); - } + public constructor(private jediFactory: JediFactory) { } private static parseData(data: proxy.IArgumentsResult): vscode.SignatureHelp { if (data && Array.isArray(data.definitions) && data.definitions.length > 0) { - let signature = new SignatureHelp(); + const signature = new SignatureHelp(); signature.activeSignature = 0; data.definitions.forEach(def => { signature.activeParameter = def.paramindex; - // Don't display the documentation, as vs code doesn't format the docmentation - // i.e. line feeds are not respected, long content is stripped - let sig = { - // documentation: def.docstring, + // Don't display the documentation, as vs code doesn't format the docmentation. + // i.e. line feeds are not respected, long content is stripped. + const sig = { label: def.description, parameters: [] }; @@ -79,15 +75,16 @@ export class PythonSignatureProvider implements vscode.SignatureHelpProvider { return new SignatureHelp(); } - provideSignatureHelp(document: TextDocument, position: Position, token: CancellationToken): Thenable { - let cmd: proxy.ICommand = { + @captureTelemetry(SIGNATURE) + public provideSignatureHelp(document: TextDocument, position: Position, token: CancellationToken): Thenable { + const cmd: proxy.ICommand = { command: proxy.CommandType.Arguments, fileName: document.fileName, columnIndex: position.character, lineIndex: position.line, source: document.getText() }; - return this.jediProxyHandler.sendCommand(cmd, token).then(data => { + return this.jediFactory.getJediProxyHandler(document.uri).sendCommand(cmd, token).then(data => { return PythonSignatureProvider.parseData(data); }); } diff --git a/src/client/providers/simpleRefactorProvider.ts b/src/client/providers/simpleRefactorProvider.ts index d55d56d956d4..ad8d94686b7d 100644 --- a/src/client/providers/simpleRefactorProvider.ts +++ b/src/client/providers/simpleRefactorProvider.ts @@ -1,41 +1,62 @@ 'use strict'; import * as vscode from 'vscode'; -import { RefactorProxy } from '../refactor/proxy'; +import { PythonSettings } from '../common/configSettings'; import { getTextEditsFromPatch } from '../common/editor'; -import { PythonSettings, IPythonSettings } from '../common/configSettings'; +import { Installer, Product } from '../common/installer'; +import { RefactorProxy } from '../refactor/proxy'; +import { sendTelemetryWhenDone } from '../telemetry'; +import { REFACTOR_EXTRACT_FUNCTION, REFACTOR_EXTRACT_VAR } from '../telemetry/constants'; +import { StopWatch } from '../telemetry/stopWatch'; interface RenameResponse { results: [{ diff: string }]; } +let installer: Installer; + export function activateSimplePythonRefactorProvider(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel) { let disposable = vscode.commands.registerCommand('python.refactorExtractVariable', () => { - extractVariable(context.extensionPath, + const stopWatch = new StopWatch(); + const promise = extractVariable(context.extensionPath, vscode.window.activeTextEditor, vscode.window.activeTextEditor.selection, + // tslint:disable-next-line:no-empty outputChannel).catch(() => { }); + sendTelemetryWhenDone(REFACTOR_EXTRACT_VAR, promise, stopWatch); }); context.subscriptions.push(disposable); disposable = vscode.commands.registerCommand('python.refactorExtractMethod', () => { - extractMethod(context.extensionPath, + const stopWatch = new StopWatch(); + const promise = extractMethod(context.extensionPath, vscode.window.activeTextEditor, vscode.window.activeTextEditor.selection, + // tslint:disable-next-line:no-empty outputChannel).catch(() => { }); + sendTelemetryWhenDone(REFACTOR_EXTRACT_FUNCTION, promise, stopWatch); }); context.subscriptions.push(disposable); + installer = new Installer(outputChannel); + context.subscriptions.push(installer); } // Exported for unit testing export function extractVariable(extensionDir: string, textEditor: vscode.TextEditor, range: vscode.Range, - outputChannel: vscode.OutputChannel, workspaceRoot: string = vscode.workspace.rootPath, - pythonSettings: IPythonSettings = PythonSettings.getInstance()): Promise { + // tslint:disable-next-line:no-any + outputChannel: vscode.OutputChannel): Promise { + + let workspaceFolder = vscode.workspace.getWorkspaceFolder(textEditor.document.uri); + if (!workspaceFolder && Array.isArray(vscode.workspace.workspaceFolders) && vscode.workspace.workspaceFolders.length > 0) { + workspaceFolder = vscode.workspace.workspaceFolders[0]; + } + const workspaceRoot = workspaceFolder ? workspaceFolder.uri.fsPath : __dirname; + const pythonSettings = PythonSettings.getInstance(workspaceFolder ? workspaceFolder.uri : undefined); return validateDocumentForRefactor(textEditor).then(() => { - let newName = 'newvariable' + new Date().getMilliseconds().toString(); - let proxy = new RefactorProxy(extensionDir, pythonSettings, workspaceRoot); - let rename = proxy.extractVariable(textEditor.document, newName, textEditor.document.uri.fsPath, range, textEditor.options).then(response => { + const newName = `newvariable${new Date().getMilliseconds().toString()}`; + const proxy = new RefactorProxy(extensionDir, pythonSettings, workspaceRoot); + const rename = proxy.extractVariable(textEditor.document, newName, textEditor.document.uri.fsPath, range, textEditor.options).then(response => { return response.results[0].diff; }); @@ -45,13 +66,20 @@ export function extractVariable(extensionDir: string, textEditor: vscode.TextEdi // Exported for unit testing export function extractMethod(extensionDir: string, textEditor: vscode.TextEditor, range: vscode.Range, - outputChannel: vscode.OutputChannel, workspaceRoot: string = vscode.workspace.rootPath, - pythonSettings: IPythonSettings = PythonSettings.getInstance()): Promise { + // tslint:disable-next-line:no-any + outputChannel: vscode.OutputChannel): Promise { + + let workspaceFolder = vscode.workspace.getWorkspaceFolder(textEditor.document.uri); + if (!workspaceFolder && Array.isArray(vscode.workspace.workspaceFolders) && vscode.workspace.workspaceFolders.length > 0) { + workspaceFolder = vscode.workspace.workspaceFolders[0]; + } + const workspaceRoot = workspaceFolder ? workspaceFolder.uri.fsPath : __dirname; + const pythonSettings = PythonSettings.getInstance(workspaceFolder ? workspaceFolder.uri : undefined); return validateDocumentForRefactor(textEditor).then(() => { - let newName = 'newmethod' + new Date().getMilliseconds().toString(); - let proxy = new RefactorProxy(extensionDir, pythonSettings, workspaceRoot); - let rename = proxy.extractMethod(textEditor.document, newName, textEditor.document.uri.fsPath, range, textEditor.options).then(response => { + const newName = `newmethod${new Date().getMilliseconds().toString()}`; + const proxy = new RefactorProxy(extensionDir, pythonSettings, workspaceRoot); + const rename = proxy.extractMethod(textEditor.document, newName, textEditor.document.uri.fsPath, range, textEditor.options).then(response => { return response.results[0].diff; }); @@ -59,17 +87,18 @@ export function extractMethod(extensionDir: string, textEditor: vscode.TextEdito }); } +// tslint:disable-next-line:no-any function validateDocumentForRefactor(textEditor: vscode.TextEditor): Promise { if (!textEditor.document.isDirty) { return Promise.resolve(); } + // tslint:disable-next-line:no-any return new Promise((resolve, reject) => { vscode.window.showInformationMessage('Please save changes before refactoring', 'Save').then(item => { if (item === 'Save') { textEditor.document.save().then(resolve, reject); - } - else { + } else { return reject(); } }); @@ -77,14 +106,14 @@ function validateDocumentForRefactor(textEditor: vscode.TextEditor): Promise, outputChannel: vscode.OutputChannel): Promise { let changeStartsAtLine = -1; return renameResponse.then(diff => { if (diff.length === 0) { return []; } - let edits = getTextEditsFromPatch(textEditor.document.getText(), diff); - return edits; + return getTextEditsFromPatch(textEditor.document.getText(), diff); }).then(edits => { return textEditor.edit(editBuilder => { edits.forEach(edit => { @@ -98,8 +127,8 @@ function extractName(extensionDir: string, textEditor: vscode.TextEditor, range: if (done && changeStartsAtLine >= 0) { let newWordPosition: vscode.Position; for (let lineNumber = changeStartsAtLine; lineNumber < textEditor.document.lineCount; lineNumber++) { - let line = textEditor.document.lineAt(lineNumber); - let indexOfWord = line.text.indexOf(newName); + const line = textEditor.document.lineAt(lineNumber); + const indexOfWord = line.text.indexOf(newName); if (indexOfWord >= 0) { newWordPosition = new vscode.Position(line.range.start.line, indexOfWord); break; @@ -121,6 +150,10 @@ function extractName(extensionDir: string, textEditor: vscode.TextEditor, range: }); } }).catch(error => { + if (error === 'Not installed') { + installer.promptToInstall(Product.rope, textEditor.document.uri); + return Promise.reject(''); + } let errorMessage = error + ''; if (typeof error === 'string') { errorMessage = error; diff --git a/src/client/providers/symbolProvider.ts b/src/client/providers/symbolProvider.ts index 00cfefe549b6..9c296edda3df 100644 --- a/src/client/providers/symbolProvider.ts +++ b/src/client/providers/symbolProvider.ts @@ -1,31 +1,51 @@ 'use strict'; import * as vscode from 'vscode'; +import { JediFactory } from '../languageServices/jediProxyFactory'; +import { captureTelemetry } from '../telemetry'; +import { SYMBOL } from '../telemetry/constants'; import * as proxy from './jediProxy'; -import * as telemetryContracts from "../common/telemetryContracts"; export class PythonSymbolProvider implements vscode.DocumentSymbolProvider { - private jediProxyHandler: proxy.JediProxyHandler; - - public constructor(context: vscode.ExtensionContext, jediProxy: proxy.JediProxy = null) { - this.jediProxyHandler = new proxy.JediProxyHandler(context, jediProxy); - } - private static parseData(data: proxy.ISymbolResult): vscode.SymbolInformation[] { + public constructor(private jediFactory: JediFactory) { } + private static parseData(document: vscode.TextDocument, data: proxy.ISymbolResult): vscode.SymbolInformation[] { if (data) { - var symbols = data.definitions.map(sym => { - var symbol = sym.kind; - var range = new vscode.Range(sym.lineIndex, sym.columnIndex, sym.lineIndex, sym.columnIndex); - return new vscode.SymbolInformation(sym.text, symbol, range, vscode.Uri.file(sym.fileName)); + const symbols = data.definitions.filter(sym => sym.fileName === document.fileName); + return symbols.map(sym => { + const symbol = sym.kind; + const range = new vscode.Range( + sym.range.startLine, sym.range.startColumn, + sym.range.endLine, sym.range.endColumn); + const uri = vscode.Uri.file(sym.fileName); + const location = new vscode.Location(uri, range); + return new vscode.SymbolInformation(sym.text, symbol, sym.container, location); }); - - return symbols; } return []; } + @captureTelemetry(SYMBOL) public provideDocumentSymbols(document: vscode.TextDocument, token: vscode.CancellationToken): Thenable { - var filename = document.fileName; + const filename = document.fileName; + + const cmd: proxy.ICommand = { + command: proxy.CommandType.Symbols, + fileName: filename, + columnIndex: 0, + lineIndex: 0 + }; + + if (document.isDirty) { + cmd.source = document.getText(); + } + + return this.jediFactory.getJediProxyHandler(document.uri).sendCommand(cmd, token).then(data => { + return PythonSymbolProvider.parseData(document, data); + }); + } + public provideDocumentSymbolsForInternalUse(document: vscode.TextDocument, token: vscode.CancellationToken): Thenable { + const filename = document.fileName; - var cmd: proxy.ICommand = { + const cmd: proxy.ICommand = { command: proxy.CommandType.Symbols, fileName: filename, columnIndex: 0, @@ -36,8 +56,8 @@ export class PythonSymbolProvider implements vscode.DocumentSymbolProvider { cmd.source = document.getText(); } - return this.jediProxyHandler.sendCommand(cmd, token).then(data => { - return PythonSymbolProvider.parseData(data); + return this.jediFactory.getJediProxyHandler(document.uri).sendCommandNonCancellableCommand(cmd, token).then(data => { + return PythonSymbolProvider.parseData(document, data); }); } } diff --git a/src/client/providers/telemetry.ts b/src/client/providers/telemetry.ts deleted file mode 100644 index 489966e710cf..000000000000 --- a/src/client/providers/telemetry.ts +++ /dev/null @@ -1,71 +0,0 @@ -import {extensions} from "vscode"; -import TelemetryReporter from "vscode-extension-telemetry"; - -// Borrowed from omnisharpServer.ts (omnisharp-vscode) -export class Delays { - immediateDelays: number = 0; // 0-25 milliseconds - nearImmediateDelays: number = 0; // 26-50 milliseconds - shortDelays: number = 0; // 51-250 milliseconds - mediumDelays: number = 0; // 251-500 milliseconds - idleDelays: number = 0; // 501-1500 milliseconds - nonFocusDelays: number = 0; // 1501-3000 milliseconds - bigDelays: number = 0; // 3000+ milliseconds - private startTime: number = Date.now(); - public stop() { - let endTime = Date.now(); - let elapsedTime = endTime - this.startTime; - - if (elapsedTime <= 25) { - this.immediateDelays += 1; - } - else if (elapsedTime <= 50) { - this.nearImmediateDelays += 1; - } - else if (elapsedTime <= 250) { - this.shortDelays += 1; - } - else if (elapsedTime <= 500) { - this.mediumDelays += 1; - } - else if (elapsedTime <= 1500) { - this.idleDelays += 1; - } - else if (elapsedTime <= 3000) { - this.nonFocusDelays += 1; - } - else { - this.bigDelays += 1; - } - } - public toMeasures(): { [key: string]: number } { - return { - immedateDelays: this.immediateDelays, - nearImmediateDelays: this.nearImmediateDelays, - shortDelays: this.shortDelays, - mediumDelays: this.mediumDelays, - idleDelays: this.idleDelays, - nonFocusDelays: this.nonFocusDelays - }; - } -} - -const extensionId = "donjayamanne.python"; -const extension = extensions.getExtension(extensionId); -const extensionVersion = extension.packageJSON.version; -const aiKey = "2f1b85ee-b1eb-469f-8ae3-5adb7114efd7"; -let reporter = new TelemetryReporter(extensionId, extensionVersion, aiKey); - -/** - * Sends a telemetry event - * @param {string} eventName The event name - * @param {object} properties An associative array of strings - * @param {object} measures An associative array of numbers - */ -export function sendTelemetryEvent(eventName: string, properties?: { - [key: string]: string; -}, measures?: { - [key: string]: number; -}) { - reporter.sendTelemetryEvent.apply(reporter, arguments); -} - diff --git a/src/client/providers/updateSparkLibraryProvider.ts b/src/client/providers/updateSparkLibraryProvider.ts index 20defe5528b7..428c7ff97018 100644 --- a/src/client/providers/updateSparkLibraryProvider.ts +++ b/src/client/providers/updateSparkLibraryProvider.ts @@ -1,7 +1,9 @@ -"use strict"; -import { Commands } from '../common/constants'; -import * as vscode from "vscode"; +'use strict'; import * as path from 'path'; +import * as vscode from 'vscode'; +import { Commands } from '../common/constants'; +import { sendTelemetryEvent } from '../telemetry'; +import { UPDATE_PYSPARK_LIBRARY } from '../telemetry/constants'; export function activateUpdateSparkLibraryProvider(): vscode.Disposable { return vscode.commands.registerCommand(Commands.Update_SparkLibrary, updateSparkLibrary); @@ -10,13 +12,15 @@ export function activateUpdateSparkLibraryProvider(): vscode.Disposable { function updateSparkLibrary() { const pythonConfig = vscode.workspace.getConfiguration('python'); const extraLibPath = 'autoComplete.extraPaths'; - let sparkHomePath = '${env.SPARK_HOME}'; + // tslint:disable-next-line:no-invalid-template-strings + const sparkHomePath = '${env.SPARK_HOME}'; pythonConfig.update(extraLibPath, [path.join(sparkHomePath, 'python'), path.join(sparkHomePath, 'python/pyspark')]).then(() => { //Done }, reason => { vscode.window.showErrorMessage(`Failed to update ${extraLibPath}. Error: ${reason.message}`); console.error(reason); - }); - vscode.window.showInformationMessage(`Make sure you have SPARK_HOME environment variable set to the root path of the local spark installation!`); -} \ No newline at end of file + }); + vscode.window.showInformationMessage('Make sure you have SPARK_HOME environment variable set to the root path of the local spark installation!'); + sendTelemetryEvent(UPDATE_PYSPARK_LIBRARY); +} diff --git a/src/client/refactor/proxy.ts b/src/client/refactor/proxy.ts index 8aa7f92ea339..2511c99450d9 100644 --- a/src/client/refactor/proxy.ts +++ b/src/client/refactor/proxy.ts @@ -1,12 +1,11 @@ 'use strict'; -import * as vscode from 'vscode'; -import * as path from 'path'; import * as child_process from 'child_process'; +import * as path from 'path'; +import * as vscode from 'vscode'; import { IPythonSettings } from '../common/configSettings'; -import { REFACTOR } from '../common/telemetryContracts'; -import { sendTelemetryEvent, Delays } from '../common/telemetry'; -import { IS_WINDOWS } from '../common/utils'; +import { mergeEnvVariables } from '../common/envFileParser'; +import { getCustomEnvVarsSync, getWindowsLineEndingCount, IS_WINDOWS } from '../common/utils'; export class RefactorProxy extends vscode.Disposable { private _process: child_process.ChildProcess; @@ -17,7 +16,7 @@ export class RefactorProxy extends vscode.Disposable { private _commandResolve: (value?: any | PromiseLike) => void; private _commandReject: (reason?: any) => void; private _initializeReject: (reason?: any) => void; - constructor(extensionDir: string, private pythonSettings: IPythonSettings, private workspaceRoot: string = vscode.workspace.rootPath) { + constructor(extensionDir: string, private pythonSettings: IPythonSettings, private workspaceRoot: string) { super(() => { }); this._extensionDir = extensionDir; } @@ -38,8 +37,11 @@ export class RefactorProxy extends vscode.Disposable { // get line count // Rope always uses LF, instead of CRLF on windows, funny isn't it // So for each line, reduce one characer (for CR) + // But Not all Windows users use CRLF const offset = document.offsetAt(position); - return offset - position.line; + const winEols = getWindowsLineEndingCount(document, offset); + + return offset - winEols; } rename(document: vscode.TextDocument, name: string, filePath: string, range: vscode.Range, options?: vscode.TextEditorOptions): Promise { if (!options) { @@ -54,7 +56,7 @@ export class RefactorProxy extends vscode.Disposable { "indent_size": options.tabSize }; - return this.sendCommand(JSON.stringify(command), REFACTOR.Rename); + return this.sendCommand(JSON.stringify(command)); } extractVariable(document: vscode.TextDocument, name: string, filePath: string, range: vscode.Range, options?: vscode.TextEditorOptions): Promise { if (!options) { @@ -69,7 +71,7 @@ export class RefactorProxy extends vscode.Disposable { "name": name, "indent_size": options.tabSize }; - return this.sendCommand(JSON.stringify(command), REFACTOR.ExtractVariable); + return this.sendCommand(JSON.stringify(command)); } extractMethod(document: vscode.TextDocument, name: string, filePath: string, range: vscode.Range, options?: vscode.TextEditorOptions): Promise { if (!options) { @@ -88,35 +90,26 @@ export class RefactorProxy extends vscode.Disposable { "name": name, "indent_size": options.tabSize }; - return this.sendCommand(JSON.stringify(command), REFACTOR.ExtractMethod); + return this.sendCommand(JSON.stringify(command)); } - private sendCommand(command: string, telemetryEvent: string): Promise { - let timer = new Delays(); + private sendCommand(command: string, telemetryEvent?: string): Promise { return this.initialize(this.pythonSettings.pythonPath).then(() => { return new Promise((resolve, reject) => { this._commandResolve = resolve; this._commandReject = reject; this._process.stdin.write(command + '\n'); }); - }).then(value => { - timer.stop(); - sendTelemetryEvent(telemetryEvent, null, timer.toMeasures()); - return value; - }).catch(reason => { - timer.stop(); - sendTelemetryEvent(telemetryEvent, null, timer.toMeasures()); - return Promise.reject(reason); }); } private initialize(pythonPath: string): Promise { return new Promise((resolve, reject) => { this._initializeReject = reject; let environmentVariables = { 'PYTHONUNBUFFERED': '1' }; - for (let setting in process.env) { - if (!environmentVariables[setting]) { - environmentVariables[setting] = process.env[setting]; - } + let customEnvironmentVars = getCustomEnvVarsSync(vscode.Uri.file(this.workspaceRoot)); + if (customEnvironmentVars) { + environmentVariables = mergeEnvVariables(environmentVariables, customEnvironmentVars); } + environmentVariables = mergeEnvVariables(environmentVariables); this._process = child_process.spawn(pythonPath, ['refactor.py', this.workspaceRoot], { cwd: path.join(this._extensionDir, 'pythonFiles'), @@ -142,7 +135,7 @@ export class RefactorProxy extends vscode.Disposable { // Possible there was an exception in parsing the data returned // So append the data then parse it let dataStr = this._previousStdErrData = this._previousStdErrData + data + ''; - let errorResponse: { message: string, traceback: string }[]; + let errorResponse: { message: string, traceback: string, type: string }[]; try { errorResponse = dataStr.split(/\r?\n/g).filter(line => line.length > 0).map(resp => JSON.parse(resp)); this._previousStdErrData = ''; @@ -152,7 +145,6 @@ export class RefactorProxy extends vscode.Disposable { // Possible we've only received part of the data, hence don't clear previousData return; } - if (typeof errorResponse[0].message !== 'string' || errorResponse[0].message.length === 0) { errorResponse[0].message = errorResponse[0].traceback.split(/\r?\n/g).pop(); } @@ -162,6 +154,11 @@ export class RefactorProxy extends vscode.Disposable { this._commandReject(`Refactor failed. ${errorMessage}`); } else { + if (typeof errorResponse[0].type === 'string' && errorResponse[0].type === 'ModuleNotFoundError') { + this._initializeReject('Not installed'); + return; + } + this._initializeReject(`Refactor failed. ${errorMessage}`); } } @@ -190,4 +187,4 @@ export class RefactorProxy extends vscode.Disposable { this._commandResolve(response[0]); this._commandResolve = null; } -} \ No newline at end of file +} diff --git a/src/client/sortImports.ts b/src/client/sortImports.ts index f525f64e3533..9ba64f0a0dbf 100644 --- a/src/client/sortImports.ts +++ b/src/client/sortImports.ts @@ -2,8 +2,6 @@ import * as vscode from 'vscode'; import * as sortProvider from './providers/importSortProvider'; -import * as telemetryHelper from './common/telemetry'; -import * as telemetryContracts from './common/telemetryContracts'; import * as os from 'os'; export function activate(context: vscode.ExtensionContext, outChannel: vscode.OutputChannel) { @@ -18,8 +16,6 @@ export function activate(context: vscode.ExtensionContext, outChannel: vscode.Ou return Promise.resolve(); } - let delays = new telemetryHelper.Delays(); - // Hack, if the document doesn't contain an empty line at the end, then add it // Else the library strips off the last line const lastLine = activeEditor.document.lineAt(activeEditor.document.lineCount - 1); @@ -41,9 +37,6 @@ export function activate(context: vscode.ExtensionContext, outChannel: vscode.Ou return activeEditor.edit(builder => { changes.forEach(change => builder.replace(change.range, change.newText)); }); - }).then(() => { - delays.stop(); - telemetryHelper.sendTelemetryEvent(telemetryContracts.Commands.SortImports, null, delays.toMeasures()); }).catch(error => { let message = typeof error === 'string' ? error : (error.message ? error.message : error); outChannel.appendLine(error); @@ -53,4 +46,4 @@ export function activate(context: vscode.ExtensionContext, outChannel: vscode.Ou }); context.subscriptions.push(disposable); -} \ No newline at end of file +} diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts new file mode 100644 index 000000000000..604ca766f8fe --- /dev/null +++ b/src/client/telemetry/constants.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +export const COMPLETION = 'COMPLETION'; +export const DEFINITION = 'DEFINITION'; +export const HOVER_DEFINITION = 'HOVER_DEFINITION'; +export const REFERENCE = 'REFERENCE'; +export const SIGNATURE = 'SIGNATURE'; +export const SYMBOL = 'SYMBOL'; +export const FORMAT_SORT_IMPORTS = 'FORMAT.SORT_IMPORTS'; +export const FORMAT = 'FORMAT.FORMAT'; +export const EDITOR_LOAD = 'EDITOR.LOAD'; +export const LINTING = 'LINTING'; +export const GO_TO_OBJECT_DEFINITION = 'GO_TO_OBJECT_DEFINITION'; +export const UPDATE_PYSPARK_LIBRARY = 'UPDATE_PYSPARK_LIBRARY'; +export const REFACTOR_RENAME = 'REFACTOR_RENAME'; +export const REFACTOR_EXTRACT_VAR = 'REFACTOR_EXTRACT_VAR'; +export const REFACTOR_EXTRACT_FUNCTION = 'REFACTOR_EXTRACT_FUNCTION'; +export const REPL = 'REPL'; +export const PYTHON_INTERPRETER = 'PYTHON_INTERPRETER'; +export const WORKSPACE_SYMBOLS_BUILD = 'WORKSPACE_SYMBOLS.BUILD'; +export const WORKSPACE_SYMBOLS_GO_TO = 'WORKSPACE_SYMBOLS.GO_TO'; +export const EXECUTION_CODE = 'EXECUTION_CODE'; +export const EXECUTION_DJANGO = 'EXECUTION_DJANGO'; +export const DEBUGGER = 'DEBUGGER'; +export const UNITTEST_STOP = 'UNITTEST.STOP'; +export const UNITTEST_RUN = 'UNITTEST.RUN'; +export const UNITTEST_DISCOVER = 'UNITTEST.DISCOVER'; +export const UNITTEST_VIEW_OUTPUT = 'UNITTEST.VIEW_OUTPUT'; +export const FEEDBACK = 'FEEDBACK'; diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts new file mode 100644 index 000000000000..05478ee21912 --- /dev/null +++ b/src/client/telemetry/index.ts @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { commands } from 'vscode'; +import { StopWatch } from './stopWatch'; +import { getTelemetryReporter } from './telemetry'; +import { TelemetryProperties } from './types'; + +export function sendTelemetryEvent(eventName: string, durationMs?: number, properties?: TelemetryProperties) { + const reporter = getTelemetryReporter(); + const measures = typeof durationMs === 'number' ? { duration: durationMs } : undefined; + + // tslint:disable-next-line:no-any + const customProperties: { [key: string]: string } = {}; + if (properties) { + // tslint:disable-next-line:prefer-type-cast no-any + const data = properties as any; + Object.getOwnPropertyNames(data).forEach(prop => { + // tslint:disable-next-line:prefer-type-cast no-any no-unsafe-any + (customProperties as any)[prop] = typeof data[prop] === 'string' ? data[prop] : data[prop].toString(); + }); + } + commands.executeCommand('python.updateFeedbackCounter', eventName); + reporter.sendTelemetryEvent(eventName, properties ? customProperties : undefined, measures); +} + +// tslint:disable-next-line:no-any function-name +export function captureTelemetry(eventName: string, properties?: TelemetryProperties) { + // tslint:disable-next-line:no-function-expression no-any + return function (target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor) { + const originalMethod = descriptor.value; + // tslint:disable-next-line:no-function-expression no-any + descriptor.value = function (...args: any[]) { + const stopWatch = new StopWatch(); + // tslint:disable-next-line:no-invalid-this no-use-before-declare no-unsafe-any + const result = originalMethod.apply(this, args); + + // If method being wrapped returns a promise then wait for it. + // tslint:disable-next-line:no-unsafe-any + if (result && typeof result.then === 'function' && typeof result.catch === 'function') { + // tslint:disable-next-line:prefer-type-cast + (result as Promise) + .then(data => { + sendTelemetryEvent(eventName, stopWatch.elapsedTime, properties); + return data; + }) + // tslint:disable-next-line:promise-function-async + .catch(ex => { + sendTelemetryEvent(eventName, stopWatch.elapsedTime, properties); + return Promise.reject(ex); + }); + } else { + sendTelemetryEvent(eventName, stopWatch.elapsedTime, properties); + } + + return result; + }; + + return descriptor; + }; +} + +// tslint:disable-next-line:no-any function-name +export function sendTelemetryWhenDone(eventName: string, promise: Promise | Thenable, + stopWatch?: StopWatch, properties?: TelemetryProperties) { + stopWatch = stopWatch ? stopWatch : new StopWatch(); + if (typeof promise.then === 'function') { + // tslint:disable-next-line:prefer-type-cast no-any + (promise as Promise) + .then(data => { + // tslint:disable-next-line:no-non-null-assertion + sendTelemetryEvent(eventName, stopWatch!.elapsedTime, properties); + return data; + // tslint:disable-next-line:promise-function-async + }, ex => { + // tslint:disable-next-line:no-non-null-assertion + sendTelemetryEvent(eventName, stopWatch!.elapsedTime, properties); + return Promise.reject(ex); + }); + } else { + throw new Error('Method is neither a Promise nor a Theneable'); + } +} diff --git a/src/client/telemetry/stopWatch.ts b/src/client/telemetry/stopWatch.ts new file mode 100644 index 000000000000..7b29fdc763fc --- /dev/null +++ b/src/client/telemetry/stopWatch.ts @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +export class StopWatch { + private started: number = Date.now(); + private stopped?: number; + public get elapsedTime() { + return (this.stopped ? this.stopped : Date.now()) - this.started; + } + public stop() { + this.stopped = Date.now(); + } +} diff --git a/src/client/telemetry/telemetry.ts b/src/client/telemetry/telemetry.ts new file mode 100644 index 000000000000..b208b0a220fc --- /dev/null +++ b/src/client/telemetry/telemetry.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable-next-line:no-reference +/// +import { extensions } from 'vscode'; +// tslint:disable-next-line:import-name +import TelemetryReporter from 'vscode-extension-telemetry'; + +// tslint:disable-next-line:no-any +let telemetryReporter: TelemetryReporter; +export function getTelemetryReporter() { + if (telemetryReporter) { + return telemetryReporter; + } + const extensionId = 'ms-python.python'; + // tslint:disable-next-line:no-non-null-assertion + const extension = extensions.getExtension(extensionId)!; + // tslint:disable-next-line:no-unsafe-any + const extensionVersion = extension.packageJSON.version; + // tslint:disable-next-line:no-unsafe-any + const aiKey = extension.packageJSON.contributes.debuggers[0].aiKey; + + // tslint:disable-next-line:no-unsafe-any + return telemetryReporter = new TelemetryReporter(extensionId, extensionVersion, aiKey); +} diff --git a/src/client/telemetry/types.ts b/src/client/telemetry/types.ts new file mode 100644 index 000000000000..ab689e7632d1 --- /dev/null +++ b/src/client/telemetry/types.ts @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export type EditorLoadTelemetry = { + condaVersion: string; +}; +export type FormatTelemetry = { + tool: 'autoppep8' | 'yapf'; + hasCustomArgs: boolean; + formatSelection: boolean; +}; +export type LintingTelemetry = { + tool: 'flake8' | 'mypy' | 'pep8' | 'prospector' | 'pydocstyle' | 'pylama' | 'pylint'; + hasCustomArgs: boolean; + trigger: 'save' | 'auto'; + executableSpecified: boolean; +}; +export type PythonInterpreterTelemetry = { + trigger: 'ui' | 'shebang' | 'load'; + failed: boolean; + version: string; + pipVersion: string; +}; +export type CodeExecutionTelemetry = { + scope: 'file' | 'selection'; +}; +export type DebuggerTelemetry = { + trigger: 'launch' | 'attach' + console?: 'none' | 'integratedTerminal' | 'externalTerminal'; + debugOptions?: string; + pyspark?: boolean; + hasEnvVars?: boolean; +}; +export type TestRunTelemetry = { + tool: 'nosetest' | 'pytest' | 'unittest' + scope: 'currentFile' | 'all' | 'file' | 'class' | 'function' | 'failed'; + debugging: boolean; + trigger: 'ui' | 'codelens' | 'commandpalette' | 'auto'; + failed: boolean; +}; +export type TestDiscoverytTelemetry = { + tool: 'nosetest' | 'pytest' | 'unittest' + trigger: 'ui' | 'commandpalette'; + failed: boolean; +}; +export type FeedbackTelemetry = { + action: 'accepted' | 'dismissed' | 'doNotShowAgain'; +}; +export type TelemetryProperties = FormatTelemetry | LintingTelemetry | EditorLoadTelemetry | PythonInterpreterTelemetry | CodeExecutionTelemetry | TestRunTelemetry | TestDiscoverytTelemetry | FeedbackTelemetry; diff --git a/src/client/typings/vscode-extension-telemetry/vscode-extension-telemetry.d.ts b/src/client/telemetry/vscode-extension-telemetry.d.ts similarity index 56% rename from src/client/typings/vscode-extension-telemetry/vscode-extension-telemetry.d.ts rename to src/client/telemetry/vscode-extension-telemetry.d.ts index 8b3e01df591a..6a53430a0f28 100644 --- a/src/client/typings/vscode-extension-telemetry/vscode-extension-telemetry.d.ts +++ b/src/client/telemetry/vscode-extension-telemetry.d.ts @@ -1,25 +1,16 @@ -declare module "vscode-extension-telemetry" { - export default class TelemetryReporter { - private extensionId; - private extensionVersion; - private appInsightsClient; - private commonProperties; - private static SQM_KEY; - private static REGISTRY_USERID_VALUE; - private static REGISTRY_MACHINEID_VALUE; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +declare module 'vscode-extension-telemetry' { + export default class TelemetryReporter { /** * Constructs a new telemetry reporter * @param {string} extensionId All events will be prefixed with this event name * @param {string} extensionVersion Extension version to be reported with each event * @param {string} key The application insights key */ + // tslint:disable-next-line:no-empty constructor(extensionId: string, extensionVersion: string, key: string); - private setupAIClient(client); - private loadVSCodeCommonProperties(machineId, sessionId, version); - private loadCommonProperties(); - private addCommonProperties(properties); - private getWinRegKeyData(key, name, hive, callback); /** * Sends a telemetry event @@ -27,10 +18,12 @@ declare module "vscode-extension-telemetry" { * @param {object} properties An associative array of strings * @param {object} measures An associative array of numbers */ - sendTelemetryEvent(eventName: string, properties?: { + // tslint:disable-next-line:member-access + public sendTelemetryEvent(eventName: string, properties?: { [key: string]: string; }, measures?: { [key: string]: number; + // tslint:disable-next-line:no-empty }): void; } -} \ No newline at end of file +} diff --git a/src/client/typings/node.d.ts b/src/client/typings/node.d.ts deleted file mode 100644 index 90d55c6f4c08..000000000000 --- a/src/client/typings/node.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -/// -/// -/// \ No newline at end of file diff --git a/src/client/typings/vscode-typings.d.ts b/src/client/typings/vscode-typings.d.ts deleted file mode 100644 index a9c71567c847..000000000000 --- a/src/client/typings/vscode-typings.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// \ No newline at end of file diff --git a/src/client/unittests/codeLenses/main.ts b/src/client/unittests/codeLenses/main.ts index ca77574c75ca..3efd30091227 100644 --- a/src/client/unittests/codeLenses/main.ts +++ b/src/client/unittests/codeLenses/main.ts @@ -1,15 +1,17 @@ import * as vscode from 'vscode'; import * as constants from '../../common/constants'; +import { PythonSymbolProvider } from '../../providers/symbolProvider'; +import { ITestCollectionStorageService } from '../common/types'; +import { TestFileCodeLensProvider } from './testFiles'; -import {TestFileCodeLensProvider} from './testFiles'; +export function activateCodeLenses(onDidChange: vscode.EventEmitter, + symboldProvider: PythonSymbolProvider, testCollectionStorage: ITestCollectionStorageService): vscode.Disposable { -export function activateCodeLenses(): vscode.Disposable { const disposables: vscode.Disposable[] = []; - disposables.push(vscode.languages.registerCodeLensProvider(constants.PythonLanguage, new TestFileCodeLensProvider())); + const codeLensProvider = new TestFileCodeLensProvider(onDidChange, symboldProvider, testCollectionStorage); + disposables.push(vscode.languages.registerCodeLensProvider(constants.PythonLanguage, codeLensProvider)); return { - dispose: function () { - disposables.forEach(d => d.dispose()); - } + dispose: () => { disposables.forEach(d => d.dispose()); } }; -} \ No newline at end of file +} diff --git a/src/client/unittests/codeLenses/testFiles.ts b/src/client/unittests/codeLenses/testFiles.ts index f93c81eb822d..10f115148594 100644 --- a/src/client/unittests/codeLenses/testFiles.ts +++ b/src/client/unittests/codeLenses/testFiles.ts @@ -1,174 +1,236 @@ 'use strict'; -import * as vscode from 'vscode'; -import { CodeLensProvider, TextDocument, CancellationToken, CodeLens, SymbolInformation } from 'vscode'; -import { TestFile, TestsToRun, TestSuite, TestFunction } from '../common/contracts'; +import { CancellationToken, CancellationTokenSource, CodeLens, CodeLensProvider, Event, EventEmitter, Position, Range, SymbolInformation, SymbolKind, TextDocument, workspace } from 'vscode'; +import { Uri } from 'vscode'; import * as constants from '../../common/constants'; -import { getDiscoveredTests } from '../common/testUtils'; +import { PythonSymbolProvider } from '../../providers/symbolProvider'; +import { CommandSource } from '../common/constants'; +import { ITestCollectionStorageService, TestFile, TestFunction, TestStatus, TestsToRun, TestSuite } from '../common/types'; -interface CodeLensData { - symbolKind: vscode.SymbolKind; - symbolName: string; - fileName: string; -} -interface FunctionsAndSuites { +type FunctionsAndSuites = { functions: TestFunction[]; suites: TestSuite[]; -} +}; export class TestFileCodeLensProvider implements CodeLensProvider { - public provideCodeLenses(document: TextDocument, token: CancellationToken): Thenable { - let testItems = getDiscoveredTests(); + // tslint:disable-next-line:variable-name + constructor(private _onDidChange: EventEmitter, + private symbolProvider: PythonSymbolProvider, + private testCollectionStorage: ITestCollectionStorageService) { + } + + get onDidChangeCodeLenses(): Event { + return this._onDidChange.event; + } + + public async provideCodeLenses(document: TextDocument, token: CancellationToken) { + const wkspace = workspace.getWorkspaceFolder(document.uri); + if (!wkspace) { + return []; + } + const testItems = this.testCollectionStorage.getTests(wkspace.uri); if (!testItems || testItems.testFiles.length === 0 || testItems.testFunctions.length === 0) { - return Promise.resolve([]); + return []; } - let cancelTokenSrc = new vscode.CancellationTokenSource(); + const cancelTokenSrc = new CancellationTokenSource(); token.onCancellationRequested(() => { cancelTokenSrc.cancel(); }); // Strop trying to build the code lenses if unable to get a list of - // symbols in this file afrer x time + // symbols in this file afrer x time. setTimeout(() => { if (!cancelTokenSrc.token.isCancellationRequested) { cancelTokenSrc.cancel(); } }, constants.Delays.MaxUnitTestCodeLensDelay); - return getCodeLenses(document.uri, token); + return this.getCodeLenses(document, token, this.symbolProvider); } - resolveCodeLens(codeLens: CodeLens, token: CancellationToken): CodeLens | Thenable { + public resolveCodeLens(codeLens: CodeLens, token: CancellationToken): CodeLens | Thenable { codeLens.command = { command: 'python.runtests', title: 'Test' }; return Promise.resolve(codeLens); } -} -function getCodeLenses(documentUri: vscode.Uri, token: vscode.CancellationToken): Thenable { - const tests = getDiscoveredTests(); - if (!tests) { - return null; - } - const file = tests.testFiles.find(file => file.fullPath === documentUri.fsPath); - if (!file) { - return Promise.resolve([]); + private async getCodeLenses(document: TextDocument, token: CancellationToken, symbolProvider: PythonSymbolProvider) { + const wkspace = workspace.getWorkspaceFolder(document.uri); + if (!wkspace) { + return []; + } + const tests = this.testCollectionStorage.getTests(wkspace.uri); + if (!tests) { + return []; + } + const file = tests.testFiles.find(item => item.fullPath === document.uri.fsPath); + if (!file) { + return []; + } + const allFuncsAndSuites = getAllTestSuitesAndFunctionsPerFile(file); + + return symbolProvider.provideDocumentSymbolsForInternalUse(document, token) + .then((symbols: SymbolInformation[]) => { + return symbols.filter(symbol => { + return symbol.kind === SymbolKind.Function || + symbol.kind === SymbolKind.Method || + symbol.kind === SymbolKind.Class; + }).map(symbol => { + // This is bloody crucial, if the start and end columns are the same + // then vscode goes bonkers when ever you edit a line (start scrolling magically). + const range = new Range(symbol.location.range.start, + new Position(symbol.location.range.end.line, + symbol.location.range.end.character + 1)); + + return this.getCodeLens(document.uri, allFuncsAndSuites, + range, symbol.name, symbol.kind, symbol.containerName); + }).reduce((previous, current) => previous.concat(current), []).filter(codeLens => codeLens !== null); + }, reason => { + if (token.isCancellationRequested) { + return []; + } + return Promise.reject(reason); + }); } - const allFuncsAndSuites = getAllTestSuitesAndFunctionsPerFile(file); - - return vscode.commands.executeCommand('vscode.executeDocumentSymbolProvider', documentUri, token) - .then((symbols: vscode.SymbolInformation[]) => { - return symbols.filter(symbol => { - return symbol.kind === vscode.SymbolKind.Function || - symbol.kind === vscode.SymbolKind.Method || - symbol.kind === vscode.SymbolKind.Class; - }).map(symbol => { - // This is bloody crucial, if the start and end columns are the same - // then vscode goes bonkers when ever you edit a line (start scrolling magically) - const range = new vscode.Range(symbol.location.range.start, - new vscode.Position(symbol.location.range.end.line, - symbol.location.range.end.character + 1)); - - return getCodeLens(documentUri.fsPath, allFuncsAndSuites, - range, symbol.name, symbol.kind); - }).reduce((previous, current) => previous.concat(current), []).filter(codeLens => codeLens !== null); - }, reason => { - if (token.isCancellationRequested) { + + private getCodeLens(file: Uri, allFuncsAndSuites: FunctionsAndSuites, + range: Range, symbolName: string, symbolKind: SymbolKind, symbolContainer: string): CodeLens[] { + + switch (symbolKind) { + case SymbolKind.Function: + case SymbolKind.Method: { + return getFunctionCodeLens(file, allFuncsAndSuites, symbolName, range, symbolContainer); + } + case SymbolKind.Class: { + const cls = allFuncsAndSuites.suites.find(item => item.name === symbolName); + if (!cls) { + return null; + } + return [ + new CodeLens(range, { + title: getTestStatusIcon(cls.status) + constants.Text.CodeLensRunUnitTest, + command: constants.Commands.Tests_Run, + arguments: [CommandSource.codelens, file, { testSuite: [cls] }] + }), + new CodeLens(range, { + title: getTestStatusIcon(cls.status) + constants.Text.CodeLensDebugUnitTest, + command: constants.Commands.Tests_Debug, + arguments: [CommandSource.codelens, file, { testSuite: [cls] }] + }) + ]; + } + default: { return []; } - return Promise.reject(reason); - }); + } + } } -// Move all of this rubbis into a separate file // too long -const testParametirizedFunction = /.*\[.*\]/g; - -function getCodeLens(fileName: string, allFuncsAndSuites: FunctionsAndSuites, - range: vscode.Range, symbolName: string, symbolKind: vscode.SymbolKind): vscode.CodeLens[] { - - switch (symbolKind) { - case vscode.SymbolKind.Function: - case vscode.SymbolKind.Method: { - return getFunctionCodeLens(fileName, allFuncsAndSuites, symbolName, range); +function getTestStatusIcon(status?: TestStatus): string { + switch (status) { + case TestStatus.Pass: { + return 'โœ” '; } - case vscode.SymbolKind.Class: { - const cls = allFuncsAndSuites.suites.find(cls => cls.name === symbolName); - if (!cls) { - return null; - } - return [ - new CodeLens(range, { - title: constants.Text.CodeLensRunUnitTest, - command: constants.Commands.Tests_Run, - arguments: [{ testSuite: [cls] }] - }), - new CodeLens(range, { - title: constants.Text.CodeLensDebugUnitTest, - command: constants.Commands.Tests_Debug, - arguments: [{ testSuite: [cls] }] - }) - ]; + case TestStatus.Error: + case TestStatus.Fail: { + return 'โœ˜ '; + } + case TestStatus.Skipped: { + return 'โƒ  '; + } + default: { + return ''; } } +} - return null; +function getTestStatusIcons(fns: TestFunction[]): string { + const statuses: string[] = []; + let count = fns.filter(fn => fn.status === TestStatus.Pass).length; + if (count > 0) { + statuses.push(`โœ” ${count}`); + } + count = fns.filter(fn => fn.status === TestStatus.Error || fn.status === TestStatus.Fail).length; + if (count > 0) { + statuses.push(`โœ˜ ${count}`); + } + count = fns.filter(fn => fn.status === TestStatus.Skipped).length; + if (count > 0) { + statuses.push(`โƒ  ${count}`); + } + + return statuses.join(' '); } +function getFunctionCodeLens(file: Uri, functionsAndSuites: FunctionsAndSuites, + symbolName: string, range: Range, symbolContainer: string): CodeLens[] { -function getFunctionCodeLens(filePath: string, functionsAndSuites: FunctionsAndSuites, - symbolName: string, range: vscode.Range): vscode.CodeLens[] { + let fn: TestFunction | undefined; + if (symbolContainer.length === 0) { + fn = functionsAndSuites.functions.find(func => func.name === symbolName); + } else { + // Assume single levels for now. + functionsAndSuites.suites + .filter(s => s.name === symbolContainer) + .forEach(s => { + const f = s.functions.find(item => item.name === symbolName); + if (f) { + fn = f; + } + }); + } - const fn = functionsAndSuites.functions.find(fn => fn.name === symbolName); if (fn) { return [ new CodeLens(range, { - title: constants.Text.CodeLensRunUnitTest, + title: getTestStatusIcon(fn.status) + constants.Text.CodeLensRunUnitTest, command: constants.Commands.Tests_Run, - arguments: [{ testFunction: [fn] }] + arguments: [CommandSource.codelens, file, { testFunction: [fn] }] }), new CodeLens(range, { - title: constants.Text.CodeLensDebugUnitTest, + title: getTestStatusIcon(fn.status) + constants.Text.CodeLensDebugUnitTest, command: constants.Commands.Tests_Debug, - arguments: [{ testFunction: [fn] }] + arguments: [CommandSource.codelens, file, { testFunction: [fn] }] }) ]; } - // Ok, possible we're dealing with parameterized unit tests - // If we have [ in the name, then this is a parameterized function - let functions = functionsAndSuites.functions.filter(fn => fn.name.startsWith(symbolName + '[') && fn.name.endsWith(']')); + // Ok, possible we're dealing with parameterized unit tests. + // If we have [ in the name, then this is a parameterized function. + const functions = functionsAndSuites.functions.filter(func => func.name.startsWith(`${symbolName}[`) && func.name.endsWith(']')); if (functions.length === 0) { - return null; + return []; } if (functions.length === 0) { return [ new CodeLens(range, { title: constants.Text.CodeLensRunUnitTest, command: constants.Commands.Tests_Run, - arguments: [{ testFunction: functions }] + arguments: [CommandSource.codelens, file, { testFunction: functions }] }), new CodeLens(range, { title: constants.Text.CodeLensDebugUnitTest, command: constants.Commands.Tests_Debug, - arguments: [{ testFunction: functions }] + arguments: [CommandSource.codelens, file, { testFunction: functions }] }) ]; } - // Find all flattened functions + // Find all flattened functions. return [ new CodeLens(range, { - title: constants.Text.CodeLensRunUnitTest + ' (Multiple)', + title: `${getTestStatusIcons(functions)}${constants.Text.CodeLensRunUnitTest} (Multiple)`, command: constants.Commands.Tests_Picker_UI, - arguments: [filePath, functions] + arguments: [CommandSource.codelens, file, functions] }), new CodeLens(range, { - title: constants.Text.CodeLensDebugUnitTest + ' (Multiple)', + title: `${getTestStatusIcons(functions)}${constants.Text.CodeLensDebugUnitTest} (Multiple)`, command: constants.Commands.Tests_Picker_UI_Debug, - arguments: [filePath, functions] + arguments: [CommandSource.codelens, file, functions] }) ]; } function getAllTestSuitesAndFunctionsPerFile(testFile: TestFile): FunctionsAndSuites { - const all = { functions: testFile.functions, suites: [] }; + // tslint:disable-next-line:prefer-type-cast + const all = { functions: testFile.functions, suites: [] as TestSuite[] }; testFile.suites.forEach(suite => { all.suites.push(suite); @@ -191,4 +253,4 @@ function getAllTestSuitesAndFunctions(testSuite: TestSuite): FunctionsAndSuites all.suites.push(...allChildItems.suites); }); return all; -} \ No newline at end of file +} diff --git a/src/client/unittests/common/baseTestManager.ts b/src/client/unittests/common/baseTestManager.ts index b54763cb4095..029f09fabc0e 100644 --- a/src/client/unittests/common/baseTestManager.ts +++ b/src/client/unittests/common/baseTestManager.ts @@ -1,35 +1,62 @@ // import {TestFolder, TestsToRun, Tests, TestFile, TestSuite, TestFunction, TestStatus, FlattenedTestFunction, FlattenedTestSuite, CANCELLATION_REASON} from './contracts'; -import { Tests, TestStatus, TestsToRun, CANCELLATION_REASON } from './contracts'; import * as vscode from 'vscode'; -import { resetTestResults, displayTestErrorMessage, storeDiscoveredTests } from './testUtils'; -import * as telemetryHelper from '../../common/telemetry'; -import * as telemetryContracts from '../../common/telemetryContracts'; -import { Installer, Product } from '../../common/installer'; +import { Uri, workspace } from 'vscode'; +import { IPythonSettings, PythonSettings } from '../../common/configSettings'; import { isNotInstalledError } from '../../common/helpers'; +import { Installer, Product } from '../../common/installer'; +import { UNITTEST_DISCOVER, UNITTEST_RUN } from '../../telemetry/constants'; +import { sendTelemetryEvent } from '../../telemetry/index'; +import { TestDiscoverytTelemetry, TestRunTelemetry } from '../../telemetry/types'; +import { CANCELLATION_REASON, CommandSource } from './constants'; +import { displayTestErrorMessage } from './testUtils'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, Tests, TestStatus, TestsToRun } from './types'; +enum CancellationTokenType { + testDiscovery, + testRunner +} +type TestProvider = 'nosetest' | 'pytest' | 'unittest'; export abstract class BaseTestManager { + public readonly workspace: Uri; + protected readonly settings: IPythonSettings; private tests: Tests; + // tslint:disable-next-line:variable-name private _status: TestStatus = TestStatus.Unknown; - private cancellationTokenSource: vscode.CancellationTokenSource; + private testDiscoveryCancellationTokenSource: vscode.CancellationTokenSource; + private testRunnerCancellationTokenSource: vscode.CancellationTokenSource; private installer: Installer; - protected get cancellationToken(): vscode.CancellationToken { - if (this.cancellationTokenSource) { - return this.cancellationTokenSource.token; - } + private discoverTestsPromise: Promise; + constructor(public readonly testProvider: TestProvider, private product: Product, protected rootDirectory: string, + protected outputChannel: vscode.OutputChannel, private testCollectionStorage: ITestCollectionStorageService, + protected testResultsService: ITestResultsService, protected testsHelper: ITestsHelper) { + this._status = TestStatus.Unknown; + this.installer = new Installer(); + this.settings = PythonSettings.getInstance(this.rootDirectory ? Uri.file(this.rootDirectory) : undefined); + this.workspace = workspace.getWorkspaceFolder(Uri.file(this.rootDirectory)).uri; + } + protected get testDiscoveryCancellationToken(): vscode.CancellationToken | undefined { + return this.testDiscoveryCancellationTokenSource ? this.testDiscoveryCancellationTokenSource.token : undefined; + } + protected get testRunnerCancellationToken(): vscode.CancellationToken | undefined { + return this.testRunnerCancellationTokenSource ? this.testRunnerCancellationTokenSource.token : undefined; } public dispose() { + this.stop(); } public get status(): TestStatus { return this._status; } + public get workingDirectory(): string { + const settings = PythonSettings.getInstance(vscode.Uri.file(this.rootDirectory)); + return settings.unitTest.cwd && settings.unitTest.cwd.length > 0 ? settings.unitTest.cwd : this.rootDirectory; + } public stop() { - if (this.cancellationTokenSource) { - this.cancellationTokenSource.cancel(); + if (this.testDiscoveryCancellationTokenSource) { + this.testDiscoveryCancellationTokenSource.cancel(); + } + if (this.testRunnerCancellationTokenSource) { + this.testRunnerCancellationTokenSource.cancel(); } - } - constructor(private testProvider: string, private product: Product, protected rootDirectory: string, protected outputChannel: vscode.OutputChannel) { - this._status = TestStatus.Unknown; - this.installer = new Installer(); } public reset() { this._status = TestStatus.Unknown; @@ -40,20 +67,9 @@ export abstract class BaseTestManager { return; } - resetTestResults(this.tests); + this.testResultsService.resetResults(this.tests); } - private createCancellationToken() { - this.disposeCancellationToken(); - this.cancellationTokenSource = new vscode.CancellationTokenSource(); - } - private disposeCancellationToken() { - if (this.cancellationTokenSource) { - this.cancellationTokenSource.dispose(); - } - this.cancellationTokenSource = null; - } - private discoverTestsPromise: Promise; - discoverTests(ignoreCache: boolean = false, quietMode: boolean = false): Promise { + public async discoverTests(cmdSource: CommandSource, ignoreCache: boolean = false, quietMode: boolean = false, userInitiated: boolean = false): Promise { if (this.discoverTestsPromise) { return this.discoverTestsPromise; } @@ -62,10 +78,21 @@ export abstract class BaseTestManager { this._status = TestStatus.Idle; return Promise.resolve(this.tests); } - let delays = new telemetryHelper.Delays(); this._status = TestStatus.Discovering; - this.createCancellationToken(); + // If ignoreCache is true, its an indication of the fact that its a user invoked operation. + // Hence we can stop the debugger. + if (userInitiated) { + this.stop(); + } + const telementryProperties: TestDiscoverytTelemetry = { + tool: this.testProvider, + // tslint:disable-next-line:no-any prefer-type-cast + trigger: cmdSource as any, + failed: false + }; + + this.createCancellationToken(CancellationTokenType.testDiscovery); return this.discoverTestsPromise = this.discoverTestsImpl(ignoreCache) .then(tests => { this.tests = tests; @@ -85,118 +112,139 @@ export abstract class BaseTestManager { } }); if (haveErrorsInDiscovering && !quietMode) { - displayTestErrorMessage('There were some errors in disovering unit tests'); + displayTestErrorMessage('There were some errors in discovering unit tests'); } - storeDiscoveredTests(tests); - this.disposeCancellationToken(); - - delays.stop(); - telemetryHelper.sendTelemetryEvent(telemetryContracts.UnitTests.Discover, { - Test_Provider: this.testProvider - }, delays.toMeasures()); - + const wkspace = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(this.rootDirectory)).uri; + this.testCollectionStorage.storeTests(wkspace, tests); + this.disposeCancellationToken(CancellationTokenType.testDiscovery); + sendTelemetryEvent(UNITTEST_DISCOVER, undefined, telementryProperties); return tests; }).catch(reason => { if (isNotInstalledError(reason) && !quietMode) { - this.installer.promptToInstall(this.product); + // tslint:disable-next-line:no-floating-promises + this.installer.promptToInstall(this.product, this.workspace); } this.tests = null; this.discoverTestsPromise = null; - if (this.cancellationToken && this.cancellationToken.isCancellationRequested) { + if (this.testDiscoveryCancellationToken && this.testDiscoveryCancellationToken.isCancellationRequested) { reason = CANCELLATION_REASON; this._status = TestStatus.Idle; - } - else { + } else { + telementryProperties.failed = true; + sendTelemetryEvent(UNITTEST_DISCOVER, undefined, telementryProperties); this._status = TestStatus.Error; - this.outputChannel.appendLine('Test Disovery failed: '); + this.outputChannel.appendLine('Test Discovery failed: '); + // tslint:disable-next-line:prefer-template this.outputChannel.appendLine('' + reason); } - storeDiscoveredTests(null); - this.disposeCancellationToken(); - - delays.stop(); - telemetryHelper.sendTelemetryEvent(telemetryContracts.UnitTests.Discover, { - Test_Provider: this.testProvider - }, delays.toMeasures()); - + const wkspace = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(this.rootDirectory)).uri; + this.testCollectionStorage.storeTests(wkspace, null); + this.disposeCancellationToken(CancellationTokenType.testDiscovery); return Promise.reject(reason); }); } - abstract discoverTestsImpl(ignoreCache: boolean, debug?: boolean): Promise; - public runTest(testsToRun?: TestsToRun, debug?: boolean): Promise; - public runTest(runFailedTests?: boolean, debug?: boolean): Promise; - public runTest(args: any, debug?: boolean): Promise { - let runFailedTests = false; - let testsToRun: TestsToRun = null; - let moreInfo = { + public runTest(cmdSource: CommandSource, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { + const moreInfo = { Test_Provider: this.testProvider, Run_Failed_Tests: 'false', Run_Specific_File: 'false', Run_Specific_Class: 'false', Run_Specific_Function: 'false' }; - - if (typeof args === 'boolean') { - runFailedTests = args === true; + const telementryProperties: TestRunTelemetry = { + tool: this.testProvider, + scope: 'all', + debugging: debug === true, + trigger: cmdSource, + failed: false + }; + if (runFailedTests === true) { + // tslint:disable-next-line:prefer-template moreInfo.Run_Failed_Tests = runFailedTests + ''; + telementryProperties.scope = 'failed'; } - if (typeof args === 'object' && args !== null) { - testsToRun = args; + if (testsToRun && typeof testsToRun === 'object') { if (Array.isArray(testsToRun.testFile) && testsToRun.testFile.length > 0) { + telementryProperties.scope = 'file'; moreInfo.Run_Specific_File = 'true'; } if (Array.isArray(testsToRun.testSuite) && testsToRun.testSuite.length > 0) { + telementryProperties.scope = 'class'; moreInfo.Run_Specific_Class = 'true'; } if (Array.isArray(testsToRun.testFunction) && testsToRun.testFunction.length > 0) { + telementryProperties.scope = 'function'; moreInfo.Run_Specific_Function = 'true'; } } + if (runFailedTests === false && testsToRun === null) { this.resetTestResults(); } - let delays = new telemetryHelper.Delays(); - this._status = TestStatus.Running; - this.createCancellationToken(); + if (this.testRunnerCancellationTokenSource) { + this.testRunnerCancellationTokenSource.cancel(); + } // If running failed tests, then don't clear the previously build UnitTests // If we do so, then we end up re-discovering the unit tests and clearing previously cached list of failed tests // Similarly, if running a specific test or test file, don't clear the cache (possible tests have some state information retained) const clearDiscoveredTestCache = runFailedTests || moreInfo.Run_Specific_File || moreInfo.Run_Specific_Class || moreInfo.Run_Specific_Function ? false : true; - return this.discoverTests(clearDiscoveredTestCache, true) + return this.discoverTests(cmdSource, clearDiscoveredTestCache, true, true) .catch(reason => { - if (this.cancellationToken && this.cancellationToken.isCancellationRequested) { - return Promise.reject(reason); + if (this.testDiscoveryCancellationToken && this.testDiscoveryCancellationToken.isCancellationRequested) { + return Promise.reject(reason); } displayTestErrorMessage('Errors in discovering tests, continuing with tests'); return { - rootTestFolders: [], testFiles: [], testFolders: [], testFunctions: [], testSuits: [], + rootTestFolders: [], testFiles: [], testFolders: [], testFunctions: [], testSuites: [], summary: { errors: 0, failures: 0, passed: 0, skipped: 0 } }; }) .then(tests => { + this.createCancellationToken(CancellationTokenType.testRunner); return this.runTestImpl(tests, testsToRun, runFailedTests, debug); }).then(() => { this._status = TestStatus.Idle; - this.disposeCancellationToken(); - delays.stop(); - telemetryHelper.sendTelemetryEvent(telemetryContracts.UnitTests.Run, moreInfo as any, delays.toMeasures()); + this.disposeCancellationToken(CancellationTokenType.testRunner); + sendTelemetryEvent(UNITTEST_RUN, undefined, telementryProperties); return this.tests; }).catch(reason => { - if (this.cancellationToken && this.cancellationToken.isCancellationRequested) { + if (this.testRunnerCancellationToken && this.testRunnerCancellationToken.isCancellationRequested) { reason = CANCELLATION_REASON; this._status = TestStatus.Idle; - } - else { + } else { this._status = TestStatus.Error; + telementryProperties.failed = true; + sendTelemetryEvent(UNITTEST_RUN, undefined, telementryProperties); } - this.disposeCancellationToken(); - delays.stop(); - telemetryHelper.sendTelemetryEvent(telemetryContracts.UnitTests.Run, moreInfo as any, delays.toMeasures()); - return Promise.reject(reason); + this.disposeCancellationToken(CancellationTokenType.testRunner); + return Promise.reject(reason); }); } - abstract runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise; -} \ No newline at end of file + // tslint:disable-next-line:no-any + protected abstract runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise; + protected abstract discoverTestsImpl(ignoreCache: boolean, debug?: boolean): Promise; + private createCancellationToken(tokenType: CancellationTokenType) { + this.disposeCancellationToken(tokenType); + if (tokenType === CancellationTokenType.testDiscovery) { + this.testDiscoveryCancellationTokenSource = new vscode.CancellationTokenSource(); + } else { + this.testRunnerCancellationTokenSource = new vscode.CancellationTokenSource(); + } + } + private disposeCancellationToken(tokenType: CancellationTokenType) { + if (tokenType === CancellationTokenType.testDiscovery) { + if (this.testDiscoveryCancellationTokenSource) { + this.testDiscoveryCancellationTokenSource.dispose(); + } + this.testDiscoveryCancellationTokenSource = null; + } else { + if (this.testRunnerCancellationTokenSource) { + this.testRunnerCancellationTokenSource.dispose(); + } + this.testRunnerCancellationTokenSource = null; + } + } +} diff --git a/src/client/unittests/common/configSettingService.ts b/src/client/unittests/common/configSettingService.ts new file mode 100644 index 000000000000..f6a90619f9fe --- /dev/null +++ b/src/client/unittests/common/configSettingService.ts @@ -0,0 +1,67 @@ +import { ConfigurationTarget, Uri, workspace, WorkspaceConfiguration } from 'vscode'; +import { Product } from '../../common/installer'; +import { ITestConfigSettingsService, UnitTestProduct } from './types'; + +export class TestConfigSettingsService implements ITestConfigSettingsService { + private static getTestArgSetting(product: UnitTestProduct) { + switch (product) { + case Product.unittest: + return 'unitTest.unittestArgs'; + case Product.pytest: + return 'unitTest.pyTestArgs'; + case Product.nosetest: + return 'unitTest.nosetestArgs'; + default: + throw new Error('Invalid Test Product'); + } + } + private static getTestEnablingSetting(product: UnitTestProduct) { + switch (product) { + case Product.unittest: + return 'unitTest.unittestEnabled'; + case Product.pytest: + return 'unitTest.pyTestEnabled'; + case Product.nosetest: + return 'unitTest.nosetestsEnabled'; + default: + throw new Error('Invalid Test Product'); + } + } + // tslint:disable-next-line:no-any + private static async updateSetting(testDirectory: string | Uri, setting: string, value: any) { + let pythonConfig: WorkspaceConfiguration; + let configTarget: ConfigurationTarget; + const resource = typeof testDirectory === 'string' ? Uri.file(testDirectory) : testDirectory; + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + configTarget = ConfigurationTarget.Workspace; + pythonConfig = workspace.getConfiguration('python'); + } else if (workspace.workspaceFolders.length === 1) { + configTarget = ConfigurationTarget.Workspace; + pythonConfig = workspace.getConfiguration('python', workspace.workspaceFolders[0].uri); + } else { + configTarget = ConfigurationTarget.Workspace; + const workspaceFolder = workspace.getWorkspaceFolder(resource); + if (!workspaceFolder) { + throw new Error(`Test directory does not belong to any workspace (${testDirectory})`); + } + // tslint:disable-next-line:no-non-null-assertion + pythonConfig = workspace.getConfiguration('python', workspaceFolder!.uri); + } + + return pythonConfig.update(setting, value); + } + public async updateTestArgs(testDirectory: string | Uri, product: UnitTestProduct, args: string[]) { + const setting = TestConfigSettingsService.getTestArgSetting(product); + return TestConfigSettingsService.updateSetting(testDirectory, setting, args); + } + + public async enable(testDirectory: string | Uri, product: UnitTestProduct): Promise { + const setting = TestConfigSettingsService.getTestEnablingSetting(product); + return TestConfigSettingsService.updateSetting(testDirectory, setting, true); + } + + public async disable(testDirectory: string | Uri, product: UnitTestProduct): Promise { + const setting = TestConfigSettingsService.getTestEnablingSetting(product); + return TestConfigSettingsService.updateSetting(testDirectory, setting, false); + } +} diff --git a/src/client/unittests/common/constants.ts b/src/client/unittests/common/constants.ts new file mode 100644 index 000000000000..9bd66948c6e3 --- /dev/null +++ b/src/client/unittests/common/constants.ts @@ -0,0 +1,7 @@ +export const CANCELLATION_REASON = 'cancelled_user_request'; +export enum CommandSource { + auto = 'auto', + ui = 'ui', + codelens = 'codelens', + commandPalette = 'commandpalette' +} diff --git a/src/client/unittests/common/contracts.ts b/src/client/unittests/common/contracts.ts deleted file mode 100644 index 02dc03c18980..000000000000 --- a/src/client/unittests/common/contracts.ts +++ /dev/null @@ -1,89 +0,0 @@ -export const CANCELLATION_REASON = 'cancelled_user_request'; - -export interface TestFolder extends TestResult { - name: string; - testFiles: TestFile[]; - nameToRun: string; - status?: TestStatus; - folders: TestFolder[]; -} -export interface TestFile extends TestResult { - name: string; - fullPath: string; - functions: TestFunction[]; - suites: TestSuite[]; - nameToRun: string; - xmlName: string; - status?: TestStatus; - errorsWhenDiscovering?: string; -} -export interface TestSuite extends TestResult { - name: string; - functions: TestFunction[]; - suites: TestSuite[]; - isUnitTest: Boolean; - isInstance: Boolean; - nameToRun: string; - xmlName: string; - status?: TestStatus; -} -export interface TestFunction extends TestResult { - name: string; - nameToRun: string; - status?: TestStatus; -} -export interface TestResult extends Node { - passed?: boolean; - time: number; - line?: number; - message?: string; - traceback?: string; - functionsPassed?: number; - functionsFailed?: number; - functionsDidNotRun?: number; -} -export interface Node { - expanded?: Boolean; -} -export interface FlattenedTestFunction { - testFunction: TestFunction; - parentTestSuite?: TestSuite; - parentTestFile: TestFile; - xmlClassName: string; -} -export interface FlattenedTestSuite { - testSuite: TestSuite; - parentTestSuite?: TestSuite; - parentTestFile: TestFile; - xmlClassName: string; -} -export interface TestSummary { - passed: number; - failures: number; - errors: number; - skipped: number; -} -export interface Tests { - summary: TestSummary; - testFiles: TestFile[]; - testFunctions: FlattenedTestFunction[]; - testSuits: FlattenedTestSuite[]; - testFolders: TestFolder[]; - rootTestFolders: TestFolder[]; -} -export enum TestStatus { - Unknown, - Discovering, - Idle, - Running, - Fail, - Error, - Skipped, - Pass -} -export interface TestsToRun { - testFolder?: TestFolder[]; - testFile?: TestFile[]; - testSuite?: TestSuite[]; - testFunction?: TestFunction[]; -} diff --git a/src/client/unittests/common/debugLauncher.ts b/src/client/unittests/common/debugLauncher.ts new file mode 100644 index 000000000000..783dbc24fd7c --- /dev/null +++ b/src/client/unittests/common/debugLauncher.ts @@ -0,0 +1,68 @@ +import * as os from 'os'; +import { CancellationToken, debug, OutputChannel, Uri, workspace } from 'vscode'; +import { PythonSettings } from '../../common/configSettings'; +import { createDeferred } from './../../common/helpers'; +import { execPythonFile } from './../../common/utils'; +import { ITestDebugLauncher } from './types'; + +export class DebugLauncher implements ITestDebugLauncher { + public async launchDebugger(rootDirectory: string, testArgs: string[], token?: CancellationToken, outChannel?: OutputChannel) { + const pythonSettings = PythonSettings.getInstance(rootDirectory ? Uri.file(rootDirectory) : undefined); + // tslint:disable-next-line:no-any + const def = createDeferred(); + // tslint:disable-next-line:no-any + const launchDef = createDeferred(); + let outputChannelShown = false; + execPythonFile(rootDirectory, pythonSettings.pythonPath, testArgs, rootDirectory, true, (data: string) => { + if (data.startsWith(`READY${os.EOL}`)) { + // debug socket server has started. + launchDef.resolve(); + data = data.substring((`READY${os.EOL}`).length); + } + + if (!outputChannelShown) { + outputChannelShown = true; + outChannel.show(); + } + outChannel.append(data); + }, token).catch(reason => { + if (!def.rejected && !def.resolved) { + def.reject(reason); + } + }).then(() => { + if (!def.rejected && !def.resolved) { + def.resolve(); + } + }).catch(reason => { + if (!def.rejected && !def.resolved) { + def.reject(reason); + } + }); + + launchDef.promise.then(() => { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + throw new Error('Please open a workspace'); + } + let workspaceFolder = workspace.getWorkspaceFolder(Uri.file(rootDirectory)); + if (!workspaceFolder) { + workspaceFolder = workspace.workspaceFolders[0]; + } + return debug.startDebugging(workspaceFolder, { + name: 'Debug Unit Test', + type: 'python', + request: 'attach', + localRoot: rootDirectory, + remoteRoot: rootDirectory, + port: pythonSettings.unitTest.debugPort, + secret: 'my_secret', + host: 'localhost' + }); + }).catch(reason => { + if (!def.rejected && !def.resolved) { + def.reject(reason); + } + }); + + return def.promise; + } +} diff --git a/src/client/unittests/common/runner.ts b/src/client/unittests/common/runner.ts index 0b4b328d4c3e..de3b90245e6e 100644 --- a/src/client/unittests/common/runner.ts +++ b/src/client/unittests/common/runner.ts @@ -1,44 +1,7 @@ -import { execPythonFile } from './../../common/utils'; import { CancellationToken, OutputChannel, window, workspace } from 'vscode'; -import { getPythonInterpreterDirectory, IS_WINDOWS, PATH_VARIABLE_NAME } from '../../common/utils'; - -let terminal = null; -export function run(file: string, args: string[], cwd: string, token?: CancellationToken, outChannel?: OutputChannel): Promise { - return execPythonFile(file, args, cwd, true, (data: string) => outChannel.append(data), token); +import { IS_WINDOWS, PATH_VARIABLE_NAME } from '../../common/utils'; +import { execPythonFile } from './../../common/utils'; - // Bug, we cannot resolve this - // Resolving here means that tests have completed - // We need a way to determine that the tests have completed succefully.. hmm - // We could use a hack, such as generating a textfile at the end of the command and monitoring.. hack hack hack - // Or we could generate a shell script file and embed all of the hacks in here... hack hack hack... - // return runTestInTerminal(file, args, cwd); +export async function run(file: string, args: string[], cwd: string, token?: CancellationToken, outChannel?: OutputChannel): Promise { + return execPythonFile(cwd, file, args, cwd, true, (data: string) => outChannel.append(data), token); } - -function runTestInTerminal(file: string, args: string[], cwd: string): Promise { - return getPythonInterpreterDirectory().then(pyPath => { - let commands = []; - if (IS_WINDOWS) { - commands.push(`set ${PATH_VARIABLE_NAME}=%${PATH_VARIABLE_NAME}%;${pyPath}`); - } - else { - commands.push(`export ${PATH_VARIABLE_NAME}=$${PATH_VARIABLE_NAME}:${pyPath}`); - } - if (cwd !== workspace.rootPath && typeof cwd === 'string') { - commands.push(`cd ${cwd}`); - } - commands.push(`${file} ${args.join(' ')}`); - terminal = window.createTerminal(`Python Test Log`); - - return new Promise((resolve) => { - setTimeout(function () { - terminal.show(); - terminal.sendText(commands.join(' && ')); - - // Bug, we cannot resolve this - // Resolving here means that tests have completed - // We need a way to determine that the tests have completed succefully.. hmm - resolve(); - }, 1000); - }); - }); -} \ No newline at end of file diff --git a/src/client/unittests/common/storageService.ts b/src/client/unittests/common/storageService.ts new file mode 100644 index 000000000000..775750de1fe9 --- /dev/null +++ b/src/client/unittests/common/storageService.ts @@ -0,0 +1,21 @@ +import { Uri, workspace } from 'vscode'; +import { ITestCollectionStorageService, Tests } from './types'; + +export class TestCollectionStorageService implements ITestCollectionStorageService { + private testsIndexedByWorkspaceUri = new Map(); + public getTests(wkspace: Uri): Tests | undefined { + const workspaceFolder = this.getWorkspaceFolderPath(wkspace) || ''; + return this.testsIndexedByWorkspaceUri.has(workspaceFolder) ? this.testsIndexedByWorkspaceUri.get(workspaceFolder) : undefined; + } + public storeTests(wkspace: Uri, tests: Tests | null | undefined): void { + const workspaceFolder = this.getWorkspaceFolderPath(wkspace) || ''; + this.testsIndexedByWorkspaceUri.set(workspaceFolder, tests); + } + public dispose() { + this.testsIndexedByWorkspaceUri.clear(); + } + private getWorkspaceFolderPath(resource: Uri): string | undefined { + const folder = workspace.getWorkspaceFolder(resource); + return folder ? folder.uri.path : undefined; + } +} diff --git a/src/client/unittests/common/testConfigurationManager.ts b/src/client/unittests/common/testConfigurationManager.ts index 8b031b7a63b2..afdad0192c87 100644 --- a/src/client/unittests/common/testConfigurationManager.ts +++ b/src/client/unittests/common/testConfigurationManager.ts @@ -1,14 +1,26 @@ +import * as path from 'path'; import * as vscode from 'vscode'; +import { Uri } from 'vscode'; import { createDeferred } from '../../common/helpers'; +import { Installer } from '../../common/installer'; import { getSubDirectories } from '../../common/utils'; -import * as path from 'path'; +import { ITestConfigSettingsService, UnitTestProduct } from './types'; export abstract class TestConfigurationManager { - public abstract enable(): Thenable; - public abstract disable(): Thenable; - constructor(protected readonly outputChannel: vscode.OutputChannel) { } - public abstract configure(rootDir: string): Promise; - + constructor(protected workspace: Uri, + protected product: UnitTestProduct, + protected readonly outputChannel: vscode.OutputChannel, + protected installer: Installer, + protected testConfigSettingsService: ITestConfigSettingsService) { } + // tslint:disable-next-line:no-any + public abstract configure(wkspace: Uri): Promise; + public async enable() { + return this.testConfigSettingsService.enable(this.workspace, this.product); + } + // tslint:disable-next-line:no-any + public async disable() { + return this.testConfigSettingsService.enable(this.workspace, this.product); + } protected selectTestDir(rootDir: string, subDirs: string[], customOptions: vscode.QuickPickItem[] = []): Promise { const options = { matchOnDescription: true, @@ -22,7 +34,7 @@ export abstract class TestConfigurationManager { } return { label: dirName, - description: '', + description: '' }; }).filter(item => item !== null); @@ -46,12 +58,12 @@ export abstract class TestConfigurationManager { matchOnDetail: true, placeHolder: 'Select the pattern to identify test files' }; - let items: vscode.QuickPickItem[] = [ - { label: '*test.py', description: `Python Files ending with 'test'` }, - { label: '*_test.py', description: `Python Files ending with '_test'` }, - { label: 'test*.py', description: `Python Files begining with 'test'` }, - { label: 'test_*.py', description: `Python Files begining with 'test_'` }, - { label: '*test*.py', description: `Python Files containing the word 'test'` } + const items: vscode.QuickPickItem[] = [ + { label: '*test.py', description: 'Python Files ending with \'test\'' }, + { label: '*_test.py', description: 'Python Files ending with \'_test\'' }, + { label: 'test*.py', description: 'Python Files begining with \'test\'' }, + { label: 'test_*.py', description: 'Python Files begining with \'test_\'' }, + { label: '*test*.py', description: 'Python Files containing the word \'test\'' } ]; const def = createDeferred(); @@ -65,17 +77,17 @@ export abstract class TestConfigurationManager { return def.promise; } - protected getTestDirs(rootDir): Promise { + protected getTestDirs(rootDir: string): Promise { return getSubDirectories(rootDir).then(subDirs => { subDirs.sort(); - // Find out if there are any dirs with the name test and place them on the top - let possibleTestDirs = subDirs.filter(dir => dir.match(/test/i)); - let nonTestDirs = subDirs.filter(dir => possibleTestDirs.indexOf(dir) === -1); + // Find out if there are any dirs with the name test and place them on the top. + const possibleTestDirs = subDirs.filter(dir => dir.match(/test/i)); + const nonTestDirs = subDirs.filter(dir => possibleTestDirs.indexOf(dir) === -1); possibleTestDirs.push(...nonTestDirs); - // The test dirs are now on top + // The test dirs are now on top. return possibleTestDirs; }); } -} \ No newline at end of file +} diff --git a/src/client/unittests/common/testManagerService.ts b/src/client/unittests/common/testManagerService.ts new file mode 100644 index 000000000000..87cfa5d2c1e4 --- /dev/null +++ b/src/client/unittests/common/testManagerService.ts @@ -0,0 +1,63 @@ +import { OutputChannel, Uri } from 'vscode'; +import { PythonSettings } from '../../common/configSettings'; +import { Product } from '../../common/installer'; +import { TestManager as NoseTestManager } from '../nosetest/main'; +import { TestManager as PyTestTestManager } from '../pytest/main'; +import { TestManager as UnitTestTestManager } from '../unittest/main'; +import { BaseTestManager } from './baseTestManager'; +import { ITestCollectionStorageService, ITestDebugLauncher, ITestManagerService, ITestResultsService, ITestsHelper, UnitTestProduct } from './types'; + +type TestManagerInstanceInfo = { instance?: BaseTestManager, create(rootDirectory: string): BaseTestManager }; + +export class TestManagerService implements ITestManagerService { + private testManagers = new Map(); + constructor(private wkspace: Uri, private outChannel: OutputChannel, + testCollectionStorage: ITestCollectionStorageService, testResultsService: ITestResultsService, + testsHelper: ITestsHelper, debugLauncher: ITestDebugLauncher) { + this.testManagers.set(Product.nosetest, { + create: (rootDirectory: string) => new NoseTestManager(rootDirectory, this.outChannel, testCollectionStorage, testResultsService, testsHelper, debugLauncher) + }); + this.testManagers.set(Product.pytest, { + create: (rootDirectory: string) => new PyTestTestManager(rootDirectory, this.outChannel, testCollectionStorage, testResultsService, testsHelper, debugLauncher) + }); + this.testManagers.set(Product.unittest, { + create: (rootDirectory: string) => new UnitTestTestManager(rootDirectory, this.outChannel, testCollectionStorage, testResultsService, testsHelper, debugLauncher) + }); + } + public dispose() { + this.testManagers.forEach(info => { + if (info.instance) { + info.instance.dispose(); + } + }); + } + public getTestManager(): BaseTestManager | undefined { + const preferredTestManager = this.getPreferredTestManager(); + if (typeof preferredTestManager !== 'number') { + return; + } + + // tslint:disable-next-line:no-non-null-assertion + const info = this.testManagers.get(preferredTestManager)!; + if (!info.instance) { + const testDirectory = this.getTestWorkingDirectory(); + info.instance = info.create(testDirectory); + } + return info.instance; + } + public getTestWorkingDirectory() { + const settings = PythonSettings.getInstance(this.wkspace); + return settings.unitTest.cwd && settings.unitTest.cwd.length > 0 ? settings.unitTest.cwd : this.wkspace.fsPath; + } + public getPreferredTestManager(): UnitTestProduct | undefined { + const settings = PythonSettings.getInstance(this.wkspace); + if (settings.unitTest.nosetestsEnabled) { + return Product.nosetest; + } else if (settings.unitTest.pyTestEnabled) { + return Product.pytest; + } else if (settings.unitTest.unittestEnabled) { + return Product.unittest; + } + return undefined; + } +} diff --git a/src/client/unittests/common/testManagerServiceFactory.ts b/src/client/unittests/common/testManagerServiceFactory.ts new file mode 100644 index 000000000000..f10600ef8b45 --- /dev/null +++ b/src/client/unittests/common/testManagerServiceFactory.ts @@ -0,0 +1,12 @@ +import { OutputChannel, Uri } from 'vscode'; +import { TestManagerService } from './testManagerService'; +import { ITestCollectionStorageService, ITestDebugLauncher, ITestManagerService, ITestManagerServiceFactory, ITestResultsService, ITestsHelper } from './types'; + +export class TestManagerServiceFactory implements ITestManagerServiceFactory { + constructor(private outChannel: OutputChannel, private testCollectionStorage: ITestCollectionStorageService, + private testResultsService: ITestResultsService, private testsHelper: ITestsHelper, + private debugLauncher: ITestDebugLauncher) { } + public createTestManagerService(wkspace: Uri): ITestManagerService { + return new TestManagerService(wkspace, this.outChannel, this.testCollectionStorage, this.testResultsService, this.testsHelper, this.debugLauncher); + } +} diff --git a/src/client/unittests/common/testResultsService.ts b/src/client/unittests/common/testResultsService.ts new file mode 100644 index 000000000000..8743b579d879 --- /dev/null +++ b/src/client/unittests/common/testResultsService.ts @@ -0,0 +1,110 @@ +import { TestResultResetVisitor } from './testVisitors/resultResetVisitor'; +import { ITestResultsService, TestFile, TestFolder, Tests, TestStatus, TestSuite } from './types'; + +export class TestResultsService implements ITestResultsService { + public resetResults(tests: Tests): void { + const resultResetVisitor = new TestResultResetVisitor(); + tests.testFolders.forEach(f => resultResetVisitor.visitTestFolder(f)); + tests.testFunctions.forEach(fn => resultResetVisitor.visitTestFunction(fn.testFunction)); + tests.testSuites.forEach(suite => resultResetVisitor.visitTestSuite(suite.testSuite)); + tests.testFiles.forEach(testFile => resultResetVisitor.visitTestFile(testFile)); + } + public updateResults(tests: Tests): void { + tests.testFiles.forEach(test => this.updateTestFileResults(test)); + tests.testFolders.forEach(folder => this.updateTestFolderResults(folder)); + } + private updateTestSuiteResults(test: TestSuite): void { + this.updateTestSuiteAndFileResults(test); + } + private updateTestFileResults(test: TestFile): void { + this.updateTestSuiteAndFileResults(test); + } + private updateTestFolderResults(testFolder: TestFolder): void { + let allFilesPassed = true; + let allFilesRan = true; + + testFolder.testFiles.forEach(fl => { + if (allFilesPassed && typeof fl.passed === 'boolean') { + if (!fl.passed) { + allFilesPassed = false; + } + } else { + allFilesRan = false; + } + + testFolder.functionsFailed += fl.functionsFailed; + testFolder.functionsPassed += fl.functionsPassed; + }); + + let allFoldersPassed = true; + let allFoldersRan = true; + + testFolder.folders.forEach(folder => { + this.updateTestFolderResults(folder); + if (allFoldersPassed && typeof folder.passed === 'boolean') { + if (!folder.passed) { + allFoldersPassed = false; + } + } else { + allFoldersRan = false; + } + + testFolder.functionsFailed += folder.functionsFailed; + testFolder.functionsPassed += folder.functionsPassed; + }); + + if (allFilesRan && allFoldersRan) { + testFolder.passed = allFilesPassed && allFoldersPassed; + testFolder.status = testFolder.passed ? TestStatus.Idle : TestStatus.Fail; + } else { + testFolder.passed = null; + testFolder.status = TestStatus.Unknown; + } + } + private updateTestSuiteAndFileResults(test: TestSuite | TestFile): void { + let totalTime = 0; + let allFunctionsPassed = true; + let allFunctionsRan = true; + + test.functions.forEach(fn => { + totalTime += fn.time; + if (typeof fn.passed === 'boolean') { + if (fn.passed) { + test.functionsPassed += 1; + } else { + test.functionsFailed += 1; + allFunctionsPassed = false; + } + } else { + allFunctionsRan = false; + } + }); + + let allSuitesPassed = true; + let allSuitesRan = true; + + test.suites.forEach(suite => { + this.updateTestSuiteResults(suite); + totalTime += suite.time; + if (allSuitesRan && typeof suite.passed === 'boolean') { + if (!suite.passed) { + allSuitesPassed = false; + } + } else { + allSuitesRan = false; + } + + test.functionsFailed += suite.functionsFailed; + test.functionsPassed += suite.functionsPassed; + }); + + test.time = totalTime; + if (allSuitesRan && allFunctionsRan) { + test.passed = allFunctionsPassed && allSuitesPassed; + test.status = test.passed ? TestStatus.Idle : TestStatus.Error; + } else { + test.passed = null; + test.status = TestStatus.Unknown; + } + } +} diff --git a/src/client/unittests/common/testUtils.ts b/src/client/unittests/common/testUtils.ts index 9df1e2ca73ef..b552b4c82b7e 100644 --- a/src/client/unittests/common/testUtils.ts +++ b/src/client/unittests/common/testUtils.ts @@ -1,269 +1,117 @@ -import {TestFolder, TestsToRun, Tests, TestFile, TestSuite, TestStatus, FlattenedTestFunction, FlattenedTestSuite} from './contracts'; -import * as vscode from 'vscode'; import * as path from 'path'; +import * as vscode from 'vscode'; +import { Uri, workspace } from 'vscode'; +import { window } from 'vscode'; import * as constants from '../../common/constants'; - -let discoveredTests: Tests; +import { CommandSource } from './constants'; +import { TestFlatteningVisitor } from './testVisitors/flatteningVisitor'; +import { TestFile, TestFolder, Tests, TestsToRun } from './types'; +import { ITestsHelper } from './types'; + +export async function selectTestWorkspace(): Promise { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + return undefined; + } else if (workspace.workspaceFolders.length === 1) { + return workspace.workspaceFolders[0].uri; + } else { + // tslint:disable-next-line:no-any prefer-type-cast + const workspaceFolder = await (window as any).showWorkspaceFolderPick({ placeHolder: 'Select a workspace' }); + return workspaceFolder ? workspaceFolder.uri : undefined; + } +} export function displayTestErrorMessage(message: string) { vscode.window.showErrorMessage(message, constants.Button_Text_Tests_View_Output).then(action => { if (action === constants.Button_Text_Tests_View_Output) { - vscode.commands.executeCommand(constants.Commands.Tests_ViewOutput); + vscode.commands.executeCommand(constants.Commands.Tests_ViewOutput, CommandSource.ui); } }); } -export function getDiscoveredTests(): Tests { - return discoveredTests; -} -export function storeDiscoveredTests(tests: Tests) { - discoveredTests = tests; -} - -export function resolveValueAsTestToRun(name: string, rootDirectory: string): TestsToRun { - // TODO: We need a better way to match (currently we have raw name, name, xmlname, etc = which one do we - // use to identify a file given the full file name, similary for a folder and function - // Perhaps something like a parser or methods like TestFunction.fromString()... something) - let tests = getDiscoveredTests(); - if (!tests) { return null; } - const absolutePath = path.isAbsolute(name) ? name : path.resolve(rootDirectory, name); - let testFolders = tests.testFolders.filter(folder => folder.nameToRun === name || folder.name === name || folder.name === absolutePath); - if (testFolders.length > 0) { return { testFolder: testFolders }; }; - - let testFiles = tests.testFiles.filter(file => file.nameToRun === name || file.name === name || file.fullPath === absolutePath); - if (testFiles.length > 0) { return { testFile: testFiles }; }; - let testFns = tests.testFunctions.filter(fn => fn.testFunction.nameToRun === name || fn.testFunction.name === name).map(fn => fn.testFunction); - if (testFns.length > 0) { return { testFunction: testFns }; }; - - // Just return this as a test file - return { testFile: [{ name: name, nameToRun: name, functions: [], suites: [], xmlName: name, fullPath: '', time: 0 }] }; -} export function extractBetweenDelimiters(content: string, startDelimiter: string, endDelimiter: string): string { content = content.substring(content.indexOf(startDelimiter) + startDelimiter.length); return content.substring(0, content.lastIndexOf(endDelimiter)); } export function convertFileToPackage(filePath: string): string { - let lastIndex = filePath.lastIndexOf('.'); + const lastIndex = filePath.lastIndexOf('.'); return filePath.substring(0, lastIndex).replace(/\//g, '.').replace(/\\/g, '.'); } -export function updateResults(tests: Tests) { - tests.testFiles.forEach(updateResultsUpstream); - tests.testFolders.forEach(updateFolderResultsUpstream); -} +export class TestsHelper implements ITestsHelper { + public flattenTestFiles(testFiles: TestFile[]): Tests { + const flatteningVisitor = new TestFlatteningVisitor(); + testFiles.forEach(testFile => flatteningVisitor.visitTestFile(testFile)); -export function updateFolderResultsUpstream(testFolder: TestFolder) { - let allFilesPassed = true; - let allFilesRan = true; - - testFolder.testFiles.forEach(fl => { - if (allFilesPassed && typeof fl.passed === 'boolean') { - if (!fl.passed) { - allFilesPassed = false; - } - } - else { - allFilesRan = false; - } - - testFolder.functionsFailed += fl.functionsFailed; - testFolder.functionsPassed += fl.functionsPassed; - }); + const tests = { + testFiles: testFiles, + testFunctions: flatteningVisitor.flattenedTestFunctions, + testSuites: flatteningVisitor.flattenedTestSuites, + testFolders: [], + rootTestFolders: [], + summary: { passed: 0, failures: 0, errors: 0, skipped: 0 } + }; - let allFoldersPassed = true; - let allFoldersRan = true; + this.placeTestFilesIntoFolders(tests); - testFolder.folders.forEach(folder => { - updateFolderResultsUpstream(folder); - if (allFoldersPassed && typeof folder.passed === 'boolean') { - if (!folder.passed) { - allFoldersPassed = false; - } - } - else { - allFoldersRan = false; - } - - testFolder.functionsFailed += folder.functionsFailed; - testFolder.functionsPassed += folder.functionsPassed; - }); - - if (allFilesRan && allFoldersRan) { - testFolder.passed = allFilesPassed && allFoldersPassed; - testFolder.status = testFolder.passed ? TestStatus.Idle : TestStatus.Fail; - } - else { - testFolder.passed = null; - testFolder.status = TestStatus.Unknown; + return tests; } -} - -export function updateResultsUpstream(test: TestSuite | TestFile) { - let totalTime = 0; - let allFunctionsPassed = true; - let allFunctionsRan = true; - - test.functions.forEach(fn => { - totalTime += fn.time; - if (typeof fn.passed === 'boolean') { - if (fn.passed) { - test.functionsPassed += 1; - } - else { - test.functionsFailed += 1; - allFunctionsPassed = false; - } - } - else { - allFunctionsRan = false; - } - }); - - let allSuitesPassed = true; - let allSuitesRan = true; - - test.suites.forEach(suite => { - updateResultsUpstream(suite); - totalTime += suite.time; - if (allSuitesRan && typeof suite.passed === 'boolean') { - if (!suite.passed) { - allSuitesPassed = false; + public placeTestFilesIntoFolders(tests: Tests): void { + // First get all the unique folders + const folders: string[] = []; + tests.testFiles.forEach(file => { + const dir = path.dirname(file.name); + if (folders.indexOf(dir) === -1) { + folders.push(dir); } - } - else { - allSuitesRan = false; - } - - test.functionsFailed += suite.functionsFailed; - test.functionsPassed += suite.functionsPassed; - }); - - test.time = totalTime; - if (allSuitesRan && allFunctionsRan) { - test.passed = allFunctionsPassed && allSuitesPassed; - test.status = test.passed ? TestStatus.Idle : TestStatus.Error; - } - else { - test.passed = null; - test.status = TestStatus.Unknown; - } -} - -export function placeTestFilesInFolders(tests: Tests) { - // First get all the unique folders - const folders: string[] = []; - tests.testFiles.forEach(file => { - let dir = path.dirname(file.name); - if (folders.indexOf(dir) === -1) { - folders.push(dir); - } - }); - - tests.testFolders = []; - const folderMap = new Map(); - folders.sort(); + }); - folders.forEach(dir => { - dir.split(path.sep).reduce((parentPath, currentName, index, values) => { - let newPath = currentName; - let parentFolder: TestFolder; - if (parentPath.length > 0) { - parentFolder = folderMap.get(parentPath); - newPath = path.join(parentPath, currentName); - } - if (!folderMap.has(newPath)) { - const testFolder: TestFolder = { name: newPath, testFiles: [], folders: [], nameToRun: newPath, time: 0 }; - folderMap.set(newPath, testFolder); - if (parentFolder) { - parentFolder.folders.push(testFolder); + tests.testFolders = []; + const folderMap = new Map(); + folders.sort(); + + folders.forEach(dir => { + dir.split(path.sep).reduce((parentPath, currentName, index, values) => { + let newPath = currentName; + let parentFolder: TestFolder; + if (parentPath.length > 0) { + parentFolder = folderMap.get(parentPath); + newPath = path.join(parentPath, currentName); } - else { - tests.rootTestFolders.push(testFolder); + if (!folderMap.has(newPath)) { + const testFolder: TestFolder = { name: newPath, testFiles: [], folders: [], nameToRun: newPath, time: 0 }; + folderMap.set(newPath, testFolder); + if (parentFolder) { + parentFolder.folders.push(testFolder); + } else { + tests.rootTestFolders.push(testFolder); + } + tests.testFiles.filter(fl => path.dirname(fl.name) === newPath).forEach(testFile => { + testFolder.testFiles.push(testFile); + }); + tests.testFolders.push(testFolder); } - tests.testFiles.filter(fl => path.dirname(fl.name) === newPath).forEach(testFile => { - testFolder.testFiles.push(testFile); - }); - tests.testFolders.push(testFolder); - } - return newPath; - }, ''); - }); -} -export function flattenTestFiles(testFiles: TestFile[]): Tests { - let fns: FlattenedTestFunction[] = []; - let suites: FlattenedTestSuite[] = []; - testFiles.forEach(testFile => { - // sample test_three (file name without extension and all / replaced with ., meaning this is the package) - const packageName = convertFileToPackage(testFile.name); - - testFile.functions.forEach(fn => { - fns.push({ testFunction: fn, xmlClassName: packageName, parentTestFile: testFile }); + return newPath; + }, ''); }); - - testFile.suites.forEach(suite => { - suites.push({ parentTestFile: testFile, testSuite: suite, xmlClassName: suite.xmlName }); - flattenTestSuites(fns, suites, testFile, suite); - }); - }); - - let tests = { - testFiles: testFiles, - testFunctions: fns, testSuits: suites, - testFolders: [], - rootTestFolders: [], - summary: { passed: 0, failures: 0, errors: 0, skipped: 0 } - }; - - placeTestFilesInFolders(tests); - - return tests; -} -export function flattenTestSuites(flattenedFns: FlattenedTestFunction[], flattenedSuites: FlattenedTestSuite[], testFile: TestFile, testSuite: TestSuite) { - testSuite.functions.forEach(fn => { - flattenedFns.push({ testFunction: fn, xmlClassName: testSuite.xmlName, parentTestFile: testFile, parentTestSuite: testSuite }); - }); - - // We may have child classes - testSuite.suites.forEach(suite => { - flattenedSuites.push({ parentTestFile: testFile, testSuite: suite, xmlClassName: suite.xmlName }); - flattenTestSuites(flattenedFns, flattenedSuites, testFile, suite); - }); + } + public parseTestName(name: string, rootDirectory: string, tests: Tests): TestsToRun { + // TODO: We need a better way to match (currently we have raw name, name, xmlname, etc = which one do we. + // Use to identify a file given the full file name, similarly for a folder and function. + // Perhaps something like a parser or methods like TestFunction.fromString()... something). + if (!tests) { return null; } + const absolutePath = path.isAbsolute(name) ? name : path.resolve(rootDirectory, name); + const testFolders = tests.testFolders.filter(folder => folder.nameToRun === name || folder.name === name || folder.name === absolutePath); + if (testFolders.length > 0) { return { testFolder: testFolders }; } + + const testFiles = tests.testFiles.filter(file => file.nameToRun === name || file.name === name || file.fullPath === absolutePath); + if (testFiles.length > 0) { return { testFile: testFiles }; } + + const testFns = tests.testFunctions.filter(fn => fn.testFunction.nameToRun === name || fn.testFunction.name === name).map(fn => fn.testFunction); + if (testFns.length > 0) { return { testFunction: testFns }; } + + // Just return this as a test file. + return { testFile: [{ name: name, nameToRun: name, functions: [], suites: [], xmlName: name, fullPath: '', time: 0 }] }; + } } - -export function resetTestResults(tests: Tests) { - tests.testFolders.forEach(f => { - f.functionsDidNotRun = 0; - f.functionsFailed = 0; - f.functionsPassed = 0; - f.passed = null; - f.status = TestStatus.Unknown; - }); - tests.testFunctions.forEach(fn => { - fn.testFunction.passed = null; - fn.testFunction.time = 0; - fn.testFunction.message = ''; - fn.testFunction.traceback = ''; - fn.testFunction.status = TestStatus.Unknown; - fn.testFunction.functionsFailed = 0; - fn.testFunction.functionsPassed = 0; - fn.testFunction.functionsDidNotRun = 0; - }); - tests.testSuits.forEach(suite => { - suite.testSuite.passed = null; - suite.testSuite.time = 0; - suite.testSuite.status = TestStatus.Unknown; - suite.testSuite.functionsFailed = 0; - suite.testSuite.functionsPassed = 0; - suite.testSuite.functionsDidNotRun = 0; - }); - tests.testFiles.forEach(testFile => { - testFile.passed = null; - testFile.time = 0; - testFile.status = TestStatus.Unknown; - testFile.functionsFailed = 0; - testFile.functionsPassed = 0; - testFile.functionsDidNotRun = 0; - }); -} \ No newline at end of file diff --git a/src/client/unittests/common/testVisitors/flatteningVisitor.ts b/src/client/unittests/common/testVisitors/flatteningVisitor.ts new file mode 100644 index 000000000000..4a8ed96babbc --- /dev/null +++ b/src/client/unittests/common/testVisitors/flatteningVisitor.ts @@ -0,0 +1,65 @@ +import { convertFileToPackage } from '../testUtils'; +import { + FlattenedTestFunction, + FlattenedTestSuite, + ITestVisitor, + TestFile, + TestFolder, + TestFunction, + TestSuite +} from '../types'; + +export class TestFlatteningVisitor implements ITestVisitor { + // tslint:disable-next-line:variable-name + private _flattedTestFunctions = new Map(); + // tslint:disable-next-line:variable-name + private _flattenedTestSuites = new Map(); + public get flattenedTestFunctions(): FlattenedTestFunction[] { + return [...this._flattedTestFunctions.values()]; + } + public get flattenedTestSuites(): FlattenedTestSuite[] { + return [...this._flattenedTestSuites.values()]; + } + // tslint:disable-next-line:no-empty + public visitTestFunction(testFunction: TestFunction): void { } + // tslint:disable-next-line:no-empty + public visitTestSuite(testSuite: TestSuite): void { } + public visitTestFile(testFile: TestFile): void { + // sample test_three (file name without extension and all / replaced with ., meaning this is the package) + const packageName = convertFileToPackage(testFile.name); + + testFile.functions.forEach(fn => this.addTestFunction(fn, testFile, packageName)); + testFile.suites.forEach(suite => this.visitTestSuiteOfAFile(suite, testFile)); + } + // tslint:disable-next-line:no-empty + public visitTestFolder(testFile: TestFolder) { } + private visitTestSuiteOfAFile(testSuite: TestSuite, parentTestFile: TestFile): void { + testSuite.functions.forEach(fn => this.visitTestFunctionOfASuite(fn, testSuite, parentTestFile)); + testSuite.suites.forEach(suite => this.visitTestSuiteOfAFile(suite, parentTestFile)); + this.addTestSuite(testSuite, parentTestFile); + } + private visitTestFunctionOfASuite(testFunction: TestFunction, parentTestSuite: TestSuite, parentTestFile: TestFile) { + const key = `Function:${testFunction.name},Suite:${parentTestSuite.name},SuiteXmlName:${parentTestSuite.xmlName},ParentFile:${parentTestFile.fullPath}`; + if (this._flattenedTestSuites.has(key)) { + return; + } + const flattenedFunction = { testFunction, xmlClassName: parentTestSuite.xmlName, parentTestFile, parentTestSuite }; + this._flattedTestFunctions.set(key, flattenedFunction); + } + private addTestSuite(testSuite: TestSuite, parentTestFile: TestFile) { + const key = `Suite:${testSuite.name},SuiteXmlName:${testSuite.xmlName},ParentFile:${parentTestFile.fullPath}`; + if (this._flattenedTestSuites.has(key)) { + return; + } + const flattenedSuite = { parentTestFile, testSuite, xmlClassName: testSuite.xmlName }; + this._flattenedTestSuites.set(key, flattenedSuite); + } + private addTestFunction(testFunction: TestFunction, parentTestFile: TestFile, parentTestPackage: string) { + const key = `Function:${testFunction.name},ParentFile:${parentTestFile.fullPath}`; + if (this._flattedTestFunctions.has(key)) { + return; + } + const flattendFunction = { testFunction, xmlClassName: parentTestPackage, parentTestFile }; + this._flattedTestFunctions.set(key, flattendFunction); + } +} diff --git a/src/client/unittests/common/testVisitors/folderGenerationVisitor.ts b/src/client/unittests/common/testVisitors/folderGenerationVisitor.ts new file mode 100644 index 000000000000..46956873edd1 --- /dev/null +++ b/src/client/unittests/common/testVisitors/folderGenerationVisitor.ts @@ -0,0 +1,55 @@ +import * as path from 'path'; +import { ITestVisitor, TestFile, TestFolder, TestFunction, TestSuite } from '../types'; + +export class TestFolderGenerationVisitor implements ITestVisitor { + // tslint:disable-next-line:variable-name + private _testFolders: TestFolder[] = []; + // tslint:disable-next-line:variable-name + private _rootTestFolders: TestFolder[] = []; + private folderMap = new Map(); + public get testFolders(): Readonly { + return [...this._testFolders]; + } + public get rootTestFolders(): Readonly { + return [...this._rootTestFolders]; + } + // tslint:disable-next-line:no-empty + public visitTestFunction(testFunction: TestFunction): void { } + // tslint:disable-next-line:no-empty + public visitTestSuite(testSuite: TestSuite): void { } + public visitTestFile(testFile: TestFile): void { + // First get all the unique folders + const folders: string[] = []; + const dir = path.dirname(testFile.name); + if (this.folderMap.has(dir)) { + const folder = this.folderMap.get(dir); + folder.testFiles.push(testFile); + return; + } + + dir.split(path.sep).reduce((accumulatedPath, currentName, index) => { + let newPath = currentName; + let parentFolder: TestFolder; + if (accumulatedPath.length > 0) { + parentFolder = this.folderMap.get(accumulatedPath); + newPath = path.join(accumulatedPath, currentName); + } + if (!this.folderMap.has(newPath)) { + const testFolder: TestFolder = { name: newPath, testFiles: [], folders: [], nameToRun: newPath, time: 0 }; + this.folderMap.set(newPath, testFolder); + if (parentFolder) { + parentFolder.folders.push(testFolder); + } else { + this._rootTestFolders.push(testFolder); + } + this._testFolders.push(testFolder); + } + return newPath; + }, ''); + + // tslint:disable-next-line:no-non-null-assertion + this.folderMap.get(dir)!.testFiles.push(testFile); + } + // tslint:disable-next-line:no-empty + public visitTestFolder(testFile: TestFolder) { } +} diff --git a/src/client/unittests/common/testVisitors/resultResetVisitor.ts b/src/client/unittests/common/testVisitors/resultResetVisitor.ts new file mode 100644 index 000000000000..0d58c1076b04 --- /dev/null +++ b/src/client/unittests/common/testVisitors/resultResetVisitor.ts @@ -0,0 +1,37 @@ +import { ITestVisitor, TestFile, TestFolder, TestFunction, TestStatus, TestSuite } from '../types'; + +export class TestResultResetVisitor implements ITestVisitor { + public visitTestFunction(testFunction: TestFunction): void { + testFunction.passed = null; + testFunction.time = 0; + testFunction.message = ''; + testFunction.traceback = ''; + testFunction.status = TestStatus.Unknown; + testFunction.functionsFailed = 0; + testFunction.functionsPassed = 0; + testFunction.functionsDidNotRun = 0; + } + public visitTestSuite(testSuite: TestSuite): void { + testSuite.passed = null; + testSuite.time = 0; + testSuite.status = TestStatus.Unknown; + testSuite.functionsFailed = 0; + testSuite.functionsPassed = 0; + testSuite.functionsDidNotRun = 0; + } + public visitTestFile(testFile: TestFile): void { + testFile.passed = null; + testFile.time = 0; + testFile.status = TestStatus.Unknown; + testFile.functionsFailed = 0; + testFile.functionsPassed = 0; + testFile.functionsDidNotRun = 0; + } + public visitTestFolder(testFolder: TestFolder) { + testFolder.functionsDidNotRun = 0; + testFolder.functionsFailed = 0; + testFolder.functionsPassed = 0; + testFolder.passed = null; + testFolder.status = TestStatus.Unknown; + } +} diff --git a/src/client/unittests/common/types.ts b/src/client/unittests/common/types.ts new file mode 100644 index 000000000000..5125b5a4c17c --- /dev/null +++ b/src/client/unittests/common/types.ts @@ -0,0 +1,152 @@ +import { CancellationToken, Disposable, OutputChannel, Uri } from 'vscode'; +import { Product } from '../../common/installer'; +import { BaseTestManager } from './baseTestManager'; + +export type TestFolder = TestResult & { + name: string; + testFiles: TestFile[]; + nameToRun: string; + status?: TestStatus; + folders: TestFolder[]; +}; + +export type TestFile = TestResult & { + name: string; + fullPath: string; + functions: TestFunction[]; + suites: TestSuite[]; + nameToRun: string; + xmlName: string; + status?: TestStatus; + errorsWhenDiscovering?: string; +}; + +export type TestSuite = TestResult & { + name: string; + functions: TestFunction[]; + suites: TestSuite[]; + isUnitTest: Boolean; + isInstance: Boolean; + nameToRun: string; + xmlName: string; + status?: TestStatus; +}; + +export type TestFunction = TestResult & { + name: string; + nameToRun: string; + status?: TestStatus; +}; + +export type TestResult = Node & { + passed?: boolean; + time: number; + line?: number; + message?: string; + traceback?: string; + functionsPassed?: number; + functionsFailed?: number; + functionsDidNotRun?: number; +}; + +export type Node = { + expanded?: Boolean; +}; + +export type FlattenedTestFunction = { + testFunction: TestFunction; + parentTestSuite?: TestSuite; + parentTestFile: TestFile; + xmlClassName: string; +}; + +export type FlattenedTestSuite = { + testSuite: TestSuite; + parentTestSuite?: TestSuite; + parentTestFile: TestFile; + xmlClassName: string; +}; + +export type TestSummary = { + passed: number; + failures: number; + errors: number; + skipped: number; +}; + +export type Tests = { + summary: TestSummary; + testFiles: TestFile[]; + testFunctions: FlattenedTestFunction[]; + testSuites: FlattenedTestSuite[]; + testFolders: TestFolder[]; + rootTestFolders: TestFolder[]; +}; + +export enum TestStatus { + Unknown, + Discovering, + Idle, + Running, + Fail, + Error, + Skipped, + Pass +} + +export type TestsToRun = { + testFolder?: TestFolder[]; + testFile?: TestFile[]; + testSuite?: TestSuite[]; + testFunction?: TestFunction[]; +}; + +export type UnitTestProduct = Product.nosetest | Product.pytest | Product.unittest; + +export interface ITestConfigSettingsService { + updateTestArgs(testDirectory: string, product: UnitTestProduct, args: string[]): Promise; + enable(testDirectory: string | Uri, product: UnitTestProduct): Promise; + disable(testDirectory: string | Uri, product: UnitTestProduct): Promise; +} + +export interface ITestManagerService extends Disposable { + getTestManager(): BaseTestManager | undefined; + getTestWorkingDirectory(): string; + getPreferredTestManager(): UnitTestProduct | undefined; +} + +export interface ITestManagerServiceFactory { + createTestManagerService(wkspace: Uri): ITestManagerService; +} + +export interface IWorkspaceTestManagerService extends Disposable { + getTestManager(resource: Uri): BaseTestManager | undefined; + getTestWorkingDirectory(resource: Uri): string; + getPreferredTestManager(resource: Uri): UnitTestProduct | undefined; +} + +export interface ITestsHelper { + flattenTestFiles(testFiles: TestFile[]): Tests; + placeTestFilesIntoFolders(tests: Tests): void; +} + +export interface ITestVisitor { + visitTestFunction(testFunction: TestFunction): void; + visitTestSuite(testSuite: TestSuite): void; + visitTestFile(testFile: TestFile): void; + visitTestFolder(testFile: TestFolder): void; +} + +export interface ITestCollectionStorageService extends Disposable { + getTests(wkspace: Uri): Tests | undefined; + storeTests(wkspace: Uri, tests: Tests | null | undefined): void; +} + +export interface ITestResultsService { + resetResults(tests: Tests): void; + updateResults(tests: Tests): void; +} + +export interface ITestDebugLauncher { + launchDebugger(rootDirectory: string, testArgs: string[], token?: CancellationToken, outChannel?: OutputChannel): Promise; +} diff --git a/src/client/unittests/common/workspaceTestManagerService.ts b/src/client/unittests/common/workspaceTestManagerService.ts new file mode 100644 index 000000000000..ff7e198cb423 --- /dev/null +++ b/src/client/unittests/common/workspaceTestManagerService.ts @@ -0,0 +1,53 @@ +import { Disposable, OutputChannel, Uri, workspace } from 'vscode'; +import { Product } from '../../common/installer'; +import { BaseTestManager } from './baseTestManager'; +import { TestManagerService } from './testManagerService'; +import { ITestManagerService, ITestManagerServiceFactory, IWorkspaceTestManagerService, UnitTestProduct } from './types'; + +export class WorkspaceTestManagerService implements IWorkspaceTestManagerService, Disposable { + private workspaceTestManagers = new Map(); + private disposables: Disposable[] = []; + constructor(private outChannel: OutputChannel, + private testManagerServiceFactory: ITestManagerServiceFactory) { + } + public dispose() { + this.workspaceTestManagers.forEach(info => info.dispose()); + } + public getTestManager(resource: Uri): BaseTestManager | undefined { + const wkspace = this.getWorkspace(resource); + this.ensureTestManagerService(wkspace); + return this.workspaceTestManagers.get(wkspace.fsPath).getTestManager(); + } + public getTestWorkingDirectory(resource: Uri) { + const wkspace = this.getWorkspace(resource); + this.ensureTestManagerService(wkspace); + return this.workspaceTestManagers.get(wkspace.fsPath).getTestWorkingDirectory(); + } + public getPreferredTestManager(resource: Uri): UnitTestProduct { + const wkspace = this.getWorkspace(resource); + this.ensureTestManagerService(wkspace); + return this.workspaceTestManagers.get(wkspace.fsPath).getPreferredTestManager(); + } + private getWorkspace(resource: Uri): Uri { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + const noWkspaceMessage = 'Please open a workspace'; + this.outChannel.appendLine(noWkspaceMessage); + throw new Error(noWkspaceMessage); + } + if (!resource || workspace.workspaceFolders.length === 1) { + return workspace.workspaceFolders[0].uri; + } + const workspaceFolder = workspace.getWorkspaceFolder(resource); + if (workspaceFolder) { + return workspaceFolder.uri; + } + const message = `Resource '${resource.fsPath}' does not belong to any workspace`; + this.outChannel.appendLine(message); + throw new Error(message); + } + private ensureTestManagerService(wkspace: Uri) { + if (!this.workspaceTestManagers.has(wkspace.fsPath)) { + this.workspaceTestManagers.set(wkspace.fsPath, this.testManagerServiceFactory.createTestManagerService(wkspace)); + } + } +} diff --git a/src/client/unittests/common/xUnitParser.ts b/src/client/unittests/common/xUnitParser.ts index 2f02601266a0..6318060a94f7 100644 --- a/src/client/unittests/common/xUnitParser.ts +++ b/src/client/unittests/common/xUnitParser.ts @@ -1,12 +1,12 @@ import * as fs from 'fs'; import * as xml2js from 'xml2js'; -import { Tests, TestStatus } from './contracts'; +import { Tests, TestStatus } from './types'; export enum PassCalculationFormulae { pytest, nosetests } -interface TestSuiteResult { +type TestSuiteResult = { $: { errors: string; failures: string; @@ -17,8 +17,8 @@ interface TestSuiteResult { time: string; }; testcase: TestCaseResult[]; -} -interface TestCaseResult { +}; +type TestCaseResult = { $: { classname: string; file: string; @@ -38,30 +38,32 @@ interface TestCaseResult { _: string; $: { message: string, type: string } }[]; -} +}; +// tslint:disable-next-line:no-any function getSafeInt(value: string, defaultValue: any = 0): number { - const num = parseInt(value); + const num = parseInt(value, 10); if (isNaN(num)) { return defaultValue; } return num; } -export function updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string, passCalculationFormulae: PassCalculationFormulae): Promise { +export function updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string, passCalculationFormulae: PassCalculationFormulae): Promise<{}> { + // tslint:disable-next-line:no-any return new Promise((resolve, reject) => { fs.readFile(outputXmlFile, 'utf8', (err, data) => { if (err) { return reject(err); } - xml2js.parseString(data, (err, result) => { - if (err) { - return reject(err); + xml2js.parseString(data, (error, parserResult) => { + if (error) { + return reject(error); } - let testSuiteResult: TestSuiteResult = result.testsuite; + const testSuiteResult: TestSuiteResult = parserResult.testsuite; tests.summary.errors = getSafeInt(testSuiteResult.$.errors); tests.summary.failures = getSafeInt(testSuiteResult.$.failures); tests.summary.skipped = getSafeInt(testSuiteResult.$.skips ? testSuiteResult.$.skips : testSuiteResult.$.skip); - let testCount = getSafeInt(testSuiteResult.$.tests); + const testCount = getSafeInt(testSuiteResult.$.tests); switch (passCalculationFormulae) { case PassCalculationFormulae.pytest: { @@ -73,7 +75,7 @@ export function updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string, break; } default: { - throw new Error("Unknown Test Pass Calculation"); + throw new Error('Unknown Test Pass Calculation'); } } @@ -83,7 +85,7 @@ export function updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string, testSuiteResult.testcase.forEach((testcase: TestCaseResult) => { const xmlClassName = testcase.$.classname.replace(/\(\)/g, '').replace(/\.\./g, '.').replace(/\.\./g, '.').replace(/\.+$/, ''); - let result = tests.testFunctions.find(fn => fn.xmlClassName === xmlClassName && fn.testFunction.name === testcase.$.name); + const result = tests.testFunctions.find(fn => fn.xmlClassName === xmlClassName && fn.testFunction.name === testcase.$.name); if (!result) { // Possible we're dealing with nosetests, where the file name isn't returned to us // When dealing with nose tests @@ -94,7 +96,7 @@ export function updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string, // fn.parentTestSuite && fn.parentTestSuite.name === testcase.$.classname); // Look for failed file test - let fileTest = testcase.$.file && tests.testFiles.find(file => file.nameToRun === testcase.$.file); + const fileTest = testcase.$.file && tests.testFiles.find(file => file.nameToRun === testcase.$.file); if (fileTest && testcase.error) { fileTest.status = TestStatus.Error; fileTest.passed = false; @@ -109,7 +111,6 @@ export function updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string, result.testFunction.passed = true; result.testFunction.status = TestStatus.Pass; - if (testcase.failure) { result.testFunction.status = TestStatus.Fail; result.testFunction.passed = false; diff --git a/src/client/unittests/configuration.ts b/src/client/unittests/configuration.ts index 090fc2bf92c8..ecbefeae8d4d 100644 --- a/src/client/unittests/configuration.ts +++ b/src/client/unittests/configuration.ts @@ -1,150 +1,159 @@ 'use strict'; +import * as path from 'path'; import * as vscode from 'vscode'; +import { OutputChannel, Uri } from 'vscode'; import { PythonSettings } from '../common/configSettings'; -import { Product } from '../common/installer'; +import { Installer, Product } from '../common/installer'; +import { getSubDirectories } from '../common/utils'; +import { TestConfigSettingsService } from './common/configSettingService'; import { TestConfigurationManager } from './common/testConfigurationManager'; +import { selectTestWorkspace } from './common/testUtils'; +import { UnitTestProduct } from './common/types'; +import { ConfigurationManager } from './nosetest/testConfigurationManager'; import * as nose from './nosetest/testConfigurationManager'; import * as pytest from './pytest/testConfigurationManager'; import * as unittest from './unittest/testConfigurationManager'; -import { getSubDirectories } from '../common/utils'; -import * as path from 'path'; - -const settings = PythonSettings.getInstance(); -function promptToEnableAndConfigureTestFramework(outputChannel: vscode.OutputChannel, messageToDisplay: string = 'Select a test framework/tool to enable', enableOnly: boolean = false): Thenable { - const items = [{ - label: 'unittest', - product: Product.unittest, - description: 'Standard Python test framework', - detail: 'https://docs.python.org/2/library/unittest.html' - }, - { - label: 'pytest', - product: Product.pytest, - description: 'Can run unittest (including trial) and nose test suites out of the box', - detail: 'http://docs.pytest.org/en/latest/' - }, - { - label: 'nose', - product: Product.nosetest, - description: 'nose framework', - detail: 'https://docs.python.org/2/library/unittest.html' - }]; - const options = { - matchOnDescription: true, - matchOnDetail: true, - placeHolder: messageToDisplay - }; - return vscode.window.showQuickPick(items, options).then(item => { - if (!item) { - return Promise.reject(null); +// tslint:disable-next-line:no-any +async function promptToEnableAndConfigureTestFramework(wkspace: Uri, outputChannel: vscode.OutputChannel, messageToDisplay: string = 'Select a test framework/tool to enable', enableOnly: boolean = false) { + const selectedTestRunner = await selectTestRunner(messageToDisplay); + if (typeof selectedTestRunner !== 'number') { + return Promise.reject(null); + } + const configMgr: TestConfigurationManager = createTestConfigurationManager(wkspace, selectedTestRunner, outputChannel); + if (enableOnly) { + // Ensure others are disabled + if (selectedTestRunner !== Product.unittest) { + createTestConfigurationManager(wkspace, Product.unittest, outputChannel).disable(); } - let configMgr: TestConfigurationManager; - switch (item.product) { - case Product.unittest: { - configMgr = new unittest.ConfigurationManager(outputChannel); - break; - } - case Product.pytest: { - configMgr = new pytest.ConfigurationManager(outputChannel); - break; - } - case Product.nosetest: { - configMgr = new nose.ConfigurationManager(outputChannel); - break; - } - default: { - throw new Error('Invalid test configuration'); - } + if (selectedTestRunner !== Product.pytest) { + createTestConfigurationManager(wkspace, Product.pytest, outputChannel).disable(); } - - if (enableOnly) { - // Ensure others are disabled - if (item.product !== Product.unittest) { - (new unittest.ConfigurationManager(outputChannel)).disable(); - } - if (item.product !== Product.pytest) { - (new pytest.ConfigurationManager(outputChannel)).disable(); - } - if (item.product !== Product.nosetest) { - (new nose.ConfigurationManager(outputChannel)).disable(); - } - return configMgr.enable(); + if (selectedTestRunner !== Product.nosetest) { + createTestConfigurationManager(wkspace, Product.nosetest, outputChannel).disable(); } + return configMgr.enable(); + } - // Configure everything before enabling - // Cuz we don't want the test engine (in main.ts file - tests get discovered when config changes are detected) - // to start discovering tests when tests haven't been configured properly - function enableTest(): Thenable { - const pythonConfig = vscode.workspace.getConfiguration('python'); - if (settings.unitTest.promptToConfigure) { - return configMgr.enable(); - } - return pythonConfig.update('unitTest.promptToConfigure', undefined).then(() => { - return configMgr.enable(); - }, reason => { - return configMgr.enable().then(() => Promise.reject(reason)); - }); - } - return configMgr.configure(vscode.workspace.rootPath).then(() => { - return enableTest(); - }).catch(reason => { - return enableTest().then(() => Promise.reject(reason)); - }); + return configMgr.configure(wkspace).then(() => { + return enableTest(wkspace, configMgr); + }).catch(reason => { + return enableTest(wkspace, configMgr).then(() => Promise.reject(reason)); }); } -export function displayTestFrameworkError(outputChannel: vscode.OutputChannel): Thenable { +export function displayTestFrameworkError(wkspace: Uri, outputChannel: vscode.OutputChannel) { + const settings = PythonSettings.getInstance(); let enabledCount = settings.unitTest.pyTestEnabled ? 1 : 0; enabledCount += settings.unitTest.nosetestsEnabled ? 1 : 0; enabledCount += settings.unitTest.unittestEnabled ? 1 : 0; if (enabledCount > 1) { - return promptToEnableAndConfigureTestFramework(outputChannel, 'Enable only one of the test frameworks (unittest, pytest or nosetest).', true); - } - else { + return promptToEnableAndConfigureTestFramework(wkspace, outputChannel, 'Enable only one of the test frameworks (unittest, pytest or nosetest).', true); + } else { const option = 'Enable and configure a Test Framework'; return vscode.window.showInformationMessage('No test framework configured (unittest, pytest or nosetest)', option).then(item => { if (item === option) { - return promptToEnableAndConfigureTestFramework(outputChannel); + return promptToEnableAndConfigureTestFramework(wkspace, outputChannel); } return Promise.reject(null); }); } } -export function displayPromptToEnableTests(rootDir: string, outputChannel: vscode.OutputChannel): Thenable { +export async function displayPromptToEnableTests(rootDir: string, outputChannel: vscode.OutputChannel) { + const settings = PythonSettings.getInstance(vscode.Uri.file(rootDir)); if (settings.unitTest.pyTestEnabled || settings.unitTest.nosetestsEnabled || settings.unitTest.unittestEnabled) { - return Promise.reject(null); + return; } if (!settings.unitTest.promptToConfigure) { - return Promise.reject(null); + return; } const yes = 'Yes'; - const no = `Later`; - const noNotAgain = `No, don't ask again`; + const no = 'Later'; + const noNotAgain = 'No, don\'t ask again'; - return checkIfHasTestDirs(rootDir).then(hasTests => { - if (!yes) { - return Promise.reject(null); - } - return vscode.window.showInformationMessage('You seem to have tests, would you like to enable a test framework?', yes, no, noNotAgain).then(item => { - if (!item || item === no) { - return Promise.reject(null); - } - if (item === yes) { - return promptToEnableAndConfigureTestFramework(outputChannel); - } - else { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('unitTest.promptToConfigure', false); - } - }); + const hasTests = checkForExistenceOfTests(rootDir); + if (!hasTests) { + return; + } + const item = await vscode.window.showInformationMessage('You seem to have tests, would you like to enable a test framework?', yes, no, noNotAgain); + if (!item || item === no) { + return; + } + if (item === yes) { + await promptToEnableAndConfigureTestFramework(vscode.workspace.getWorkspaceFolder(vscode.Uri.file(rootDir)).uri, outputChannel); + } else { + const pythonConfig = vscode.workspace.getConfiguration('python'); + await pythonConfig.update('unitTest.promptToConfigure', false); + } +} + +// Configure everything before enabling. +// Cuz we don't want the test engine (in main.ts file - tests get discovered when config changes are detected) +// to start discovering tests when tests haven't been configured properly. +function enableTest(wkspace: Uri, configMgr: ConfigurationManager) { + const pythonConfig = vscode.workspace.getConfiguration('python', wkspace); + // tslint:disable-next-line:no-backbone-get-set-outside-model + if (pythonConfig.get('unitTest.promptToConfigure')) { + return configMgr.enable(); + } + return pythonConfig.update('unitTest.promptToConfigure', undefined).then(() => { + return configMgr.enable(); + }, reason => { + return configMgr.enable().then(() => Promise.reject(reason)); }); } -function checkIfHasTestDirs(rootDir: string): Promise { +function checkForExistenceOfTests(rootDir: string): Promise { return getSubDirectories(rootDir).then(subDirs => { return subDirs.map(dir => path.relative(rootDir, dir)).filter(dir => dir.match(/test/i)).length > 0; }); -} \ No newline at end of file +} +function createTestConfigurationManager(wkspace: Uri, product: Product, outputChannel: OutputChannel) { + const installer = new Installer(outputChannel); + const configSettingService = new TestConfigSettingsService(); + switch (product) { + case Product.unittest: { + return new unittest.ConfigurationManager(wkspace, outputChannel, installer, configSettingService); + } + case Product.pytest: { + return new pytest.ConfigurationManager(wkspace, outputChannel, installer, configSettingService); + } + case Product.nosetest: { + return new nose.ConfigurationManager(wkspace, outputChannel, installer, configSettingService); + } + default: { + throw new Error('Invalid test configuration'); + } + } +} +async function selectTestRunner(placeHolderMessage: string): Promise { + const items = [{ + label: 'unittest', + product: Product.unittest, + description: 'Standard Python test framework', + detail: 'https://docs.python.org/2/library/unittest.html' + }, + { + label: 'pytest', + product: Product.pytest, + description: 'Can run unittest (including trial) and nose test suites out of the box', + // tslint:disable-next-line:no-http-string + detail: 'http://docs.pytest.org/en/latest/' + }, + { + label: 'nose', + product: Product.nosetest, + description: 'nose framework', + detail: 'https://docs.python.org/2/library/unittest.html' + }]; + const options = { + matchOnDescription: true, + matchOnDetail: true, + placeHolder: placeHolderMessage + }; + const selectedTestRunner = await vscode.window.showQuickPick(items, options); + // tslint:disable-next-line:prefer-type-cast + return selectedTestRunner ? selectedTestRunner.product as UnitTestProduct : undefined; +} diff --git a/src/client/unittests/display/main.ts b/src/client/unittests/display/main.ts index 5383e09fd389..ab232152e295 100644 --- a/src/client/unittests/display/main.ts +++ b/src/client/unittests/display/main.ts @@ -1,13 +1,19 @@ 'use strict'; import * as vscode from 'vscode'; -import { Tests, CANCELLATION_REASON } from '../common/contracts'; import * as constants from '../../common/constants'; +import { createDeferred, isNotInstalledError } from '../../common/helpers'; +import { CANCELLATION_REASON } from '../common/constants'; import { displayTestErrorMessage } from '../common/testUtils'; -import { isNotInstalledError, createDeferred } from '../../common/helpers'; +import { Tests } from '../common/types'; export class TestResultDisplay { private statusBar: vscode.StatusBarItem; - constructor(private outputChannel: vscode.OutputChannel) { + private discoverCounter = 0; + private ticker = ['|', '/', '-', '|', '/', '-', '\\']; + private progressTimeout; + private progressPrefix: string; + // tslint:disable-next-line:no-any + constructor(private outputChannel: vscode.OutputChannel, private onDidChange: vscode.EventEmitter = null) { this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); } public dispose() { @@ -16,19 +22,29 @@ export class TestResultDisplay { public set enabled(enable: boolean) { if (enable) { this.statusBar.show(); - } - else { + } else { this.statusBar.hide(); } } - public DisplayProgressStatus(tests: Promise, debug: boolean = false) { - this.displayProgress('Running Tests', `Running Tests (Click to Stop)`, constants.Commands.Tests_Ask_To_Stop_Test); - tests + public displayProgressStatus(testRunResult: Promise, debug: boolean = false) { + this.displayProgress('Running Tests', 'Running Tests (Click to Stop)', constants.Commands.Tests_Ask_To_Stop_Test); + testRunResult .then(tests => this.updateTestRunWithSuccess(tests, debug)) .catch(this.updateTestRunWithFailure.bind(this)) // We don't care about any other exceptions returned by updateTestRunWithFailure + // tslint:disable-next-line:no-empty .catch(() => { }); } + public displayDiscoverStatus(testDiscovery: Promise) { + this.displayProgress('Discovering Tests', 'Discovering Tests (Click to Stop)', constants.Commands.Tests_Ask_To_Stop_Discovery); + return testDiscovery.then(tests => { + this.updateWithDiscoverSuccess(tests); + return tests; + }).catch(reason => { + this.updateWithDiscoverFailure(reason); + return Promise.reject(reason); + }); + } private updateTestRunWithSuccess(tests: Tests, debug: boolean = false): Tests { this.clearProgressTicker(); @@ -58,38 +74,36 @@ export class TestResultDisplay { toolTip.push(`${tests.summary.errors} Error${tests.summary.errors > 1 ? 's' : ''}`); foreColor = 'yellow'; } - this.statusBar.tooltip = toolTip.length === 0 ? 'No Tests Ran' : toolTip.join(', ') + ' (Tests)'; + this.statusBar.tooltip = toolTip.length === 0 ? 'No Tests Ran' : `${toolTip.join(', ')} (Tests)`; this.statusBar.text = statusText.length === 0 ? 'No Tests Ran' : statusText.join(' '); this.statusBar.color = foreColor; this.statusBar.command = constants.Commands.Tests_View_UI; - + if (this.onDidChange) { + this.onDidChange.fire(); + } if (statusText.length === 0 && !debug) { vscode.window.showWarningMessage('No tests ran, please check the configuration settings for the tests.'); } return tests; } + // tslint:disable-next-line:no-any private updateTestRunWithFailure(reason: any): Promise { this.clearProgressTicker(); this.statusBar.command = constants.Commands.Tests_View_UI; if (reason === CANCELLATION_REASON) { this.statusBar.text = '$(zap) Run Tests'; this.statusBar.tooltip = 'Run Tests'; - } - else { - this.statusBar.text = `$(alert) Tests Failed`; + } else { + this.statusBar.text = '$(alert) Tests Failed'; this.statusBar.tooltip = 'Running Tests Failed'; displayTestErrorMessage('There was an error in running the tests.'); } return Promise.reject(reason); } - private discoverCounter = 0; - private ticker = ['|', '/', '-', '|', '/', '-', '\\']; - private progressTimeout; - private progressPrefix: string; private displayProgress(message: string, tooltip: string, command: string) { - this.progressPrefix = this.statusBar.text = '$(stop) ' + message; + this.progressPrefix = this.statusBar.text = `$(stop) ${message}`; this.statusBar.command = command; this.statusBar.tooltip = tooltip; this.statusBar.show(); @@ -97,7 +111,7 @@ export class TestResultDisplay { this.progressTimeout = setInterval(() => this.updateProgressTicker(), 150); } private updateProgressTicker() { - let text = `${this.progressPrefix} ${this.ticker[this.discoverCounter % 7]}`; + const text = `${this.progressPrefix} ${this.ticker[this.discoverCounter % 7]}`; this.discoverCounter += 1; this.statusBar.text = text; } @@ -109,21 +123,12 @@ export class TestResultDisplay { this.discoverCounter = 0; } - public DisplayDiscoverStatus(tests: Promise) { - this.displayProgress('Discovering Tests', 'Discovering Tests (Click to Stop)', constants.Commands.Tests_Ask_To_Stop_Discovery); - return tests.then(tests => { - this.updateWithDiscoverSuccess(tests); - return tests; - }).catch(reason => { - this.updateWithDiscoverFailure(reason); - return Promise.reject(reason); - }); - } - + // tslint:disable-next-line:no-any private disableTests(): Promise { + // tslint:disable-next-line:no-any const def = createDeferred(); const pythonConfig = vscode.workspace.getConfiguration('python'); - let settingsToDisable = ['unitTest.promptToConfigure', 'unitTest.pyTestEnabled', + const settingsToDisable = ['unitTest.promptToConfigure', 'unitTest.pyTestEnabled', 'unitTest.unittestEnabled', 'unitTest.nosetestsEnabled']; function disableTest() { @@ -145,32 +150,37 @@ export class TestResultDisplay { this.statusBar.tooltip = 'Run Tests'; this.statusBar.command = constants.Commands.Tests_View_UI; this.statusBar.show(); + if (this.onDidChange) { + this.onDidChange.fire(); + } if (!haveTests) { vscode.window.showInformationMessage('No tests discovered, please check the configuration settings for the tests.', 'Disable Tests').then(item => { if (item === 'Disable Tests') { + // tslint:disable-next-line:no-floating-promises this.disableTests(); } }); } } + // tslint:disable-next-line:no-any private updateWithDiscoverFailure(reason: any) { this.clearProgressTicker(); - this.statusBar.text = `$(zap) Discover Tests`; + this.statusBar.text = '$(zap) Discover Tests'; this.statusBar.tooltip = 'Discover Tests'; this.statusBar.command = constants.Commands.Tests_Discover; this.statusBar.show(); this.statusBar.color = 'yellow'; if (reason !== CANCELLATION_REASON) { - this.statusBar.text = `$(alert) Test discovery failed`; - this.statusBar.tooltip = `Discovering Tests failed (view 'Python Test Log' output panel for details)`; - // TODO: ignore this quitemode, always display the error message (inform the user) + this.statusBar.text = '$(alert) Test discovery failed'; + this.statusBar.tooltip = 'Discovering Tests failed (view \'Python Test Log\' output panel for details)'; + // TODO: ignore this quitemode, always display the error message (inform the user). if (!isNotInstalledError(reason)) { - // TODO: show an option that will invoke a command 'python.test.configureTest' or similar - // This will be hanlded by main.ts that will capture input from user and configure the tests + // TODO: show an option that will invoke a command 'python.test.configureTest' or similar. + // This will be hanlded by main.ts that will capture input from user and configure the tests. vscode.window.showErrorMessage('There was an error in discovering tests, please check the configuration settings for the tests.'); } } } -} \ No newline at end of file +} diff --git a/src/client/unittests/display/picker.ts b/src/client/unittests/display/picker.ts index de1e89e6000c..2b3d47652abf 100644 --- a/src/client/unittests/display/picker.ts +++ b/src/client/unittests/display/picker.ts @@ -1,23 +1,23 @@ -import { QuickPickItem, window } from 'vscode'; +import * as path from 'path'; +import { QuickPickItem, Uri, window } from 'vscode'; import * as vscode from 'vscode'; -import { Tests, TestFunction, FlattenedTestFunction, TestStatus } from '../common/contracts'; -import { getDiscoveredTests } from '../common/testUtils'; import * as constants from '../../common/constants'; -import * as path from 'path'; +import { CommandSource } from '../common/constants'; +import { FlattenedTestFunction, ITestCollectionStorageService, TestFile, TestFunction, Tests, TestStatus, TestsToRun } from '../common/types'; export class TestDisplay { - constructor() { - } - public displayStopTestUI(message: string) { + constructor(private testCollectionStorage: ITestCollectionStorageService) { } + public displayStopTestUI(workspace: Uri, message: string) { window.showQuickPick([message]).then(item => { if (item === message) { - vscode.commands.executeCommand(constants.Commands.Tests_Stop); + vscode.commands.executeCommand(constants.Commands.Tests_Stop, workspace); } }); } - public displayTestUI(rootDirectory: string) { - const tests = getDiscoveredTests(); - window.showQuickPick(buildItems(rootDirectory, tests), { matchOnDescription: true, matchOnDetail: true }).then(onItemSelected); + public displayTestUI(cmdSource: CommandSource, wkspace: Uri) { + const tests = this.testCollectionStorage.getTests(wkspace); + window.showQuickPick(buildItems(tests), { matchOnDescription: true, matchOnDetail: true }) + .then(item => onItemSelected(cmdSource, wkspace, item, false)); } public selectTestFunction(rootDirectory: string, tests: Tests): Promise { return new Promise((resolve, reject) => { @@ -30,12 +30,24 @@ export class TestDisplay { }, reject); }); } - public displayFunctionTestPickerUI(rootDirectory: string, fileName: string, testFunctions: TestFunction[], debug?: boolean) { - const tests = getDiscoveredTests(); + public selectTestFile(rootDirectory: string, tests: Tests): Promise { + return new Promise((resolve, reject) => { + window.showQuickPick(buildItemsForTestFiles(rootDirectory, tests.testFiles), { matchOnDescription: true, matchOnDetail: true }) + .then(item => { + if (item && item.testFile) { + return resolve(item.testFile); + } + return reject(); + }, reject); + }); + } + public displayFunctionTestPickerUI(cmdSource: CommandSource, wkspace: Uri, rootDirectory: string, file: Uri, testFunctions: TestFunction[], debug?: boolean) { + const tests = this.testCollectionStorage.getTests(wkspace); if (!tests) { return; } - const testFile = tests.testFiles.find(file => file.name === fileName || file.fullPath === fileName); + const fileName = file.fsPath; + const testFile = tests.testFiles.find(item => item.name === fileName || item.fullPath === fileName); if (!testFile) { return; } @@ -44,9 +56,9 @@ export class TestDisplay { testFunctions.some(testFunc => testFunc.nameToRun === fn.testFunction.nameToRun); }); - window.showQuickPick(buildItemsForFunctions(rootDirectory, flattenedFunctions), + window.showQuickPick(buildItemsForFunctions(rootDirectory, flattenedFunctions, undefined, undefined, debug), { matchOnDescription: true, matchOnDetail: true }).then(testItem => { - return onItemSelected(testItem, debug); + return onItemSelected(cmdSource, wkspace, testItem, debug); }); } } @@ -61,7 +73,8 @@ enum Type { RunMethod = 6, ViewTestOutput = 7, Null = 8, - SelectAndRunMethod = 9 + SelectAndRunMethod = 9, + DebugMethod = 10 } const statusIconMapping = new Map(); statusIconMapping.set(TestStatus.Pass, constants.Octicons.Test_Pass); @@ -69,10 +82,16 @@ statusIconMapping.set(TestStatus.Fail, constants.Octicons.Test_Fail); statusIconMapping.set(TestStatus.Error, constants.Octicons.Test_Error); statusIconMapping.set(TestStatus.Skipped, constants.Octicons.Test_Skip); -interface TestItem extends QuickPickItem { +type TestItem = QuickPickItem & { type: Type; fn?: FlattenedTestFunction; -} +}; + +type TestFileItem = QuickPickItem & { + type: Type; + testFile?: TestFile; +}; + function getSummary(tests?: Tests) { if (!tests || !tests.summary) { return ''; @@ -86,22 +105,20 @@ function getSummary(tests?: Tests) { } if (tests.summary.errors > 0) { const plural = tests.summary.errors === 1 ? '' : 's'; - statusText.push(`${constants.Octicons.Test_Error} ${tests.summary.errors} Error` + plural); + statusText.push(`${constants.Octicons.Test_Error} ${tests.summary.errors} Error${plural}`); } if (tests.summary.skipped > 0) { statusText.push(`${constants.Octicons.Test_Skip} ${tests.summary.skipped} Skipped`); } return statusText.join(', ').trim(); } -function buildItems(rootDirectory: string, tests?: Tests): TestItem[] { +function buildItems(tests?: Tests): TestItem[] { const items: TestItem[] = []; items.push({ description: '', label: 'Run All Unit Tests', type: Type.RunAll }); - if (!tests || tests.testFiles.length === 0) { - items.push({ description: '', label: 'Discover Unit Tests', type: Type.ReDiscover }); - } + items.push({ description: '', label: 'Discover Unit Tests', type: Type.ReDiscover }); items.push({ description: '', label: 'Run Unit Test Method ...', type: Type.SelectAndRunMethod }); - let summary = getSummary(tests); + const summary = getSummary(tests); items.push({ description: '', label: 'View Unit Test Output', type: Type.ViewTestOutput, detail: summary }); if (tests && tests.summary.failures > 0) { @@ -117,8 +134,8 @@ statusSortPrefix[TestStatus.Fail] = '2'; statusSortPrefix[TestStatus.Skipped] = '3'; statusSortPrefix[TestStatus.Pass] = '4'; -function buildItemsForFunctions(rootDirectory: string, tests: FlattenedTestFunction[], sortBasedOnResults: boolean = false, displayStatusIcons: boolean = false): TestItem[] { - let functionItems: TestItem[] = []; +function buildItemsForFunctions(rootDirectory: string, tests: FlattenedTestFunction[], sortBasedOnResults: boolean = false, displayStatusIcons: boolean = false, debug: boolean = false): TestItem[] { + const functionItems: TestItem[] = []; tests.forEach(fn => { let icon = ''; if (displayStatusIcons && statusIconMapping.has(fn.testFunction.status)) { @@ -129,7 +146,7 @@ function buildItemsForFunctions(rootDirectory: string, tests: FlattenedTestFunct description: '', detail: path.relative(rootDirectory, fn.parentTestFile.fullPath), label: icon + fn.testFunction.name, - type: Type.RunMethod, + type: debug === true ? Type.DebugMethod : Type.RunMethod, fn: fn }); }); @@ -150,12 +167,34 @@ function buildItemsForFunctions(rootDirectory: string, tests: FlattenedTestFunct }); return functionItems; } -function onItemSelected(selection: TestItem, debug?: boolean) { +function buildItemsForTestFiles(rootDirectory: string, testFiles: TestFile[]): TestFileItem[] { + const fileItems: TestFileItem[] = testFiles.map(testFile => { + return { + description: '', + detail: path.relative(rootDirectory, testFile.fullPath), + type: Type.RunFile, + label: path.basename(testFile.fullPath), + testFile: testFile + }; + }); + fileItems.sort((a, b) => { + if (a.detail < b.detail) { + return -1; + } + if (a.detail > b.detail) { + return 1; + } + return 0; + }); + return fileItems; +} +function onItemSelected(cmdSource: CommandSource, wkspace: Uri, selection: TestItem, debug?: boolean) { if (!selection || typeof selection.type !== 'number') { return; } let cmd = ''; - let args = []; + // tslint:disable-next-line:no-any + const args: any[] = [cmdSource, wkspace]; switch (selection.type) { case Type.Null: { return; @@ -182,10 +221,21 @@ function onItemSelected(selection: TestItem, debug?: boolean) { } case Type.RunMethod: { cmd = constants.Commands.Tests_Run; - args.push(selection.fn); + // tslint:disable-next-line:prefer-type-cast + args.push({ testFunction: [selection.fn.testFunction] } as TestsToRun); + break; + } + case Type.DebugMethod: { + cmd = constants.Commands.Tests_Debug; + // tslint:disable-next-line:prefer-type-cast + args.push({ testFunction: [selection.fn.testFunction] } as TestsToRun); + args.push(true); break; } + default: { + return; + } } vscode.commands.executeCommand(cmd, ...args); -} \ No newline at end of file +} diff --git a/src/client/unittests/main.ts b/src/client/unittests/main.ts index 3e52bc05f5f6..bf6c30f223a0 100644 --- a/src/client/unittests/main.ts +++ b/src/client/unittests/main.ts @@ -1,123 +1,245 @@ 'use strict'; +import { Uri, window, workspace } from 'vscode'; import * as vscode from 'vscode'; -import { TestsToRun, TestStatus, TestFunction, FlattenedTestFunction, CANCELLATION_REASON } from './common/contracts'; -import * as nosetests from './nosetest/main'; -import * as pytest from './pytest/main'; -import * as unittest from './unittest/main'; -import { resolveValueAsTestToRun, getDiscoveredTests } from './common/testUtils'; -import { BaseTestManager } from './common/baseTestManager'; import { PythonSettings } from '../common/configSettings'; -import { TestResultDisplay } from './display/main'; -import { TestDisplay } from './display/picker'; import * as constants from '../common/constants'; +import { PythonSymbolProvider } from '../providers/symbolProvider'; +import { UNITTEST_STOP, UNITTEST_VIEW_OUTPUT } from '../telemetry/constants'; +import { sendTelemetryEvent } from '../telemetry/index'; import { activateCodeLenses } from './codeLenses/main'; -import { displayTestFrameworkError, displayPromptToEnableTests } from './configuration'; +import { BaseTestManager } from './common/baseTestManager'; +import { CANCELLATION_REASON, CommandSource } from './common/constants'; +import { DebugLauncher } from './common/debugLauncher'; +import { TestCollectionStorageService } from './common/storageService'; +import { TestManagerServiceFactory } from './common/testManagerServiceFactory'; +import { TestResultsService } from './common/testResultsService'; +import { selectTestWorkspace, TestsHelper } from './common/testUtils'; +import { ITestCollectionStorageService, IWorkspaceTestManagerService, TestFile, TestFunction, TestStatus, TestsToRun } from './common/types'; +import { WorkspaceTestManagerService } from './common/workspaceTestManagerService'; +import { displayTestFrameworkError } from './configuration'; +import { TestResultDisplay } from './display/main'; +import { TestDisplay } from './display/picker'; -const settings = PythonSettings.getInstance(); -let testManager: BaseTestManager; -let pyTestManager: pytest.TestManager; -let unittestManager: unittest.TestManager; -let nosetestManager: nosetests.TestManager; +let workspaceTestManagerService: IWorkspaceTestManagerService; let testResultDisplay: TestResultDisplay; let testDisplay: TestDisplay; let outChannel: vscode.OutputChannel; +const onDidChange: vscode.EventEmitter = new vscode.EventEmitter(); +let testCollectionStorage: ITestCollectionStorageService; -export function activate(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel) { +export function activate(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel, symboldProvider: PythonSymbolProvider) { context.subscriptions.push({ dispose: dispose }); outChannel = outputChannel; - let disposables = registerCommands(); + const disposables = registerCommands(); context.subscriptions.push(...disposables); - if (settings.unitTest.nosetestsEnabled || settings.unitTest.pyTestEnabled || settings.unitTest.unittestEnabled) { - // Ignore the exceptions returned - // This function is invoked via a command which will be invoked else where in the extension - discoverTests(true).catch(() => { - // Ignore the errors - }); - } + testCollectionStorage = new TestCollectionStorageService(); + const testResultsService = new TestResultsService(); + const testsHelper = new TestsHelper(); + const debugLauncher = new DebugLauncher(); + const testManagerServiceFactory = new TestManagerServiceFactory(outChannel, testCollectionStorage, testResultsService, testsHelper, debugLauncher); + workspaceTestManagerService = new WorkspaceTestManagerService(outChannel, testManagerServiceFactory); + + context.subscriptions.push(autoResetTests()); + context.subscriptions.push(activateCodeLenses(onDidChange, symboldProvider, testCollectionStorage)); + context.subscriptions.push(vscode.workspace.onDidSaveTextDocument(onDocumentSaved)); - settings.addListener('change', onConfigChanged); - context.subscriptions.push(activateCodeLenses()); + autoDiscoverTests(); } -function dispose() { - if (pyTestManager) { - pyTestManager.dispose(); + +async function getTestManager(displayTestNotConfiguredMessage: boolean, resource?: Uri): Promise { + let wkspace: Uri | undefined; + if (resource) { + const wkspaceFolder = workspace.getWorkspaceFolder(resource); + wkspace = wkspaceFolder ? wkspaceFolder.uri : undefined; + } else { + wkspace = await selectTestWorkspace(); + } + if (!wkspace) { + return; + } + const testManager = workspaceTestManagerService.getTestManager(wkspace); + if (testManager) { + return testManager; + } + if (displayTestNotConfiguredMessage) { + await displayTestFrameworkError(wkspace, outChannel); + } +} +let timeoutId: number; +async function onDocumentSaved(doc: vscode.TextDocument): Promise { + const testManager = await getTestManager(false, doc.uri); + if (!testManager) { + return; + } + const tests = await testManager.discoverTests(CommandSource.auto, false, true); + if (!tests || !Array.isArray(tests.testFiles) || tests.testFiles.length === 0) { + return; } - if (nosetestManager) { - nosetestManager.dispose(); + if (tests.testFiles.findIndex((f: TestFile) => f.fullPath === doc.uri.fsPath) === -1) { + return; } - if (unittestManager) { - unittestManager.dispose(); + + if (timeoutId) { + clearTimeout(timeoutId); } + timeoutId = setTimeout(() => discoverTests(CommandSource.auto, doc.uri, true), 1000); +} + +function dispose() { + workspaceTestManagerService.dispose(); + testCollectionStorage.dispose(); } function registerCommands(): vscode.Disposable[] { const disposables = []; - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Discover, () => { - // Ignore the exceptions returned - // This command will be invoked else where in the extension - discoverTests(true).catch(() => { return null; }); + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Discover, (cmdSource: CommandSource = CommandSource.commandPalette, resource?: Uri) => { + // Ignore the exceptions returned. + // This command will be invoked else where in the extension. + // tslint:disable-next-line:no-empty + discoverTests(cmdSource, resource, true, true).catch(() => { }); })); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run_Failed, () => runTestsImpl(true))); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run, (testId) => runTestsImpl(testId))); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Debug, (testId) => runTestsImpl(testId, true))); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_View_UI, () => displayUI())); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Picker_UI, (file, testFunctions) => displayPickerUI(file, testFunctions))); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Picker_UI_Debug, (file, testFunctions) => displayPickerUI(file, testFunctions, true))); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Stop, () => stopTests())); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_ViewOutput, () => outChannel.show())); + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run_Failed, (cmdSource: CommandSource = CommandSource.commandPalette, resource: Uri) => runTestsImpl(cmdSource, resource, undefined, true))); + // tslint:disable-next-line:no-unnecessary-callback-wrapper + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run, (cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testToRun?: TestsToRun) => runTestsImpl(cmdSource, file, testToRun))); + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Debug, (cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testToRun: TestsToRun) => runTestsImpl(cmdSource, file, testToRun, false, true))); + // tslint:disable-next-line:no-unnecessary-callback-wrapper + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_View_UI, () => displayUI(CommandSource.commandPalette))); + // tslint:disable-next-line:no-unnecessary-callback-wrapper + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Picker_UI, (cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testFunctions: TestFunction[]) => displayPickerUI(cmdSource, file, testFunctions))); + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Picker_UI_Debug, (cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testFunctions: TestFunction[]) => displayPickerUI(cmdSource, file, testFunctions, true))); + // tslint:disable-next-line:no-unnecessary-callback-wrapper + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Stop, (resource: Uri) => stopTests(resource))); + // tslint:disable-next-line:no-unnecessary-callback-wrapper + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_ViewOutput, (cmdSource: CommandSource = CommandSource.commandPalette) => viewOutput(cmdSource))); disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Ask_To_Stop_Discovery, () => displayStopUI('Stop discovering tests'))); disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Ask_To_Stop_Test, () => displayStopUI('Stop running tests'))); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Select_And_Run_Method, () => selectAndRunTestMethod())); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Select_And_Debug_Method, () => selectAndRunTestMethod(true))); + // tslint:disable-next-line:no-unnecessary-callback-wrapper + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Select_And_Run_Method, (cmdSource: CommandSource = CommandSource.commandPalette, resource: Uri) => selectAndRunTestMethod(cmdSource, resource))); + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Select_And_Debug_Method, (cmdSource: CommandSource = CommandSource.commandPalette, resource: Uri) => selectAndRunTestMethod(cmdSource, resource, true))); + // tslint:disable-next-line:no-unnecessary-callback-wrapper + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Select_And_Run_File, (cmdSource: CommandSource = CommandSource.commandPalette) => selectAndRunTestFile(cmdSource))); + // tslint:disable-next-line:no-unnecessary-callback-wrapper + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run_Current_File, (cmdSource: CommandSource = CommandSource.commandPalette) => runCurrentTestFile(cmdSource))); return disposables; } -function displayUI() { - let testManager = getTestRunner(); +function viewOutput(cmdSource: CommandSource) { + sendTelemetryEvent(UNITTEST_VIEW_OUTPUT); + outChannel.show(); +} +async function displayUI(cmdSource: CommandSource) { + const testManager = await getTestManager(true); if (!testManager) { - return displayTestFrameworkError(outChannel); + return; } - testDisplay = testDisplay ? testDisplay : new TestDisplay(); - testDisplay.displayTestUI(vscode.workspace.rootPath); + testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); + testDisplay.displayTestUI(cmdSource, testManager.workspace); } -function displayPickerUI(file: string, testFunctions: TestFunction[], debug?: boolean) { - let testManager = getTestRunner(); +async function displayPickerUI(cmdSource: CommandSource, file: Uri, testFunctions: TestFunction[], debug?: boolean) { + const testManager = await getTestManager(true, file); if (!testManager) { - return displayTestFrameworkError(outChannel); + return; } - testDisplay = testDisplay ? testDisplay : new TestDisplay(); - testDisplay.displayFunctionTestPickerUI(vscode.workspace.rootPath, file, testFunctions, debug); + testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); + testDisplay.displayFunctionTestPickerUI(cmdSource, testManager.workspace, testManager.workingDirectory, file, testFunctions, debug); } -function selectAndRunTestMethod(debug?: boolean) { - let testManager = getTestRunner(); +async function selectAndRunTestMethod(cmdSource: CommandSource, resource: Uri, debug?: boolean) { + const testManager = await getTestManager(true, resource); if (!testManager) { - return displayTestFrameworkError(outChannel); - } - testManager.discoverTests(true, true).then(() => { - const tests = getDiscoveredTests(); - testDisplay = testDisplay ? testDisplay : new TestDisplay(); - testDisplay.selectTestFunction(vscode.workspace.rootPath, tests).then(testFn => { - runTestsImpl(testFn, debug); - }).catch(() => { }); + return; + } + try { + await testManager.discoverTests(cmdSource, true, true, true); + } catch (ex) { + return; + } + + const tests = testCollectionStorage.getTests(testManager.workspace); + testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); + const selectedTestFn = await testDisplay.selectTestFunction(testManager.workspace.fsPath, tests); + if (!selectedTestFn) { + return; + } + // tslint:disable-next-line:prefer-type-cast + await runTestsImpl(cmdSource, testManager.workspace, { testFunction: [selectedTestFn.testFunction] } as TestsToRun, debug); +} +async function selectAndRunTestFile(cmdSource: CommandSource) { + const testManager = await getTestManager(true); + if (!testManager) { + return; + } + try { + await testManager.discoverTests(cmdSource, true, true, true); + } catch (ex) { + return; + } + + const tests = testCollectionStorage.getTests(testManager.workspace); + testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); + const selectedFile = await testDisplay.selectTestFile(testManager.workspace.fsPath, tests); + if (!selectedFile) { + return; + } + // tslint:disable-next-line:prefer-type-cast + await runTestsImpl(cmdSource, testManager.workspace, { testFile: [selectedFile] } as TestsToRun); +} +async function runCurrentTestFile(cmdSource: CommandSource) { + if (!window.activeTextEditor) { + return; + } + const testManager = await getTestManager(true, window.activeTextEditor.document.uri); + if (!testManager) { + return; + } + try { + await testManager.discoverTests(cmdSource, true, true, true); + } catch (ex) { + return; + } + const tests = testCollectionStorage.getTests(testManager.workspace); + const testFiles = tests.testFiles.filter(testFile => { + return testFile.fullPath === window.activeTextEditor.document.uri.fsPath; }); + if (testFiles.length < 1) { + return; + } + // tslint:disable-next-line:prefer-type-cast + await runTestsImpl(cmdSource, testManager.workspace, { testFile: [testFiles[0]] } as TestsToRun); } -function displayStopUI(message: string) { - let testManager = getTestRunner(); +async function displayStopUI(message: string) { + const testManager = await getTestManager(true); if (!testManager) { - return displayTestFrameworkError(outChannel); + return; } - testDisplay = testDisplay ? testDisplay : new TestDisplay(); - testDisplay.displayStopTestUI(message); + testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); + testDisplay.displayStopTestUI(testManager.workspace, message); } -let uniTestSettingsString = JSON.stringify(settings.unitTest); +let uniTestSettingsString: string; +function autoResetTests() { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length > 1) { + // tslint:disable-next-line:no-empty + return { dispose: () => { } }; + } + + const settings = PythonSettings.getInstance(); + uniTestSettingsString = JSON.stringify(settings.unitTest); + return workspace.onDidChangeConfiguration(() => setTimeout(onConfigChanged, 1000)); +} function onConfigChanged() { - // Possible that a test framework has been enabled or some settings have changed - // Meaning we need to re-load the discovered tests (as something could have changed) + // If there's one workspace, then stop the tests and restart, + // else let the user do this manually. + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length > 1) { + return; + } + const settings = PythonSettings.getInstance(); + + // Possible that a test framework has been enabled or some settings have changed. + // Meaning we need to re-load the discovered tests (as something could have changed). const newSettings = JSON.stringify(settings.unitTest); if (uniTestSettingsString === newSettings) { return; @@ -128,70 +250,48 @@ function onConfigChanged() { if (testResultDisplay) { testResultDisplay.enabled = false; } - - if (testManager) { - testManager.stop(); - testManager = null; - } - if (pyTestManager) { - pyTestManager.dispose(); - pyTestManager = null; - } - if (nosetestManager) { - nosetestManager.dispose(); - nosetestManager = null; - } - if (unittestManager) { - unittestManager.dispose(); - unittestManager = null; - } + workspaceTestManagerService.dispose(); return; } - if (testResultDisplay) { testResultDisplay.enabled = true; } - - // No need to display errors - if (settings.unitTest.nosetestsEnabled || settings.unitTest.pyTestEnabled || settings.unitTest.unittestEnabled) { - discoverTests(true); - } + autoDiscoverTests(); } -function getTestRunner() { - const rootDirectory = vscode.workspace.rootPath; - if (settings.unitTest.nosetestsEnabled) { - return nosetestManager = nosetestManager ? nosetestManager : new nosetests.TestManager(rootDirectory, outChannel); - } - else if (settings.unitTest.pyTestEnabled) { - return pyTestManager = pyTestManager ? pyTestManager : new pytest.TestManager(rootDirectory, outChannel); +function autoDiscoverTests() { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length > 1) { + return; } - else if (settings.unitTest.unittestEnabled) { - return unittestManager = unittestManager ? unittestManager : new unittest.TestManager(rootDirectory, outChannel); + const settings = PythonSettings.getInstance(); + if (!settings.unitTest.nosetestsEnabled && !settings.unitTest.pyTestEnabled && !settings.unitTest.unittestEnabled) { + return; } - return null; -} -function stopTests() { - let testManager = getTestRunner(); + // No need to display errors. + // tslint:disable-next-line:no-empty + discoverTests(CommandSource.auto, workspace.workspaceFolders[0].uri, true).catch(() => { }); +} +async function stopTests(resource: Uri) { + sendTelemetryEvent(UNITTEST_STOP); + const testManager = await getTestManager(true, resource); if (testManager) { testManager.stop(); } } -function discoverTests(ignoreCache?: boolean) { - let testManager = getTestRunner(); +async function discoverTests(cmdSource: CommandSource, resource?: Uri, ignoreCache?: boolean, userInitiated?: boolean) { + const testManager = await getTestManager(true, resource); if (!testManager) { - displayTestFrameworkError(outChannel); - return Promise.resolve(null); + return; } if (testManager && (testManager.status !== TestStatus.Discovering && testManager.status !== TestStatus.Running)) { - testResultDisplay = testResultDisplay ? testResultDisplay : new TestResultDisplay(outChannel); - return testResultDisplay.DisplayDiscoverStatus(testManager.discoverTests(ignoreCache)); - } - else { - return Promise.resolve(null); + testResultDisplay = testResultDisplay ? testResultDisplay : new TestResultDisplay(outChannel, onDidChange); + const discoveryPromise = testManager.discoverTests(cmdSource, ignoreCache, false, userInitiated); + testResultDisplay.displayDiscoverStatus(discoveryPromise); + await discoveryPromise; } } +// tslint:disable-next-line:no-any function isTestsToRun(arg: any): arg is TestsToRun { if (arg && arg.testFunction && Array.isArray(arg.testFunction)) { return true; @@ -204,45 +304,21 @@ function isTestsToRun(arg: any): arg is TestsToRun { } return false; } -function isUri(arg: any): arg is vscode.Uri { - return arg && arg.fsPath && typeof arg.fsPath === 'string'; -} -function isFlattenedTestFunction(arg: any): arg is FlattenedTestFunction { - return arg && arg.testFunction && typeof arg.xmlClassName === 'string' && - arg.parentTestFile && typeof arg.testFunction.name === 'string'; -} -function identifyTestType(rootDirectory: string, arg?: vscode.Uri | TestsToRun | boolean | FlattenedTestFunction): TestsToRun | Boolean { - if (typeof arg === 'boolean') { - return arg === true; - } - if (isTestsToRun(arg)) { - return arg; - } - if (isFlattenedTestFunction(arg)) { - return { testFunction: [arg.testFunction] }; - } - if (isUri(arg)) { - return resolveValueAsTestToRun(arg.fsPath, rootDirectory); - } - return null; -} -function runTestsImpl(arg?: vscode.Uri | TestsToRun | boolean | FlattenedTestFunction, debug: boolean = false) { - let testManager = getTestRunner(); +async function runTestsImpl(cmdSource: CommandSource, resource?: Uri, testsToRun?: TestsToRun, runFailedTests?: boolean, debug: boolean = false) { + const testManager = await getTestManager(true, resource); if (!testManager) { - return displayTestFrameworkError(outChannel); + return; } - // lastRanTests = testsToRun; - let runInfo = identifyTestType(vscode.workspace.rootPath, arg); - - testResultDisplay = testResultDisplay ? testResultDisplay : new TestResultDisplay(outChannel); - - let runPromise = testManager.runTest(runInfo, debug).catch(reason => { - if (reason !== CANCELLATION_REASON) { - outChannel.appendLine('Error: ' + reason); - } - return Promise.reject(reason); - }); + testResultDisplay = testResultDisplay ? testResultDisplay : new TestResultDisplay(outChannel, onDidChange); + const promise = testManager.runTest(cmdSource, testsToRun, runFailedTests, debug) + .catch(reason => { + if (reason !== CANCELLATION_REASON) { + outChannel.appendLine(`Error: ${reason}`); + } + return Promise.reject(reason); + }); - testResultDisplay.DisplayProgressStatus(runPromise, debug); -} \ No newline at end of file + testResultDisplay.displayProgressStatus(promise, debug); + await promise; +} diff --git a/src/client/unittests/nosetest/collector.ts b/src/client/unittests/nosetest/collector.ts index 54bb7e6f85d1..3a202654e6fd 100644 --- a/src/client/unittests/nosetest/collector.ts +++ b/src/client/unittests/nosetest/collector.ts @@ -1,14 +1,13 @@ 'use strict'; -import * as path from 'path'; -import { execPythonFile } from './../../common/utils'; -import { TestFile, TestSuite, TestFunction, Tests } from '../common/contracts'; import * as os from 'os'; -import { extractBetweenDelimiters, convertFileToPackage, flattenTestFiles } from '../common/testUtils'; +import * as path from 'path'; import { CancellationToken } from 'vscode'; +import { OutputChannel, Uri } from 'vscode'; import { PythonSettings } from '../../common/configSettings'; -import { OutputChannel } from 'vscode'; +import { convertFileToPackage, extractBetweenDelimiters } from '../common/testUtils'; +import { ITestsHelper, TestFile, TestFunction, Tests, TestSuite } from '../common/types'; +import { execPythonFile } from './../../common/utils'; -const pythonSettings = PythonSettings.getInstance(); const NOSE_WANT_FILE_PREFIX = 'nose.selector: DEBUG: wantFile '; const NOSE_WANT_FILE_SUFFIX = '.py? True'; const NOSE_WANT_FILE_SUFFIX_WITHOUT_EXT = '? True'; @@ -21,7 +20,7 @@ const argsToExcludeForDiscovery = ['-v', '--verbose', '--failed', '--process-restartworker', '--with-xunit']; const settingsInArgsToExcludeForDiscovery = ['--verbosity']; -export function discoverTests(rootDirectory: string, args: string[], token: CancellationToken, ignoreCache: boolean, outChannel: OutputChannel): Promise { +export function discoverTests(rootDirectory: string, args: string[], token: CancellationToken, ignoreCache: boolean, outChannel: OutputChannel, testsHelper: ITestsHelper): Promise { let logOutputLines: string[] = ['']; let testFiles: TestFile[] = []; @@ -45,8 +44,7 @@ export function discoverTests(rootDirectory: string, args: string[], token: Canc // and starts with nose.selector: DEBUG: want if (logOutputLines[lastLineIndex].endsWith('? True')) { logOutputLines.push(''); - } - else { + } else { // We don't need this line logOutputLines[lastLineIndex] = ''; } @@ -78,14 +76,14 @@ export function discoverTests(rootDirectory: string, args: string[], token: Canc }); } - return execPythonFile(pythonSettings.unitTest.nosetestPath, args.concat(['--collect-only', '-vvv']), rootDirectory, true) + return execPythonFile(rootDirectory, PythonSettings.getInstance(Uri.file(rootDirectory)).unitTest.nosetestPath, args.concat(['--collect-only', '-vvv']), rootDirectory, true) .then(data => { - outChannel.appendLine(data); + outChannel.appendLine(data); processOutput(data); // Exclude tests that don't have any functions or test suites testFiles = testFiles.filter(testFile => testFile.suites.length > 0 || testFile.functions.length > 0); - return flattenTestFiles(testFiles); + return testsHelper.flattenTestFiles(testFiles); }); } @@ -116,9 +114,10 @@ function parseNoseTestModuleCollectionResult(rootDirectory: string, lines: strin } if (line.startsWith('nose.selector: DEBUG: wantClass ? True'); + const name = extractBetweenDelimiters(line, 'nose.selector: DEBUG: wantClass ? True'); + const clsName = path.extname(name).substring(1); const testSuite: TestSuite = { - name: path.extname(name).substring(1), nameToRun: fileName + `:${name}`, + name: clsName, nameToRun: `${fileName}:${clsName}`, functions: [], suites: [], xmlName: name, time: 0, isUnitTest: false, isInstance: false, functionsFailed: 0, functionsPassed: 0 }; @@ -126,7 +125,7 @@ function parseNoseTestModuleCollectionResult(rootDirectory: string, lines: strin return; } if (line.startsWith('nose.selector: DEBUG: wantClass ')) { - let name = extractBetweenDelimiters(line, 'nose.selector: DEBUG: wantClass ', '? True'); + const name = extractBetweenDelimiters(line, 'nose.selector: DEBUG: wantClass ', '? True'); const testSuite: TestSuite = { name: path.extname(name).substring(1), nameToRun: `${fileName}:.${name}`, functions: [], suites: [], xmlName: name, time: 0, isUnitTest: false, @@ -144,7 +143,8 @@ function parseNoseTestModuleCollectionResult(rootDirectory: string, lines: strin time: 0, functionsFailed: 0, functionsPassed: 0 }; - let cls = testFile.suites.find(suite => suite.name === clsName); + // tslint:disable-next-line:no-non-null-assertion + const cls = testFile.suites.find(suite => suite.name === clsName)!; cls.functions.push(fn); return; } diff --git a/src/client/unittests/nosetest/main.ts b/src/client/unittests/nosetest/main.ts index 1f3def92cfb8..f670fb2333a3 100644 --- a/src/client/unittests/nosetest/main.ts +++ b/src/client/unittests/nosetest/main.ts @@ -1,31 +1,32 @@ 'use strict'; -import { PythonSettings } from '../../common/configSettings'; import { OutputChannel } from 'vscode'; -import { TestsToRun, Tests } from '../common/contracts'; import * as vscode from 'vscode'; -import { discoverTests } from './collector'; +import { PythonSettings } from '../../common/configSettings'; +import { Product } from '../../common/installer'; import { BaseTestManager } from '../common/baseTestManager'; +import { ITestCollectionStorageService, ITestDebugLauncher, ITestResultsService, ITestsHelper, Tests, TestsToRun } from '../common/types'; +import { discoverTests } from './collector'; import { runTest } from './runner'; -import { Product } from '../../common/installer'; - -const settings = PythonSettings.getInstance(); export class TestManager extends BaseTestManager { - constructor(rootDirectory: string, outputChannel: vscode.OutputChannel) { - super('nosetest', Product.nosetest, rootDirectory, outputChannel); + constructor(rootDirectory: string, outputChannel: vscode.OutputChannel, + testCollectionStorage: ITestCollectionStorageService, + testResultsService: ITestResultsService, testsHelper: ITestsHelper, private debugLauncher: ITestDebugLauncher) { + super('nosetest', Product.nosetest, rootDirectory, outputChannel, testCollectionStorage, testResultsService, testsHelper); } - discoverTestsImpl(ignoreCache: boolean): Promise { - let args = settings.unitTest.nosetestArgs.slice(0); - return discoverTests(this.rootDirectory, args, this.cancellationToken, ignoreCache, this.outputChannel); + public discoverTestsImpl(ignoreCache: boolean): Promise { + const args = this.settings.unitTest.nosetestArgs.slice(0); + return discoverTests(this.rootDirectory, args, this.testDiscoveryCancellationToken, ignoreCache, this.outputChannel, this.testsHelper); } - runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { - let args = settings.unitTest.nosetestArgs.slice(0); + // tslint:disable-next-line:no-any + public runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { + const args = this.settings.unitTest.nosetestArgs.slice(0); if (runFailedTests === true && args.indexOf('--failed') === -1) { args.push('--failed'); } if (!runFailedTests && args.indexOf('--with-id') === -1) { args.push('--with-id'); } - return runTest(this.rootDirectory, tests, args, testsToRun, this.cancellationToken, this.outputChannel, debug); + return runTest(this.testResultsService, this.debugLauncher, this.rootDirectory, tests, args, testsToRun, this.testRunnerCancellationToken, this.outputChannel, debug); } } diff --git a/src/client/unittests/nosetest/runner.ts b/src/client/unittests/nosetest/runner.ts index c3edb74780f3..ec67b2adc656 100644 --- a/src/client/unittests/nosetest/runner.ts +++ b/src/client/unittests/nosetest/runner.ts @@ -1,22 +1,17 @@ 'use strict'; +import * as path from 'path'; +import { CancellationToken, OutputChannel, Uri } from 'vscode'; +import { PythonSettings } from '../../common/configSettings'; import { createTemporaryFile } from '../../common/helpers'; -import { OutputChannel, CancellationToken } from 'vscode'; -import { TestsToRun, Tests } from '../common/contracts'; -import { updateResults } from '../common/testUtils'; -import { updateResultsFromXmlLogFile, PassCalculationFormulae } from '../common/xUnitParser'; import { run } from '../common/runner'; -import { PythonSettings } from '../../common/configSettings'; -import * as vscode from 'vscode'; -import { execPythonFile } from './../../common/utils'; -import { createDeferred } from './../../common/helpers'; -import * as os from 'os'; -import * as path from 'path'; +import { ITestDebugLauncher, ITestResultsService, Tests, TestsToRun } from '../common/types'; +import { PassCalculationFormulae, updateResultsFromXmlLogFile } from '../common/xUnitParser'; -const pythonSettings = PythonSettings.getInstance(); const WITH_XUNIT = '--with-xunit'; const XUNIT_FILE = '--xunit-file'; -export function runTest(rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { +// tslint:disable-next-line:no-any +export function runTest(testResultsService: ITestResultsService, debugLauncher: ITestDebugLauncher, rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { let testPaths = []; if (testsToRun && testsToRun.testFolder) { testPaths = testPaths.concat(testsToRun.testFolder.map(f => f.nameToRun)); @@ -32,6 +27,7 @@ export function runTest(rootDirectory: string, tests: Tests, args: string[], tes } let xmlLogFile = ''; + // tslint:disable-next-line:no-empty let xmlLogFileCleanup: Function = () => { }; // Check if '--with-xunit' is in args list @@ -41,7 +37,7 @@ export function runTest(rootDirectory: string, tests: Tests, args: string[], tes } // Check if '--xunit-file' exists, if not generate random xml file - let indexOfXUnitFile = noseTestArgs.findIndex(value => value.indexOf(XUNIT_FILE) === 0); + const indexOfXUnitFile = noseTestArgs.findIndex(value => value.indexOf(XUNIT_FILE) === 0); let promiseToGetXmlLogFile: Promise; if (indexOfXUnitFile === -1) { promiseToGetXmlLogFile = createTemporaryFile('.xml').then(xmlLogResult => { @@ -51,12 +47,10 @@ export function runTest(rootDirectory: string, tests: Tests, args: string[], tes noseTestArgs.push(`${XUNIT_FILE}=${xmlLogFile}`); return xmlLogResult.filePath; }); - } - else { + } else { if (noseTestArgs[indexOfXUnitFile].indexOf('=') === -1) { xmlLogFile = noseTestArgs[indexOfXUnitFile + 1]; - } - else { + } else { xmlLogFile = noseTestArgs[indexOfXUnitFile].substring(noseTestArgs[indexOfXUnitFile].indexOf('=') + 1).trim(); } @@ -64,65 +58,19 @@ export function runTest(rootDirectory: string, tests: Tests, args: string[], tes } return promiseToGetXmlLogFile.then(() => { + const pythonSettings = PythonSettings.getInstance(Uri.file(rootDirectory)); if (debug === true) { - const def = createDeferred(); - const launchDef = createDeferred(); const testLauncherFile = path.join(__dirname, '..', '..', '..', '..', 'pythonFiles', 'PythonTools', 'testlauncher.py'); - - // start the debug adapter only once we have started the debug process - // pytestlauncherargs const nosetestlauncherargs = [rootDirectory, 'my_secret', pythonSettings.unitTest.debugPort.toString(), 'nose']; - let outputChannelShown = false; - execPythonFile(pythonSettings.pythonPath, [testLauncherFile].concat(nosetestlauncherargs).concat(noseTestArgs.concat(testPaths)), rootDirectory, true, (data: string) => { - if (data === 'READY' + os.EOL) { - // debug socket server has started - launchDef.resolve(); - } - else { - if (!outputChannelShown) { - outputChannelShown = true; - outChannel.show(); - } - outChannel.append(data); - } - }, token).catch(reason => { - if (!def.rejected && !def.resolved) { - def.reject(reason); - } - }).then(() => { - if (!def.rejected && !def.resolved) { - def.resolve(); - } - }).catch(reason => { - if (!def.rejected && !def.resolved) { - def.reject(reason); - } - }); - - launchDef.promise.then(() => { - return vscode.commands.executeCommand('vscode.startDebug', { - "name": "Debug Unit Test", - "type": "python", - "request": "attach", - "localRoot": rootDirectory, - "remoteRoot": rootDirectory, - "port": pythonSettings.unitTest.debugPort, - "secret": "my_secret", - "host": "localhost" - }); - }).catch(reason => { - if (!def.rejected && !def.resolved) { - def.reject(reason); - } - }); - - return def.promise; - } - else { - return run(pythonSettings.unitTest.nosetestPath, noseTestArgs.concat(testPaths), rootDirectory, token, outChannel); + const debuggerArgs = [testLauncherFile].concat(nosetestlauncherargs).concat(noseTestArgs.concat(testPaths)); + // tslint:disable-next-line:prefer-type-cast no-any + return debugLauncher.launchDebugger(rootDirectory, debuggerArgs, token, outChannel) as Promise; + } else { + // tslint:disable-next-line:prefer-type-cast no-any + return run(pythonSettings.unitTest.nosetestPath, noseTestArgs.concat(testPaths), rootDirectory, token, outChannel) as Promise; } }).then(() => { - return updateResultsFromLogFiles(tests, xmlLogFile); + return updateResultsFromLogFiles(tests, xmlLogFile, testResultsService); }).then(result => { xmlLogFileCleanup(); return result; @@ -132,9 +80,10 @@ export function runTest(rootDirectory: string, tests: Tests, args: string[], tes }); } -export function updateResultsFromLogFiles(tests: Tests, outputXmlFile: string): Promise { +// tslint:disable-next-line:no-any +export function updateResultsFromLogFiles(tests: Tests, outputXmlFile: string, testResultsService: ITestResultsService): Promise { return updateResultsFromXmlLogFile(tests, outputXmlFile, PassCalculationFormulae.nosetests).then(() => { - updateResults(tests); + testResultsService.updateResults(tests); return tests; }); } diff --git a/src/client/unittests/nosetest/testConfigurationManager.ts b/src/client/unittests/nosetest/testConfigurationManager.ts index 5bc42bbb2c1c..7a8b0bedf152 100644 --- a/src/client/unittests/nosetest/testConfigurationManager.ts +++ b/src/client/unittests/nosetest/testConfigurationManager.ts @@ -1,54 +1,44 @@ -import * as vscode from 'vscode'; -import { TestConfigurationManager } from '../common/testConfigurationManager'; import * as fs from 'fs'; import * as path from 'path'; +import * as vscode from 'vscode'; +import { Uri } from 'vscode'; import { Installer, Product } from '../../common/installer'; +import { TestConfigurationManager } from '../common/testConfigurationManager'; +import { ITestConfigSettingsService } from '../common/types'; export class ConfigurationManager extends TestConfigurationManager { - public enable(): Thenable { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('unitTest.nosetestsEnabled', true); - } - public disable(): Thenable { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('unitTest.nosetestsEnabled', false); + constructor(workspace: Uri, outputChannel: vscode.OutputChannel, + installer: Installer, testConfigSettingsService: ITestConfigSettingsService) { + super(workspace, Product.nosetest, outputChannel, installer, testConfigSettingsService); } - - private static configFilesExist(rootDir: string): Promise { + private static async configFilesExist(rootDir: string): Promise { const promises = ['.noserc', 'nose.cfg'].map(cfg => { return new Promise(resolve => { fs.exists(path.join(rootDir, cfg), exists => { resolve(exists ? cfg : ''); }); }); }); - return Promise.all(promises).then(values => { - return values.filter(exists => exists.length > 0); - }); + const values = await Promise.all(promises); + return values.filter(exists => exists.length > 0); } - public configure(rootDir: string): Promise { - const args = []; + // tslint:disable-next-line:no-any + public async configure(wkspace: Uri): Promise { + const args: string[] = []; const configFileOptionLabel = 'Use existing config file'; - let installer = new Installer(this.outputChannel); - return ConfigurationManager.configFilesExist(rootDir).then(configFiles => { - if (configFiles.length > 0) { - return Promise.resolve(); - } + const configFiles = await ConfigurationManager.configFilesExist(wkspace.fsPath); + // If a config file exits, there's nothing to be configured. + if (configFiles.length > 0) { + return; + } - return this.getTestDirs(rootDir).then(subDirs => { - return this.selectTestDir(rootDir, subDirs); - }).then(testDir => { - if (typeof testDir === 'string' && testDir !== configFileOptionLabel) { - args.push(testDir); - } - }); - }).then(() => { - return installer.isProductInstalled(Product.nosetest); - }).then(installed => { - if (!installed) { - return installer.installProduct(Product.nosetest); - } - }).then(() => { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('unitTest.nosetestArgs', args); - }); + const subDirs = await this.getTestDirs(wkspace.fsPath); + const testDir = await this.selectTestDir(wkspace.fsPath, subDirs); + if (typeof testDir === 'string' && testDir !== configFileOptionLabel) { + args.push(testDir); + } + const installed = await this.installer.isInstalled(Product.nosetest); + if (!installed) { + await this.installer.install(Product.nosetest); + } + await this.testConfigSettingsService.updateTestArgs(wkspace.fsPath, Product.nosetest, args); } -} \ No newline at end of file +} diff --git a/src/client/unittests/pytest/collector.ts b/src/client/unittests/pytest/collector.ts index af36b2b9fb97..4507f1fdb64c 100644 --- a/src/client/unittests/pytest/collector.ts +++ b/src/client/unittests/pytest/collector.ts @@ -1,14 +1,12 @@ 'use strict'; -import { execPythonFile } from './../../common/utils'; -import { TestFile, TestSuite, TestFunction, Tests } from '../common/contracts'; import * as os from 'os'; -import { extractBetweenDelimiters, flattenTestFiles, convertFileToPackage } from '../common/testUtils'; -import * as vscode from 'vscode'; import * as path from 'path'; -import { PythonSettings } from '../../common/configSettings'; +import * as vscode from 'vscode'; import { OutputChannel } from 'vscode'; - -const pythonSettings = PythonSettings.getInstance(); +import { PythonSettings } from '../../common/configSettings'; +import { convertFileToPackage, extractBetweenDelimiters } from '../common/testUtils'; +import { ITestsHelper, TestFile, TestFunction, Tests, TestSuite } from '../common/types'; +import { execPythonFile } from './../../common/utils'; const argsToExcludeForDiscovery = ['-x', '--exitfirst', '--fixtures-per-test', '--pdb', '--runxfail', @@ -18,10 +16,10 @@ const argsToExcludeForDiscovery = ['-x', '--exitfirst', '--disable-pytest-warnings', '-l', '--showlocals']; const settingsInArgsToExcludeForDiscovery = []; -export function discoverTests(rootDirectory: string, args: string[], token: vscode.CancellationToken, ignoreCache: boolean, outChannel: OutputChannel): Promise { +export function discoverTests(rootDirectory: string, args: string[], token: vscode.CancellationToken, ignoreCache: boolean, outChannel: OutputChannel, testsHelper: ITestsHelper): Promise { let logOutputLines: string[] = ['']; - let testFiles: TestFile[] = []; - let parentNodes: { indent: number, item: TestFile | TestSuite }[] = []; + const testFiles: TestFile[] = []; + const parentNodes: { indent: number, item: TestFile | TestSuite }[] = []; const errorLine = /==*( *)ERRORS( *)=*/; const errorFileLine = /__*( *)ERROR collecting (.*)/; const lastLineWithErrors = /==*.*/; @@ -85,14 +83,14 @@ export function discoverTests(rootDirectory: string, args: string[], token: vsco }); } - return execPythonFile(pythonSettings.unitTest.pyTestPath, args.concat(['--collect-only']), rootDirectory, false, null, token) + return execPythonFile(rootDirectory, PythonSettings.getInstance(vscode.Uri.file(rootDirectory)).unitTest.pyTestPath, args.concat(['--collect-only']), rootDirectory, false, null, token) .then(data => { outChannel.appendLine(data); processOutput(data); if (token && token.isCancellationRequested) { return Promise.reject('cancelled'); } - return flattenTestFiles(testFiles); + return testsHelper.flattenTestFiles(testFiles); }); } @@ -106,7 +104,7 @@ function parsePyTestModuleCollectionError(rootDirectory: string, lines: string[] return; } - let errorFileLine = lines[0]; + const errorFileLine = lines[0]; let fileName = errorFileLine.substring(errorFileLine.indexOf('ERROR collecting') + 'ERROR collecting'.length).trim(); fileName = fileName.substr(0, fileName.lastIndexOf(' ')); @@ -146,22 +144,23 @@ function parsePyTestModuleCollectionResult(rootDirectory: string, lines: string[ if (trimmedLine.startsWith(' 0) { - let parentNode = parentNodes[parentNodes.length - 1]; + const parentNode = parentNodes[parentNodes.length - 1]; if (parentNode.indent < indentOfCurrentItem) { return parentNode; } diff --git a/src/client/unittests/pytest/main.ts b/src/client/unittests/pytest/main.ts index 8be3477c1b81..ee53c93de207 100644 --- a/src/client/unittests/pytest/main.ts +++ b/src/client/unittests/pytest/main.ts @@ -1,26 +1,26 @@ 'use strict'; -import { PythonSettings } from '../../common/configSettings'; -import { TestsToRun, Tests } from '../common/contracts'; -import { runTest } from './runner'; import * as vscode from 'vscode'; -import { discoverTests } from './collector'; -import { BaseTestManager } from '../common/baseTestManager'; import { Product } from '../../common/installer'; +import { BaseTestManager } from '../common/baseTestManager'; +import { ITestCollectionStorageService, ITestDebugLauncher, ITestResultsService, ITestsHelper, Tests, TestsToRun } from '../common/types'; +import { discoverTests } from './collector'; +import { runTest } from './runner'; -const settings = PythonSettings.getInstance(); export class TestManager extends BaseTestManager { - constructor(rootDirectory: string, outputChannel: vscode.OutputChannel) { - super('pytest', Product.pytest, rootDirectory, outputChannel); + constructor(rootDirectory: string, outputChannel: vscode.OutputChannel, + testCollectionStorage: ITestCollectionStorageService, + testResultsService: ITestResultsService, testsHelper: ITestsHelper, private debugLauncher: ITestDebugLauncher) { + super('pytest', Product.pytest, rootDirectory, outputChannel, testCollectionStorage, testResultsService, testsHelper); } - discoverTestsImpl(ignoreCache: boolean): Promise { - let args = settings.unitTest.pyTestArgs.slice(0); - return discoverTests(this.rootDirectory, args, this.cancellationToken, ignoreCache, this.outputChannel); + public discoverTestsImpl(ignoreCache: boolean): Promise { + const args = this.settings.unitTest.pyTestArgs.slice(0); + return discoverTests(this.rootDirectory, args, this.testDiscoveryCancellationToken, ignoreCache, this.outputChannel, this.testsHelper); } - runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { - let args = settings.unitTest.pyTestArgs.slice(0); + public runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise<{}> { + const args = this.settings.unitTest.pyTestArgs.slice(0); if (runFailedTests === true && args.indexOf('--lf') === -1 && args.indexOf('--last-failed') === -1) { args.push('--last-failed'); } - return runTest(this.rootDirectory, tests, args, testsToRun, this.cancellationToken, this.outputChannel, debug); + return runTest(this.testResultsService, this.debugLauncher, this.rootDirectory, tests, args, testsToRun, this.testRunnerCancellationToken, this.outputChannel, debug); } } diff --git a/src/client/unittests/pytest/runner.ts b/src/client/unittests/pytest/runner.ts index b4f6f6bf8372..3e314b305cab 100644 --- a/src/client/unittests/pytest/runner.ts +++ b/src/client/unittests/pytest/runner.ts @@ -1,22 +1,13 @@ -/// - 'use strict'; +import * as path from 'path'; +import { CancellationToken, OutputChannel, Uri } from 'vscode'; +import { PythonSettings } from '../../common/configSettings'; import { createTemporaryFile } from '../../common/helpers'; -import { TestsToRun, Tests } from '../common/contracts'; -import { updateResults } from '../common/testUtils'; -import { CancellationToken, OutputChannel } from 'vscode'; -import { updateResultsFromXmlLogFile, PassCalculationFormulae } from '../common/xUnitParser'; import { run } from '../common/runner'; -import { PythonSettings } from '../../common/configSettings'; -import * as vscode from 'vscode'; -import { execPythonFile } from './../../common/utils'; -import { createDeferred } from './../../common/helpers'; -import * as os from 'os'; -import * as path from 'path'; - -const pythonSettings = PythonSettings.getInstance(); +import { ITestDebugLauncher, ITestResultsService, Tests, TestsToRun } from '../common/types'; +import { PassCalculationFormulae, updateResultsFromXmlLogFile } from '../common/xUnitParser'; -export function runTest(rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { +export function runTest(testResultsService: ITestResultsService, debugLauncher: ITestDebugLauncher, rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { let testPaths = []; if (testsToRun && testsToRun.testFolder) { testPaths = testPaths.concat(testsToRun.testFolder.map(f => f.nameToRun)); @@ -42,65 +33,19 @@ export function runTest(rootDirectory: string, tests: Tests, args: string[], tes args = args.filter(arg => arg.trim().startsWith('-')); } const testArgs = testPaths.concat(args, [`--junitxml=${xmlLogFile}`]); + const pythonSettings = PythonSettings.getInstance(Uri.file(rootDirectory)); if (debug) { - const def = createDeferred(); - const launchDef = createDeferred(); const testLauncherFile = path.join(__dirname, '..', '..', '..', '..', 'pythonFiles', 'PythonTools', 'testlauncher.py'); - - // start the debug adapter only once we have started the debug process - // pytestlauncherargs const pytestlauncherargs = [rootDirectory, 'my_secret', pythonSettings.unitTest.debugPort.toString(), 'pytest']; - let outputChannelShown = false; - execPythonFile(pythonSettings.pythonPath, [testLauncherFile].concat(pytestlauncherargs).concat(testArgs), rootDirectory, true, (data: string) => { - if (data === 'READY' + os.EOL) { - // debug socket server has started - launchDef.resolve(); - } - else { - if (!outputChannelShown){ - outputChannelShown = true; - outChannel.show(); - } - outChannel.append(data); - } - }, token).catch(reason => { - if (!def.rejected && !def.resolved) { - def.reject(reason); - } - }).then(() => { - if (!def.rejected && !def.resolved) { - def.resolve(); - } - }).catch(reason => { - if (!def.rejected && !def.resolved) { - def.reject(reason); - } - }); - - launchDef.promise.then(() => { - return vscode.commands.executeCommand('vscode.startDebug', { - "name": "Debug Unit Test", - "type": "python", - "request": "attach", - "localRoot": rootDirectory, - "remoteRoot": rootDirectory, - "port": pythonSettings.unitTest.debugPort, - "secret": "my_secret", - "host": "localhost" - }); - }).catch(reason => { - if (!def.rejected && !def.resolved) { - def.reject(reason); - } - }); - - return def.promise; - } - else { - return run(pythonSettings.unitTest.pyTestPath, testArgs, rootDirectory, token, outChannel); + const debuggerArgs = [testLauncherFile].concat(pytestlauncherargs).concat(testArgs); + // tslint:disable-next-line:prefer-type-cast no-any + return debugLauncher.launchDebugger(rootDirectory, debuggerArgs, token, outChannel) as Promise; + } else { + // tslint:disable-next-line:prefer-type-cast no-any + return run(pythonSettings.unitTest.pyTestPath, testArgs, rootDirectory, token, outChannel) as Promise; } }).then(() => { - return updateResultsFromLogFiles(tests, xmlLogFile); + return updateResultsFromLogFiles(tests, xmlLogFile, testResultsService); }).then(result => { xmlLogFileCleanup(); return result; @@ -110,9 +55,9 @@ export function runTest(rootDirectory: string, tests: Tests, args: string[], tes }); } -export function updateResultsFromLogFiles(tests: Tests, outputXmlFile: string): Promise { +export function updateResultsFromLogFiles(tests: Tests, outputXmlFile: string, testResultsService: ITestResultsService): Promise { return updateResultsFromXmlLogFile(tests, outputXmlFile, PassCalculationFormulae.pytest).then(() => { - updateResults(tests); + testResultsService.updateResults(tests); return tests; }); -} \ No newline at end of file +} diff --git a/src/client/unittests/pytest/testConfigurationManager.ts b/src/client/unittests/pytest/testConfigurationManager.ts index fac9009ac1fb..8017835d7b95 100644 --- a/src/client/unittests/pytest/testConfigurationManager.ts +++ b/src/client/unittests/pytest/testConfigurationManager.ts @@ -1,61 +1,51 @@ -import * as vscode from 'vscode'; -import { TestConfigurationManager } from '../common/testConfigurationManager'; import * as fs from 'fs'; import * as path from 'path'; +import * as vscode from 'vscode'; +import { Uri } from 'vscode'; import { Installer, Product } from '../../common/installer'; +import { TestConfigurationManager } from '../common/testConfigurationManager'; +import { ITestConfigSettingsService } from '../common/types'; export class ConfigurationManager extends TestConfigurationManager { - public enable(): Thenable { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('unitTest.pyTestEnabled', true); + constructor(workspace: Uri, outputChannel: vscode.OutputChannel, + installer: Installer, testConfigSettingsService: ITestConfigSettingsService) { + super(workspace, Product.pytest, outputChannel, installer, testConfigSettingsService); } - public disable(): Thenable { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('unitTest.pyTestEnabled', false); - } - - private static configFilesExist(rootDir: string): Promise { + private static async configFilesExist(rootDir: string): Promise { const promises = ['pytest.ini', 'tox.ini', 'setup.cfg'].map(cfg => { return new Promise(resolve => { fs.exists(path.join(rootDir, cfg), exists => { resolve(exists ? cfg : ''); }); }); }); - return Promise.all(promises).then(values => { - return values.filter(exists => exists.length > 0); - }); + const values = await Promise.all(promises); + return values.filter(exists => exists.length > 0); } - public configure(rootDir: string): Promise { + // tslint:disable-next-line:no-any + public async configure(wkspace: Uri) { const args = []; const configFileOptionLabel = 'Use existing config file'; const options: vscode.QuickPickItem[] = []; - let installer = new Installer(this.outputChannel); - return ConfigurationManager.configFilesExist(rootDir).then(configFiles => { - if (configFiles.length > 0 && configFiles.length !== 1 && configFiles[0] !== 'setup.cfg') { - return Promise.resolve(); - } + const configFiles = await ConfigurationManager.configFilesExist(wkspace.fsPath); + // If a config file exits, there's nothing to be configured. + if (configFiles.length > 0 && configFiles.length !== 1 && configFiles[0] !== 'setup.cfg') { + return; + } - if (configFiles.length === 1 && configFiles[0] === 'setup.cfg') { - options.push({ - label: configFileOptionLabel, - description: 'setup.cfg' - }); - } - return this.getTestDirs(rootDir).then(subDirs => { - return this.selectTestDir(rootDir, subDirs, options); - }).then(testDir => { - if (typeof testDir === 'string' && testDir !== configFileOptionLabel) { - args.push(testDir); - } + if (configFiles.length === 1 && configFiles[0] === 'setup.cfg') { + options.push({ + label: configFileOptionLabel, + description: 'setup.cfg' }); - }).then(() => { - return installer.isProductInstalled(Product.pytest); - }).then(installed => { - if (!installed) { - return installer.installProduct(Product.pytest); - } - }).then(() => { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('unitTest.pyTestArgs', args); - }); + } + const subDirs = await this.getTestDirs(wkspace.fsPath); + const testDir = await this.selectTestDir(wkspace.fsPath, subDirs, options); + if (typeof testDir === 'string' && testDir !== configFileOptionLabel) { + args.push(testDir); + } + const installed = await this.installer.isInstalled(Product.pytest); + if (!installed) { + await this.installer.install(Product.pytest); + } + await this.testConfigSettingsService.updateTestArgs(wkspace.fsPath, Product.pytest, args); } -} \ No newline at end of file +} diff --git a/src/client/unittests/unittest/collector.ts b/src/client/unittests/unittest/collector.ts index 91affb058814..50c6edc19dec 100644 --- a/src/client/unittests/unittest/collector.ts +++ b/src/client/unittests/unittest/collector.ts @@ -1,15 +1,12 @@ 'use strict'; -import { execPythonFile } from './../../common/utils'; -import { TestFile, Tests, TestStatus } from '../common/contracts'; -import { flattenTestFiles } from '../common/testUtils'; -import * as vscode from 'vscode'; import * as path from 'path'; -import { PythonSettings } from '../../common/configSettings'; +import * as vscode from 'vscode'; import { OutputChannel } from 'vscode'; +import { PythonSettings } from '../../common/configSettings'; +import { ITestsHelper, TestFile, TestFunction, Tests, TestStatus, TestSuite } from '../common/types'; +import { execPythonFile } from './../../common/utils'; -const pythonSettings = PythonSettings.getInstance(); - -export function discoverTests(rootDirectory: string, args: string[], token: vscode.CancellationToken, ignoreCache: boolean, outChannel: OutputChannel): Promise { +export function discoverTests(rootDirectory: string, args: string[], token: vscode.CancellationToken, ignoreCache: boolean, outChannel: OutputChannel, testsHelper: ITestsHelper): Promise { let startDirectory = '.'; let pattern = 'test*.py'; const indexOfStartDir = args.findIndex(arg => arg.indexOf('-s') === 0); @@ -18,8 +15,7 @@ export function discoverTests(rootDirectory: string, args: string[], token: vsco if (startDir.trim() === '-s' && args.length >= indexOfStartDir) { // Assume the next items is the directory startDirectory = args[indexOfStartDir + 1]; - } - else { + } else { startDirectory = startDir.substring(2).trim(); if (startDirectory.startsWith('=') || startDirectory.startsWith(' ')) { startDirectory = startDirectory.substring(1); @@ -32,8 +28,7 @@ export function discoverTests(rootDirectory: string, args: string[], token: vsco if (patternValue.trim() === '-p' && args.length >= indexOfPattern) { // Assume the next items is the directory pattern = args[indexOfPattern + 1]; - } - else { + } else { pattern = patternValue.substring(2).trim(); if (pattern.startsWith('=')) { pattern = pattern.substring(1); @@ -53,7 +48,7 @@ for suite in suites._tests: pass`; let startedCollecting = false; - let testItems: string[] = []; + const testItems: string[] = []; function processOutput(output: string) { output.split(/\r?\n/g).forEach((line, index, lines) => { if (token && token.isCancellationRequested) { @@ -73,7 +68,7 @@ for suite in suites._tests: }); } args = []; - return execPythonFile(pythonSettings.pythonPath, args.concat(['-c', pythonScript]), rootDirectory, true, null, token) + return execPythonFile(rootDirectory, PythonSettings.getInstance(vscode.Uri.file(rootDirectory)).pythonPath, args.concat(['-c', pythonScript]), rootDirectory, true, null, token) .then(data => { outChannel.appendLine(data); processOutput(data); @@ -85,17 +80,17 @@ for suite in suites._tests: if (startDirectory.length > 1) { testsDirectory = path.isAbsolute(startDirectory) ? startDirectory : path.resolve(rootDirectory, startDirectory); } - return parseTestIds(testsDirectory, testItems); + return parseTestIds(testsDirectory, testItems, testsHelper); }); } -function parseTestIds(rootDirectory: string, testIds: string[]): Tests { +function parseTestIds(rootDirectory: string, testIds: string[], testsHelper: ITestsHelper): Tests { const testFiles: TestFile[] = []; testIds.forEach(testId => { addTestId(rootDirectory, testId, testFiles); }); - return flattenTestFiles(testFiles); + return testsHelper.flattenTestFiles(testFiles); } function addTestId(rootDirectory: string, testId: string, testFiles: TestFile[]) { @@ -106,7 +101,7 @@ function addTestId(rootDirectory: string, testId: string, testFiles: TestFile[]) } const paths = testIdParts.slice(0, testIdParts.length - 2); - const filePath = path.join(rootDirectory, ...paths) + '.py'; + const filePath = `${path.join(rootDirectory, ...paths)}.py`; const functionName = testIdParts.pop(); const className = testIdParts.pop(); @@ -116,8 +111,10 @@ function addTestId(rootDirectory: string, testId: string, testFiles: TestFile[]) testFile = { name: path.basename(filePath), fullPath: filePath, - functions: [], - suites: [], + // tslint:disable-next-line:prefer-type-cast + functions: [] as TestFunction[], + // tslint:disable-next-line:prefer-type-cast + suites: [] as TestSuite[], nameToRun: `${className}.${functionName}`, xmlName: '', status: TestStatus.Idle, @@ -132,8 +129,10 @@ function addTestId(rootDirectory: string, testId: string, testFiles: TestFile[]) if (!testSuite) { testSuite = { name: className, - functions: [], - suites: [], + // tslint:disable-next-line:prefer-type-cast + functions: [] as TestFunction[], + // tslint:disable-next-line:prefer-type-cast + suites: [] as TestSuite[], isUnitTest: true, isInstance: false, nameToRun: classNameToRun, @@ -144,7 +143,7 @@ function addTestId(rootDirectory: string, testId: string, testFiles: TestFile[]) testFile.suites.push(testSuite); } - const testFunction = { + const testFunction: TestFunction = { name: functionName, nameToRun: testId, status: TestStatus.Idle, @@ -152,4 +151,4 @@ function addTestId(rootDirectory: string, testId: string, testFiles: TestFile[]) }; testSuite.functions.push(testFunction); -} \ No newline at end of file +} diff --git a/src/client/unittests/unittest/main.ts b/src/client/unittests/unittest/main.ts index 51cee1b32ac1..09e96e307fa5 100644 --- a/src/client/unittests/unittest/main.ts +++ b/src/client/unittests/unittest/main.ts @@ -1,31 +1,32 @@ 'use strict'; -import { PythonSettings } from '../../common/configSettings'; -import { TestsToRun, Tests, TestStatus } from '../common/contracts'; -import { runTest } from './runner'; import * as vscode from 'vscode'; -import { discoverTests } from './collector'; -import { BaseTestManager } from '../common/baseTestManager'; import { Product } from '../../common/installer'; - -const settings = PythonSettings.getInstance(); +import { BaseTestManager } from '../common/baseTestManager'; +import { ITestCollectionStorageService, ITestDebugLauncher, ITestResultsService, ITestsHelper, Tests, TestStatus, TestsToRun } from '../common/types'; +import { discoverTests } from './collector'; +import { runTest } from './runner'; export class TestManager extends BaseTestManager { - constructor(rootDirectory: string, outputChannel: vscode.OutputChannel) { - super('unitest', Product.unittest, rootDirectory, outputChannel); + constructor(rootDirectory: string, outputChannel: vscode.OutputChannel, + testCollectionStorage: ITestCollectionStorageService, + testResultsService: ITestResultsService, testsHelper: ITestsHelper, private debugLauncher: ITestDebugLauncher) { + super('unittest', Product.unittest, rootDirectory, outputChannel, testCollectionStorage, testResultsService, testsHelper); } - configure() { + // tslint:disable-next-line:no-empty + public configure() { } - discoverTestsImpl(ignoreCache: boolean): Promise { - let args = settings.unitTest.unittestArgs.slice(0); - return discoverTests(this.rootDirectory, args, this.cancellationToken, ignoreCache, this.outputChannel); + public async discoverTestsImpl(ignoreCache: boolean): Promise { + const args = this.settings.unitTest.unittestArgs.slice(0); + // tslint:disable-next-line:no-non-null-assertion + return discoverTests(this.rootDirectory, args, this.testDiscoveryCancellationToken!, ignoreCache, this.outputChannel, this.testsHelper); } - runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { - let args = settings.unitTest.unittestArgs.slice(0); + public async runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise<{}> { + const args = this.settings.unitTest.unittestArgs.slice(0); if (runFailedTests === true) { testsToRun = { testFile: [], testFolder: [], testSuite: [], testFunction: [] }; testsToRun.testFunction = tests.testFunctions.filter(fn => { return fn.testFunction.status === TestStatus.Error || fn.testFunction.status === TestStatus.Fail; }).map(fn => fn.testFunction); } - return runTest(this, this.rootDirectory, tests, args, testsToRun, this.cancellationToken, this.outputChannel, debug); + return runTest(this, this.testResultsService, this.debugLauncher, this.rootDirectory, tests, args, testsToRun, this.testRunnerCancellationToken, this.outputChannel, debug); } } diff --git a/src/client/unittests/unittest/runner.ts b/src/client/unittests/unittest/runner.ts index 34670c659872..db72c77b14ef 100644 --- a/src/client/unittests/unittest/runner.ts +++ b/src/client/unittests/unittest/runner.ts @@ -1,29 +1,24 @@ -/// - 'use strict'; import * as path from 'path'; -import { TestsToRun, Tests, TestStatus } from '../common/contracts'; -import { updateResults } from '../common/testUtils'; +import { CancellationToken, OutputChannel, Uri } from 'vscode'; +import { PythonSettings } from '../../common/configSettings'; import { BaseTestManager } from '../common/baseTestManager'; -import { CancellationToken, OutputChannel } from 'vscode'; import { run } from '../common/runner'; +import { ITestDebugLauncher, ITestResultsService, Tests, TestStatus, TestsToRun } from '../common/types'; import { Server } from './socketServer'; -import { PythonSettings } from '../../common/configSettings'; -import * as vscode from 'vscode'; -import { execPythonFile } from './../../common/utils'; -import { createDeferred } from './../../common/helpers'; -import * as os from 'os'; - -const settings = PythonSettings.getInstance(); -interface TestStatusMap { +type TestStatusMap = { status: TestStatus; summaryProperty: string; -} +}; const outcomeMapping = new Map(); +// tslint:disable-next-line:no-backbone-get-set-outside-model outcomeMapping.set('passed', { status: TestStatus.Pass, summaryProperty: 'passed' }); +// tslint:disable-next-line:no-backbone-get-set-outside-model outcomeMapping.set('failed', { status: TestStatus.Fail, summaryProperty: 'failures' }); +// tslint:disable-next-line:no-backbone-get-set-outside-model outcomeMapping.set('error', { status: TestStatus.Error, summaryProperty: 'errors' }); +// tslint:disable-next-line:no-backbone-get-set-outside-model outcomeMapping.set('skipped', { status: TestStatus.Skipped, summaryProperty: 'skipped' }); interface ITestData { @@ -33,7 +28,8 @@ interface ITestData { traceback: string; } -export function runTest(testManager: BaseTestManager, rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { +// tslint:disable-next-line:max-func-body-length +export function runTest(testManager: BaseTestManager, testResultsService: ITestResultsService, debugLauncher: ITestDebugLauncher, rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { tests.summary.errors = 0; tests.summary.failures = 0; tests.summary.passed = 0; @@ -42,12 +38,16 @@ export function runTest(testManager: BaseTestManager, rootDirectory: string, tes const testLauncherFile = path.join(__dirname, '..', '..', '..', '..', 'pythonFiles', 'PythonTools', 'visualstudio_py_testlauncher.py'); const server = new Server(); server.on('error', (message: string, ...data: string[]) => { + // tslint:disable-next-line:no-console console.log(`${message} ${data.join(' ')}`); }); + // tslint:disable-next-line:no-empty server.on('log', (message: string, ...data: string[]) => { }); + // tslint:disable-next-line:no-empty server.on('connect', (data) => { }); + // tslint:disable-next-line:no-empty server.on('start', (data: { test: string }) => { }); server.on('result', (data: ITestData) => { @@ -62,31 +62,32 @@ export function runTest(testManager: BaseTestManager, rootDirectory: string, tes if (failFast && (statusDetails.summaryProperty === 'failures' || statusDetails.summaryProperty === 'errors')) { testManager.stop(); } - } - else { + } else { if (statusDetails) { tests.summary[statusDetails.summaryProperty] += 1; } } }); + // tslint:disable-next-line:no-empty server.on('socket.disconnected', (data) => { }); return server.start().then(port => { - let testPaths: string[] = getIdsOfTestsToRun(tests, testsToRun); - for (let counter = 0; counter < testPaths.length; counter++) { - testPaths[counter] = '-t' + testPaths[counter].trim(); + const testPaths: string[] = getIdsOfTestsToRun(tests, testsToRun); + for (let counter = 0; counter < testPaths.length; counter += 1) { + testPaths[counter] = `-t${testPaths[counter].trim()}`; } const startTestDiscoveryDirectory = getStartDirectory(args); - function runTest(testFile: string = '', testId: string = '') { + function runTestInternal(testFile: string = '', testId: string = '') { let testArgs = buildTestArgs(args); failFast = testArgs.indexOf('--uf') >= 0; testArgs = testArgs.filter(arg => arg !== '--uf'); testArgs.push(`--result-port=${port}`); if (debug === true) { - testArgs.push(...[`--secret=my_secret`, `--port=3000`]); + const debugPort = PythonSettings.getInstance(Uri.file(rootDirectory)).unitTest.debugPort; + testArgs.push(...['--secret=my_secret', `--port=${debugPort}`]); } testArgs.push(`--us=${startTestDiscoveryDirectory}`); if (testId.length > 0) { @@ -96,88 +97,45 @@ export function runTest(testManager: BaseTestManager, rootDirectory: string, tes testArgs.push(`--testFile=${testFile}`); } if (debug === true) { - const def = createDeferred(); - const launchDef = createDeferred(); - let outputChannelShown = false; - - // start the debug adapter only once we have started the debug process - execPythonFile(settings.pythonPath, [testLauncherFile].concat(testArgs), rootDirectory, true, (data: string) => { - if (data === 'READY' + os.EOL) { - // debug socket server has started - launchDef.resolve(); - } - else { - if (!outputChannelShown) { - outputChannelShown = true; - outChannel.show(); - } - outChannel.append(data); - } - }, token).catch(reason => { - if (!def.rejected && !def.resolved) { - def.reject(reason); - } - }).then(() => { - if (!def.rejected && !def.resolved) { - def.resolve(); - } - }).catch(reason => { - if (!def.rejected && !def.resolved) { - def.reject(reason); - } - }); - - launchDef.promise.then(() => { - return vscode.commands.executeCommand('vscode.startDebug', { - "name": "Debug Unit Test", - "type": "python", - "request": "attach", - "localRoot": rootDirectory, - "remoteRoot": rootDirectory, - "port": settings.unitTest.debugPort, - "secret": "my_secret", - "host": "localhost" - }); - }).catch(reason => { - if (!def.rejected && !def.resolved) { - def.reject(reason); - } - }); - - return def.promise; - } - else { - return run(settings.pythonPath, [testLauncherFile].concat(testArgs), rootDirectory, token, outChannel); + // tslint:disable-next-line:prefer-type-cast no-any + return debugLauncher.launchDebugger(rootDirectory, [testLauncherFile].concat(testArgs), token, outChannel); + } else { + // tslint:disable-next-line:prefer-type-cast no-any + return run(PythonSettings.getInstance(Uri.file(rootDirectory)).pythonPath, [testLauncherFile].concat(testArgs), rootDirectory, token, outChannel); } } // Test everything if (testPaths.length === 0) { - return runTest(); + return runTestInternal(); } // Ok, the ptvs test runner can only work with one test at a time let promise = Promise.resolve(''); if (Array.isArray(testsToRun.testFile)) { testsToRun.testFile.forEach(testFile => { - promise = promise.then(() => runTest(testFile.fullPath, testFile.nameToRun)); + // tslint:disable-next-line:prefer-type-cast no-any + promise = promise.then(() => runTestInternal(testFile.fullPath, testFile.nameToRun) as Promise); }); } if (Array.isArray(testsToRun.testSuite)) { testsToRun.testSuite.forEach(testSuite => { - const testFileName = tests.testSuits.find(t => t.testSuite === testSuite).parentTestFile.fullPath; - promise = promise.then(() => runTest(testFileName, testSuite.nameToRun)); + const testFileName = tests.testSuites.find(t => t.testSuite === testSuite).parentTestFile.fullPath; + // tslint:disable-next-line:prefer-type-cast no-any + promise = promise.then(() => runTestInternal(testFileName, testSuite.nameToRun) as Promise); }); } if (Array.isArray(testsToRun.testFunction)) { testsToRun.testFunction.forEach(testFn => { const testFileName = tests.testFunctions.find(t => t.testFunction === testFn).parentTestFile.fullPath; - promise = promise.then(() => runTest(testFileName, testFn.nameToRun)); + // tslint:disable-next-line:prefer-type-cast no-any + promise = promise.then(() => runTestInternal(testFileName, testFn.nameToRun) as Promise); }); } - return promise; + // tslint:disable-next-line:prefer-type-cast no-any + return promise as Promise; }).then(() => { - updateResults(tests); + testResultsService.updateResults(tests); return tests; }).catch(reason => { return Promise.reject(reason); @@ -192,8 +150,7 @@ function getStartDirectory(args: string[]): string { if ((startDir.trim() === '-s' || startDir.trim() === '--start-directory') && args.length >= indexOfStartDir) { // Assume the next items is the directory startDirectory = args[indexOfStartDir + 1]; - } - else { + } else { const lenToStartFrom = startDir.startsWith('-s') ? '-s'.length : '--start-directory'.length; startDirectory = startDir.substring(lenToStartFrom).trim(); if (startDirectory.startsWith('=')) { @@ -212,8 +169,7 @@ function buildTestArgs(args: string[]): string[] { if ((patternValue.trim() === '-p' || patternValue.trim() === '--pattern') && args.length >= indexOfPattern) { // Assume the next items is the directory pattern = args[indexOfPattern + 1]; - } - else { + } else { const lenToStartFrom = patternValue.startsWith('-p') ? '-p'.length : '--pattern'.length; pattern = patternValue.substring(lenToStartFrom).trim(); if (pattern.startsWith('=')) { @@ -251,4 +207,4 @@ function getIdsOfTestsToRun(tests: Tests, testsToRun: TestsToRun): string[] { testIds.push(...testsToRun.testFunction.map(f => f.nameToRun)); } return testIds; -} \ No newline at end of file +} diff --git a/src/client/unittests/unittest/socketServer.ts b/src/client/unittests/unittest/socketServer.ts index 6c4d3bc6166b..334ae6b85c4e 100644 --- a/src/client/unittests/unittest/socketServer.ts +++ b/src/client/unittests/unittest/socketServer.ts @@ -1,16 +1,19 @@ 'use strict'; -import * as net from 'net'; +import { EventEmitter } from 'events'; import * as fs from 'fs'; +import * as net from 'net'; import * as os from 'os'; -import { Disposable } from 'vscode' +import { Disposable } from 'vscode'; import { createDeferred, Deferred } from '../../common/helpers'; -import { EventEmitter } from 'events'; +// tslint:disable-next-line:variable-name const MaxConnections = 100; function getIPType() { const networkInterfaces = os.networkInterfaces(); + // tslint:disable-next-line:variable-name let IPType = ''; + // tslint:disable-next-line:prefer-type-cast no-any if (networkInterfaces && Array.isArray(networkInterfaces) && (networkInterfaces as any).length > 0) { // getting the family of first network interface available IPType = networkInterfaces[Object.keys(networkInterfaces)[0]][0].family; @@ -41,7 +44,7 @@ export class Server extends EventEmitter implements Disposable { } } public start(): Promise { - this.startedDef = createDeferred() + this.startedDef = createDeferred(); fs.unlink(this.path, () => { this.server = net.createServer(this.connectionListener.bind(this)); this.server.maxConnections = MaxConnections; @@ -72,16 +75,17 @@ export class Server extends EventEmitter implements Disposable { this.emit('error', err); }); socket.on('data', (data) => { - let sock = socket; + const sock = socket; // Assume we have just one client socket connection let dataStr = this.ipcBuffer += data; + // tslint:disable-next-line:no-constant-condition while (true) { const startIndex = dataStr.indexOf('{'); if (startIndex === -1) { return; } - const lengthOfMessage = parseInt(dataStr.slice(dataStr.indexOf(':') + 1, dataStr.indexOf('{')).trim()); + const lengthOfMessage = parseInt(dataStr.slice(dataStr.indexOf(':') + 1, dataStr.indexOf('{')).trim(), 10); if (dataStr.length < startIndex + lengthOfMessage) { return; } @@ -97,13 +101,16 @@ export class Server extends EventEmitter implements Disposable { this.emit('log', message, ...data); } private onCloseSocket() { - for (let i = 0, count = this.sockets.length; i < count; i++) { - let socket = this.sockets[i]; + // tslint:disable-next-line:one-variable-per-declaration + for (let i = 0, count = this.sockets.length; i < count; i += 1) { + const socket = this.sockets[i]; let destroyedSocketId = false; if (socket && socket.readable) { continue; } + // tslint:disable-next-line:no-any prefer-type-cast if ((socket as any).id) { + // tslint:disable-next-line:no-any prefer-type-cast destroyedSocketId = (socket as any).id; } this.log('socket disconnected', destroyedSocketId.toString()); @@ -115,4 +122,4 @@ export class Server extends EventEmitter implements Disposable { return; } } -} \ No newline at end of file +} diff --git a/src/client/unittests/unittest/testConfigurationManager.ts b/src/client/unittests/unittest/testConfigurationManager.ts index 85041676e06b..87334cd72746 100644 --- a/src/client/unittests/unittest/testConfigurationManager.ts +++ b/src/client/unittests/unittest/testConfigurationManager.ts @@ -1,42 +1,33 @@ -import * as vscode from 'vscode'; import * as path from 'path'; +import { OutputChannel, Uri } from 'vscode'; +import { Installer, Product } from '../../common/installer'; import { TestConfigurationManager } from '../common/testConfigurationManager'; +import { ITestConfigSettingsService } from '../common/types'; export class ConfigurationManager extends TestConfigurationManager { - public enable(): Thenable { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('unitTest.unittestEnabled', true); + constructor(workspace: Uri, outputChannel: OutputChannel, + installer: Installer, testConfigSettingsService: ITestConfigSettingsService) { + super(workspace, Product.unittest, outputChannel, installer, testConfigSettingsService); } - public disable(): Thenable { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('unitTest.unittestEnabled', false); - } - - public configure(rootDir: string): Promise { + // tslint:disable-next-line:no-any + public async configure(wkspace: Uri) { const args = ['-v']; - return this.getTestDirs(rootDir).then(subDirs => { - return this.selectTestDir(rootDir, subDirs); - }).then(testDir => { - args.push('-s'); - if (typeof testDir === 'string' && testDir !== '.') { - args.push(`.${path.sep}${testDir}`); - } - else { - args.push('.'); - } + const subDirs = await this.getTestDirs(wkspace.fsPath); + const testDir = await this.selectTestDir(wkspace.fsPath, subDirs); + args.push('-s'); + if (typeof testDir === 'string' && testDir !== '.') { + args.push(`./${testDir}`); + } else { + args.push('.'); + } - return this.selectTestFilePattern(); - }).then(testfilePattern => { - args.push('-p'); - if (typeof testfilePattern === 'string') { - args.push(testfilePattern); - } - else { - args.push('test*.py'); - } - }).then(() => { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('unitTest.unittestArgs', args); - }); + const testfilePattern = await this.selectTestFilePattern(); + args.push('-p'); + if (typeof testfilePattern === 'string') { + args.push(testfilePattern); + } else { + args.push('test*.py'); + } + await this.testConfigSettingsService.updateTestArgs(wkspace.fsPath, Product.unittest, args); } -} \ No newline at end of file +} diff --git a/src/client/workspaceSymbols/contracts.ts b/src/client/workspaceSymbols/contracts.ts index c5255c3fbcc2..cb53f8b7d397 100644 --- a/src/client/workspaceSymbols/contracts.ts +++ b/src/client/workspaceSymbols/contracts.ts @@ -1,4 +1,4 @@ -import { SymbolKind, Position } from 'vscode'; +import { Position, SymbolKind } from 'vscode'; export interface Tag { fileName: string; diff --git a/src/client/workspaceSymbols/generator.ts b/src/client/workspaceSymbols/generator.ts index 79b9053d98a0..447e38ea95a3 100644 --- a/src/client/workspaceSymbols/generator.ts +++ b/src/client/workspaceSymbols/generator.ts @@ -1,39 +1,52 @@ -import * as vscode from 'vscode'; -import * as path from 'path'; import * as child_process from 'child_process'; -import { PythonSettings } from '../common/configSettings'; - -const pythonSettings = PythonSettings.getInstance(); +import * as fs from 'fs'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { IPythonSettings, PythonSettings } from '../common/configSettings'; +import { captureTelemetry } from '../telemetry'; +import { WORKSPACE_SYMBOLS_BUILD } from '../telemetry/constants'; export class Generator implements vscode.Disposable { private optionsFile: string; private disposables: vscode.Disposable[]; - - constructor(private output: vscode.OutputChannel) { + private pythonSettings: IPythonSettings; + public get tagFilePath(): string { + return this.pythonSettings.workspaceSymbols.tagFilePath; + } + public get enabled(): boolean { + return this.pythonSettings.workspaceSymbols.enabled; + } + constructor(public readonly workspaceFolder: vscode.Uri, private output: vscode.OutputChannel) { this.disposables = []; this.optionsFile = path.join(__dirname, '..', '..', '..', 'resources', 'ctagOptions'); + this.pythonSettings = PythonSettings.getInstance(workspaceFolder); } - dispose() { + public dispose() { this.disposables.forEach(d => d.dispose()); } - private buildCmdArgsg(): string[] { + private buildCmdArgs(): string[] { const optionsFile = this.optionsFile.indexOf(' ') > 0 ? `"${this.optionsFile}"` : this.optionsFile; - const exclusions = pythonSettings.workspaceSymbols.exclusionPatterns; + const exclusions = this.pythonSettings.workspaceSymbols.exclusionPatterns; const excludes = exclusions.length === 0 ? [] : exclusions.map(pattern => `--exclude=${pattern}`); return [`--options=${optionsFile}`, '--languages=Python'].concat(excludes); } - generateWorkspaceTags(): Promise { - const tagFile = pythonSettings.workspaceSymbols.tagFilePath; - return this.generateTags(tagFile, { directory: vscode.workspace.rootPath }); + public async generateWorkspaceTags(): Promise { + if (!this.pythonSettings.workspaceSymbols.enabled) { + return; + } + return await this.generateTags({ directory: this.workspaceFolder.fsPath }); } + @captureTelemetry(WORKSPACE_SYMBOLS_BUILD) + private generateTags(source: { directory?: string, file?: string }): Promise { + const tagFile = path.normalize(this.pythonSettings.workspaceSymbols.tagFilePath); + const cmd = this.pythonSettings.workspaceSymbols.ctagsPath; + const args = this.buildCmdArgs(); - private generateTags(outputFile: string, source: { directory?: string, file?: string }): Promise { - const cmd = pythonSettings.workspaceSymbols.ctagsPath; - const args = this.buildCmdArgsg(); + let outputFile = tagFile; if (source.file && source.file.length > 0) { source.directory = path.dirname(source.file); } @@ -41,12 +54,15 @@ export class Generator implements vscode.Disposable { if (path.dirname(outputFile) === source.directory) { outputFile = path.basename(outputFile); } + const outputDir = path.dirname(outputFile); + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir); + } outputFile = outputFile.indexOf(' ') > 0 ? `"${outputFile}"` : outputFile; - args.push(`-o ${outputFile}`, '.'); this.output.appendLine('-'.repeat(10) + 'Generating Tags' + '-'.repeat(10)); this.output.appendLine(`${cmd} ${args.join(' ')}`); - const promise = new Promise((resolve, reject) => { + const promise = new Promise((resolve, reject) => { let options: child_process.SpawnOptions = { cwd: source.directory }; @@ -71,7 +87,7 @@ export class Generator implements vscode.Disposable { reject(errorMsg); } else { - resolve(outputFile); + resolve(); } }); }); @@ -80,4 +96,4 @@ export class Generator implements vscode.Disposable { return promise; } -} \ No newline at end of file +} diff --git a/src/client/workspaceSymbols/main.ts b/src/client/workspaceSymbols/main.ts index 2fb454d45f43..1be92e608df4 100644 --- a/src/client/workspaceSymbols/main.ts +++ b/src/client/workspaceSymbols/main.ts @@ -1,33 +1,45 @@ import * as vscode from 'vscode'; import { Generator } from './generator'; -import { Product, Installer } from '../common/installer'; -import { PythonSettings } from '../common/configSettings'; +import { Installer, InstallerResponse, Product } from '../common/installer'; import { fsExistsAsync } from '../common/utils'; import { isNotInstalledError } from '../common/helpers'; import { PythonLanguage, Commands } from '../common/constants'; import { WorkspaceSymbolProvider } from './provider'; +import { workspace } from 'vscode'; -const pythonSettings = PythonSettings.getInstance(); +const MAX_NUMBER_OF_ATTEMPTS_TO_INSTALL_AND_BUILD = 2; export class WorkspaceSymbols implements vscode.Disposable { private disposables: vscode.Disposable[]; - private generator: Generator; + private generators: Generator[] = []; private installer: Installer; constructor(private outputChannel: vscode.OutputChannel) { this.disposables = []; this.disposables.push(this.outputChannel); - this.generator = new Generator(this.outputChannel); - this.disposables.push(this.generator); this.installer = new Installer(); this.disposables.push(this.installer); this.registerCommands(); - - // The extension has just loaded, so lets rebuild the tags - vscode.languages.registerWorkspaceSymbolProvider(new WorkspaceSymbolProvider(this.generator, this.outputChannel)); + this.initializeGenerators(); + vscode.languages.registerWorkspaceSymbolProvider(new WorkspaceSymbolProvider(this.generators, this.outputChannel)); this.buildWorkspaceSymbols(true); + this.disposables.push(vscode.workspace.onDidChangeWorkspaceFolders(() => this.initializeGenerators())); + } + private initializeGenerators() { + while (this.generators.length > 0) { + const generator = this.generators.shift(); + generator.dispose(); + } + + if (Array.isArray(vscode.workspace.workspaceFolders)) { + vscode.workspace.workspaceFolders.forEach(wkSpc => { + this.generators.push(new Generator(wkSpc.uri, this.outputChannel)); + }); + } } registerCommands() { - this.disposables.push(vscode.commands.registerCommand(Commands.Build_Workspace_Symbols, this.buildWorkspaceSymbols.bind(this))); + this.disposables.push(vscode.commands.registerCommand(Commands.Build_Workspace_Symbols, (rebuild: boolean = true, token?: vscode.CancellationToken) => { + this.buildWorkspaceSymbols(rebuild, token); + })); } registerOnSaveHandlers() { this.disposables.push(vscode.workspace.onDidSaveTextDocument(this.onDidSaveTextDocument.bind(this))); @@ -50,50 +62,53 @@ export class WorkspaceSymbols implements vscode.Disposable { dispose() { this.disposables.forEach(d => d.dispose()); } - disableDocumentLanguageProvider(): Thenable { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('python.workspaceSymbols.enabled', false); - - } - buildWorkspaceSymbols(rebuild: boolean = true, token?: vscode.CancellationToken): Promise { - if (!pythonSettings.workspaceSymbols.enabled || (token && token.isCancellationRequested)) { + async buildWorkspaceSymbols(rebuild: boolean = true, token?: vscode.CancellationToken): Promise { + if (token && token.isCancellationRequested) { + return Promise.resolve([]); + } + if (this.generators.length === 0) { return Promise.resolve([]); } - return fsExistsAsync(pythonSettings.workspaceSymbols.tagFilePath).then(exits => { - let promise = Promise.resolve(); + let promptPromise: Promise; + let promptResponse: InstallerResponse; + return this.generators.map(async generator => { + if (!generator.enabled) { + return; + } + const exists = await fsExistsAsync(generator.tagFilePath); // if file doesn't exist, then run the ctag generator // Or check if required to rebuild - if (rebuild || !exits) { - promise = this.generator.generateWorkspaceTags(); + if (!rebuild && exists) { + return; } - - return promise.catch(reason => { - if (!isNotInstalledError(reason)) { - this.outputChannel.show(); - return Promise.reject(reason); + for (let counter = 0; counter < MAX_NUMBER_OF_ATTEMPTS_TO_INSTALL_AND_BUILD; counter++) { + try { + await generator.generateWorkspaceTags(); + return; + } + catch (error) { + if (!isNotInstalledError(error)) { + this.outputChannel.show(); + return; + } } if (!token || token.isCancellationRequested) { return; } - return new Promise((resolve, reject) => { - vscode.window.showErrorMessage('CTags needs to be installed to get support for Python workspace symbols', - 'Install', `Don't ask again`).then(item => { - switch (item) { - case 'Install': { - this.installer.installProduct(Product.ctags).then(() => { - return this.buildWorkspaceSymbols(rebuild, token); - }).catch(reason => reject(reason)); - break; - } - case `Don't ask again`: { - this.disableDocumentLanguageProvider().then(() => resolve(), reason => reject(reason)); - break; - } - } - }); - }); - }); + // Display prompt once for all workspaces + if (promptPromise) { + promptResponse = await promptPromise; + continue; + } + else { + promptPromise = this.installer.promptToInstall(Product.ctags, workspace.workspaceFolders[0].uri); + promptResponse = await promptPromise; + } + if (promptResponse !== InstallerResponse.Installed || (!token || token.isCancellationRequested)) { + return; + } + } }); } } diff --git a/src/client/workspaceSymbols/parser.ts b/src/client/workspaceSymbols/parser.ts index 4bc9b8c520c9..7be6e51183d4 100644 --- a/src/client/workspaceSymbols/parser.ts +++ b/src/client/workspaceSymbols/parser.ts @@ -1,14 +1,12 @@ import * as vscode from 'vscode'; import { Tag } from './contracts'; import * as path from 'path'; -import { PythonSettings } from '../common/configSettings'; import { fsExistsAsync } from '../common/utils'; const LineByLineReader = require("line-by-line"); const NamedRegexp = require('named-js-regexp'); const fuzzy = require('fuzzy'); -const pythonSettings = PythonSettings.getInstance(); const IsFileRegEx = /\tkind:file\tline:\d+$/g; const LINE_REGEX = '(?\\w+)\\t(?.*)\\t\\/\\^(?.*)\\$\\/;"\\tkind:(?\\w+)\\tline:(?\\d+)$'; @@ -102,15 +100,14 @@ Object.keys(newValuesAndKeys).forEach(key => { CTagKinMapping.set(key, newValuesAndKeys[key]); }); -export function parseTags(query: string, token: vscode.CancellationToken, maxItems: number = 200): Promise { - const file = pythonSettings.workspaceSymbols.tagFilePath; - return fsExistsAsync(file).then(exists => { +export function parseTags(workspaceFolder: string, tagFile: string, query: string, token: vscode.CancellationToken, maxItems: number = 200): Promise { + return fsExistsAsync(tagFile).then(exists => { if (!exists) { return null; } return new Promise((resolve, reject) => { - let lr = new LineByLineReader(file); + let lr = new LineByLineReader(tagFile); let lineNumber = 0; let tags: Tag[] = []; @@ -124,7 +121,7 @@ export function parseTags(query: string, token: vscode.CancellationToken, maxIte lr.close(); return; } - const tag = parseTagsLine(line, query); + const tag = parseTagsLine(workspaceFolder, line, query); if (tag) { tags.push(tag); } @@ -139,7 +136,7 @@ export function parseTags(query: string, token: vscode.CancellationToken, maxIte }); }); } -function parseTagsLine(line: string, searchPattern: string): Tag { +function parseTagsLine(workspaceFolder: string, line: string, searchPattern: string): Tag { if (IsFileRegEx.test(line)) { return; } @@ -152,7 +149,7 @@ function parseTagsLine(line: string, searchPattern: string): Tag { } let file = match.file; if (!path.isAbsolute(file)) { - file = path.resolve(vscode.workspace.rootPath, file); + file = path.resolve(workspaceFolder, '.vscode', file); } const symbolKind = CTagKinMapping.has(match.type) ? CTagKinMapping.get(match.type) : vscode.SymbolKind.Null; diff --git a/src/client/workspaceSymbols/provider.ts b/src/client/workspaceSymbols/provider.ts index 76d2e3e23e3c..d6f2eb7604d5 100644 --- a/src/client/workspaceSymbols/provider.ts +++ b/src/client/workspaceSymbols/provider.ts @@ -1,39 +1,46 @@ +import * as _ from 'lodash'; import * as vscode from 'vscode'; +import { Commands } from '../common/constants'; +import { fsExistsAsync } from '../common/utils'; +import { captureTelemetry } from '../telemetry'; +import { WORKSPACE_SYMBOLS_GO_TO } from '../telemetry/constants'; import { Generator } from './generator'; -import { PythonSettings } from '../common/configSettings'; import { parseTags } from './parser'; -import { fsExistsAsync } from '../common/utils'; -import { createDeferred } from '../common/helpers'; -import { Commands } from '../common/constants'; -const pythonSettings = PythonSettings.getInstance(); export class WorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider { - public constructor(private tagGenerator: Generator, private outputChannel: vscode.OutputChannel) { + public constructor(private tagGenerators: Generator[], private outputChannel: vscode.OutputChannel) { } - provideWorkspaceSymbols(query: string, token: vscode.CancellationToken): Thenable { - if (!pythonSettings.workspaceSymbols.enabled) { - return Promise.resolve([]); + @captureTelemetry(WORKSPACE_SYMBOLS_GO_TO) + public async provideWorkspaceSymbols(query: string, token: vscode.CancellationToken): Promise { + if (this.tagGenerators.length === 0) { + return []; } - return fsExistsAsync(pythonSettings.workspaceSymbols.tagFilePath).then(exits => { - let def = createDeferred(); - if (exits) { - def.resolve(); - } - else { - vscode.commands.executeCommand(Commands.Build_Workspace_Symbols, false, token).then(() => def.resolve(), reason => def.reject(reason)); - } + const generatorsWithTagFiles = await Promise.all(this.tagGenerators.map(generator => fsExistsAsync(generator.tagFilePath))); + if (generatorsWithTagFiles.filter(exists => exists).length !== this.tagGenerators.length) { + await vscode.commands.executeCommand(Commands.Build_Workspace_Symbols, true, token); + } + + const generators = await Promise.all(this.tagGenerators.map(async generator => { + const tagFileExists = await fsExistsAsync(generator.tagFilePath); + return tagFileExists ? generator : undefined; + })); + + const promises = generators + .filter(generator => generator !== undefined && generator.enabled) + .map(async generator => { + // load tags + const items = await parseTags(generator.workspaceFolder.fsPath, generator.tagFilePath, query, token); + if (!Array.isArray(items)) { + return []; + } + return items.map(item => new vscode.SymbolInformation( + item.symbolName, item.symbolKind, '', + new vscode.Location(vscode.Uri.file(item.fileName), item.position) + )); + }); - return def.promise - .then(() => parseTags(query, token)) - .then(items => { - if (!Array.isArray(items)) { - return []; - } - return items.map(item => new vscode.SymbolInformation(item.symbolName, - item.symbolKind, '', - new vscode.Location(vscode.Uri.file(item.fileName), item.position))); - }); - }); + const symbols = await Promise.all(promises); + return _.flatten(symbols); } } diff --git a/src/test/.vscode/settings.json b/src/test/.vscode/settings.json new file mode 100644 index 000000000000..2218e2cecd87 --- /dev/null +++ b/src/test/.vscode/settings.json @@ -0,0 +1,25 @@ +{ + "python.linting.pylintEnabled": false, + "python.linting.flake8Enabled": false, + "python.workspaceSymbols.enabled": false, + "python.unitTest.nosetestArgs": [], + "python.unitTest.pyTestArgs": [], + "python.unitTest.unittestArgs": [ + "-v", + "-s", + ".", + "-p", + "*test*.py" + ], + "python.formatting.formatOnSave": false, + "python.sortImports.args": [], + "python.linting.lintOnSave": false, + "python.linting.lintOnTextChange": false, + "python.linting.enabled": true, + "python.linting.pep8Enabled": false, + "python.linting.prospectorEnabled": false, + "python.linting.pydocstyleEnabled": false, + "python.linting.pylamaEnabled": false, + "python.linting.mypyEnabled": false, + "python.formatting.provider": "yapf" +} \ No newline at end of file diff --git a/src/test/.vscode/tags b/src/test/.vscode/tags new file mode 100644 index 000000000000..c4371e74af04 --- /dev/null +++ b/src/test/.vscode/tags @@ -0,0 +1,721 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +A ..\\pythonFiles\\autocomp\\pep526.py /^class A:$/;" kind:class line:13 +A ..\\pythonFiles\\definition\\await.test.py /^class A:$/;" kind:class line:3 +B ..\\pythonFiles\\autocomp\\pep526.py /^class B:$/;" kind:class line:17 +B ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^class B(Exception):$/;" kind:class line:19 +B ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^class B(Exception):$/;" kind:class line:19 +B ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^class B(Exception):$/;" kind:class line:19 +BaseRefactoring ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^class BaseRefactoring(object):$/;" kind:class line:54 +BoundedQueue ..\\pythonFiles\\autocomp\\misc.py /^ class BoundedQueue(_Verbose):$/;" kind:class line:1250 +BoundedSemaphore ..\\pythonFiles\\autocomp\\misc.py /^def BoundedSemaphore(*args, **kwargs):$/;" kind:function line:497 +C ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^class C(B):$/;" kind:class line:22 +C ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^class C(B):$/;" kind:class line:22 +C ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^class C(B):$/;" kind:class line:22 +Change ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^class Change():$/;" kind:class line:41 +ChangeType ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^class ChangeType():$/;" kind:class line:32 +Child2Class ..\\pythonFiles\\symbolFiles\\childFile.py /^class Child2Class(object):$/;" kind:class line:5 +Class1 ..\\pythonFiles\\autocomp\\one.py /^class Class1(object):$/;" kind:class line:6 +Class1 ..\\pythonFiles\\definition\\one.py /^class Class1(object):$/;" kind:class line:6 +Condition ..\\pythonFiles\\autocomp\\misc.py /^def Condition(*args, **kwargs):$/;" kind:function line:242 +ConsumerThread ..\\pythonFiles\\autocomp\\misc.py /^ class ConsumerThread(Thread):$/;" kind:class line:1298 +D ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^class D(C):$/;" kind:class line:25 +D ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^class D(C):$/;" kind:class line:25 +D ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^class D(C):$/;" kind:class line:25 +DELETE ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ DELETE = 2$/;" kind:variable line:38 +DELETE ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ DELETE = 2$/;" kind:variable line:46 +Decorator ..\\pythonFiles\\autocomp\\deco.py /^class Decorator(metaclass=abc.ABCMeta):$/;" kind:class line:3 +DoSomething ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^class DoSomething():$/;" kind:class line:200 +DoSomething ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^class DoSomething():$/;" kind:class line:200 +DoSomething ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^class DoSomething():$/;" kind:class line:200 +EDIT ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ EDIT = 0$/;" kind:variable line:36 +EDIT ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ EDIT = 0$/;" kind:variable line:44 +Event ..\\pythonFiles\\autocomp\\misc.py /^def Event(*args, **kwargs):$/;" kind:function line:542 +Example3 ..\\pythonFiles\\formatting\\fileToFormat.py /^class Example3( object ):$/;" kind:class line:12 +ExtractMethodRefactor ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^class ExtractMethodRefactor(ExtractVariableRefactor):$/;" kind:class line:144 +ExtractVariableRefactor ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^class ExtractVariableRefactor(BaseRefactoring):$/;" kind:class line:120 +Foo ..\\multiRootWkspc\\disableLinters\\file.py /^class Foo(object):$/;" kind:class line:5 +Foo ..\\multiRootWkspc\\parent\\child\\file.py /^class Foo(object):$/;" kind:class line:5 +Foo ..\\multiRootWkspc\\workspace1\\file.py /^class Foo(object):$/;" kind:class line:5 +Foo ..\\multiRootWkspc\\workspace2\\file.py /^class Foo(object):$/;" kind:class line:5 +Foo ..\\multiRootWkspc\\workspace3\\file.py /^class Foo(object):$/;" kind:class line:5 +Foo ..\\pythonFiles\\autocomp\\four.py /^class Foo(object):$/;" kind:class line:7 +Foo ..\\pythonFiles\\definition\\four.py /^class Foo(object):$/;" kind:class line:7 +Foo ..\\pythonFiles\\linting\\file.py /^class Foo(object):$/;" kind:class line:5 +Foo ..\\pythonFiles\\linting\\flake8config\\file.py /^class Foo(object):$/;" kind:class line:5 +Foo ..\\pythonFiles\\linting\\pep8config\\file.py /^class Foo(object):$/;" kind:class line:5 +Foo ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^class Foo(object):$/;" kind:class line:5 +Foo ..\\pythonFiles\\linting\\pylintconfig\\file.py /^class Foo(object):$/;" kind:class line:5 +Foo ..\\pythonFiles\\symbolFiles\\file.py /^class Foo(object):$/;" kind:class line:5 +Gaussian ..\\pythonFiles\\jupyter\\cells.py /^class Gaussian(object):$/;" kind:class line:100 +Lock ..\\pythonFiles\\autocomp\\misc.py /^Lock = _allocate_lock$/;" kind:variable line:112 +N ..\\pythonFiles\\jupyter\\cells.py /^N = 50$/;" kind:variable line:42 +NEW ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ NEW = 1$/;" kind:variable line:37 +NEW ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ NEW = 1$/;" kind:variable line:45 +PEP_484_style ..\\pythonFiles\\autocomp\\pep526.py /^PEP_484_style = SOMETHING # type: str$/;" kind:variable line:5 +PEP_526_style ..\\pythonFiles\\autocomp\\pep526.py /^PEP_526_style: str = "hello world"$/;" kind:variable line:3 +ProducerThread ..\\pythonFiles\\autocomp\\misc.py /^ class ProducerThread(Thread):$/;" kind:class line:1282 +RLock ..\\pythonFiles\\autocomp\\misc.py /^def RLock(*args, **kwargs):$/;" kind:function line:114 +ROPE_PROJECT_FOLDER ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ROPE_PROJECT_FOLDER = sys.argv[2]$/;" kind:variable line:18 +ROPE_PROJECT_FOLDER ..\\pythonFiles\\sorting\\noconfig\\after.py /^ROPE_PROJECT_FOLDER = sys.argv[2]$/;" kind:variable line:12 +ROPE_PROJECT_FOLDER ..\\pythonFiles\\sorting\\noconfig\\before.py /^ROPE_PROJECT_FOLDER = sys.argv[2]$/;" kind:variable line:9 +ROPE_PROJECT_FOLDER ..\\pythonFiles\\sorting\\noconfig\\original.py /^ROPE_PROJECT_FOLDER = sys.argv[2]$/;" kind:variable line:9 +Random ..\\pythonFiles\\autocomp\\misc.py /^class Random(_random.Random):$/;" kind:class line:1331 +RefactorProgress ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^class RefactorProgress():$/;" kind:class line:21 +RenameRefactor ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^class RenameRefactor(BaseRefactoring):$/;" kind:class line:101 +RopeRefactoring ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^class RopeRefactoring(object):$/;" kind:class line:162 +Semaphore ..\\pythonFiles\\autocomp\\misc.py /^def Semaphore(*args, **kwargs):$/;" kind:function line:412 +TOOLS ..\\pythonFiles\\jupyter\\cells.py /^TOOLS = "pan,wheel_zoom,box_zoom,reset,save,box_select"$/;" kind:variable line:68 +Test_CheckMyApp ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^class Test_CheckMyApp:$/;" kind:class line:6 +Test_CheckMyApp ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_pytest.py /^class Test_CheckMyApp:$/;" kind:class line:6 +Test_CheckMyApp ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_pytest.py /^class Test_CheckMyApp:$/;" kind:class line:6 +Test_Current_Working_Directory ..\\pythonFiles\\testFiles\\cwd\\src\\tests\\test_cwd.py /^class Test_Current_Working_Directory(unittest.TestCase):$/;" kind:class line:6 +Test_NestedClassA ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^ class Test_NestedClassA:$/;" kind:class line:13 +Test_NestedClassA ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_pytest.py /^ class Test_NestedClassA:$/;" kind:class line:13 +Test_NestedClassA ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_pytest.py /^ class Test_NestedClassA:$/;" kind:class line:13 +Test_Root_test1 ..\\pythonFiles\\testFiles\\single\\test_root.py /^class Test_Root_test1(unittest.TestCase):$/;" kind:class line:6 +Test_Root_test1 ..\\pythonFiles\\testFiles\\standard\\test_root.py /^class Test_Root_test1(unittest.TestCase):$/;" kind:class line:6 +Test_Root_test1 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\test_root.py /^class Test_Root_test1(unittest.TestCase):$/;" kind:class line:6 +Test_nested_classB_Of_A ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^ class Test_nested_classB_Of_A:$/;" kind:class line:16 +Test_nested_classB_Of_A ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_pytest.py /^ class Test_nested_classB_Of_A:$/;" kind:class line:16 +Test_nested_classB_Of_A ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_pytest.py /^ class Test_nested_classB_Of_A:$/;" kind:class line:16 +Test_test1 ..\\pythonFiles\\testFiles\\single\\tests\\test_one.py /^class Test_test1(unittest.TestCase):$/;" kind:class line:6 +Test_test1 ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_one.py /^class Test_test1(unittest.TestCase):$/;" kind:class line:6 +Test_test1 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_unittest_one.py /^class Test_test1(unittest.TestCase):$/;" kind:class line:6 +Test_test1 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_one.py /^class Test_test1(unittest.TestCase):$/;" kind:class line:6 +Test_test2 ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^class Test_test2(unittest.TestCase):$/;" kind:class line:3 +Test_test2 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_two.py /^class Test_test2(unittest.TestCase):$/;" kind:class line:3 +Test_test2a ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^class Test_test2a(unittest.TestCase):$/;" kind:class line:17 +Test_test2a ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_two.py /^class Test_test2a(unittest.TestCase):$/;" kind:class line:17 +Test_test2a1 ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^ class Test_test2a1(unittest.TestCase):$/;" kind:class line:24 +Test_test2a1 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_two.py /^ class Test_test2a1(unittest.TestCase):$/;" kind:class line:24 +Test_test3 ..\\pythonFiles\\testFiles\\standard\\tests\\unittest_three_test.py /^class Test_test3(unittest.TestCase):$/;" kind:class line:4 +Test_test3 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\unittest_three_test.py /^class Test_test3(unittest.TestCase):$/;" kind:class line:4 +Test_test_one_1 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_one.py /^class Test_test_one_1(unittest.TestCase):$/;" kind:class line:3 +Test_test_one_2 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_one.py /^class Test_test_one_2(unittest.TestCase):$/;" kind:class line:14 +Test_test_two_1 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_two.py /^class Test_test_two_1(unittest.TestCase):$/;" kind:class line:3 +Test_test_two_2 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_two.py /^class Test_test_two_2(unittest.TestCase):$/;" kind:class line:14 +Thread ..\\pythonFiles\\autocomp\\misc.py /^class Thread(_Verbose):$/;" kind:class line:640 +ThreadError ..\\pythonFiles\\autocomp\\misc.py /^ThreadError = thread.error$/;" kind:variable line:38 +Timer ..\\pythonFiles\\autocomp\\misc.py /^def Timer(*args, **kwargs):$/;" kind:function line:1046 +VERSION ..\\pythonFiles\\autocomp\\misc.py /^ VERSION = 3 # used by getstate\/setstate$/;" kind:variable line:1345 +WORKSPACE_ROOT ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^WORKSPACE_ROOT = sys.argv[1]$/;" kind:variable line:17 +WORKSPACE_ROOT ..\\pythonFiles\\sorting\\noconfig\\after.py /^WORKSPACE_ROOT = sys.argv[1]$/;" kind:variable line:11 +WORKSPACE_ROOT ..\\pythonFiles\\sorting\\noconfig\\before.py /^WORKSPACE_ROOT = sys.argv[1]$/;" kind:variable line:8 +WORKSPACE_ROOT ..\\pythonFiles\\sorting\\noconfig\\original.py /^WORKSPACE_ROOT = sys.argv[1]$/;" kind:variable line:8 +Workspace2Class ..\\pythonFiles\\symbolFiles\\workspace2File.py /^class Workspace2Class(object):$/;" kind:class line:5 +_BoundedSemaphore ..\\pythonFiles\\autocomp\\misc.py /^class _BoundedSemaphore(_Semaphore):$/;" kind:class line:515 +_Condition ..\\pythonFiles\\autocomp\\misc.py /^class _Condition(_Verbose):$/;" kind:class line:255 +_DummyThread ..\\pythonFiles\\autocomp\\misc.py /^class _DummyThread(Thread):$/;" kind:class line:1128 +_Event ..\\pythonFiles\\autocomp\\misc.py /^class _Event(_Verbose):$/;" kind:class line:552 +_MainThread ..\\pythonFiles\\autocomp\\misc.py /^class _MainThread(Thread):$/;" kind:class line:1088 +_RLock ..\\pythonFiles\\autocomp\\misc.py /^class _RLock(_Verbose):$/;" kind:class line:125 +_Semaphore ..\\pythonFiles\\autocomp\\misc.py /^class _Semaphore(_Verbose):$/;" kind:class line:423 +_Timer ..\\pythonFiles\\autocomp\\misc.py /^class _Timer(Thread):$/;" kind:class line:1058 +_VERBOSE ..\\pythonFiles\\autocomp\\misc.py /^_VERBOSE = False$/;" kind:variable line:53 +_Verbose ..\\pythonFiles\\autocomp\\misc.py /^ class _Verbose(object):$/;" kind:class line:57 +_Verbose ..\\pythonFiles\\autocomp\\misc.py /^ class _Verbose(object):$/;" kind:class line:79 +__all__ ..\\pythonFiles\\autocomp\\misc.py /^__all__ = ['activeCount', 'active_count', 'Condition', 'currentThread',$/;" kind:variable line:30 +__bootstrap ..\\pythonFiles\\autocomp\\misc.py /^ def __bootstrap(self):$/;" kind:member line:769 +__bootstrap_inner ..\\pythonFiles\\autocomp\\misc.py /^ def __bootstrap_inner(self):$/;" kind:member line:792 +__delete ..\\pythonFiles\\autocomp\\misc.py /^ def __delete(self):$/;" kind:member line:876 +__enter__ ..\\pythonFiles\\autocomp\\misc.py /^ __enter__ = acquire$/;" kind:variable line:185 +__enter__ ..\\pythonFiles\\autocomp\\misc.py /^ __enter__ = acquire$/;" kind:variable line:477 +__enter__ ..\\pythonFiles\\autocomp\\misc.py /^ def __enter__(self):$/;" kind:member line:285 +__exc_clear ..\\pythonFiles\\autocomp\\misc.py /^ __exc_clear = _sys.exc_clear$/;" kind:variable line:654 +__exc_info ..\\pythonFiles\\autocomp\\misc.py /^ __exc_info = _sys.exc_info$/;" kind:variable line:651 +__exit__ ..\\pythonFiles\\autocomp\\misc.py /^ def __exit__(self, *args):$/;" kind:member line:288 +__exit__ ..\\pythonFiles\\autocomp\\misc.py /^ def __exit__(self, t, v, tb):$/;" kind:member line:215 +__exit__ ..\\pythonFiles\\autocomp\\misc.py /^ def __exit__(self, t, v, tb):$/;" kind:member line:493 +__getstate__ ..\\pythonFiles\\autocomp\\misc.py /^ def __getstate__(self): # for pickle$/;" kind:member line:1422 +__init__ ..\\multiRootWkspc\\disableLinters\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\multiRootWkspc\\parent\\child\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\multiRootWkspc\\workspace1\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\multiRootWkspc\\workspace2\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\multiRootWkspc\\workspace3\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, limit):$/;" kind:member line:1252 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, queue, count):$/;" kind:member line:1300 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, queue, quota):$/;" kind:member line:1284 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, verbose=None):$/;" kind:member line:59 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, verbose=None):$/;" kind:member line:80 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self):$/;" kind:member line:1090 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self):$/;" kind:member line:1130 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, group=None, target=None, name=None,$/;" kind:member line:656 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, interval, function, args=[], kwargs={}):$/;" kind:member line:1067 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, lock=None, verbose=None):$/;" kind:member line:260 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, value=1, verbose=None):$/;" kind:member line:433 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, value=1, verbose=None):$/;" kind:member line:521 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, verbose=None):$/;" kind:member line:132 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, verbose=None):$/;" kind:member line:561 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, x=None):$/;" kind:member line:1347 +__init__ ..\\pythonFiles\\autocomp\\one.py /^ def __init__(self, file_path=None, file_contents=None):$/;" kind:member line:14 +__init__ ..\\pythonFiles\\definition\\await.test.py /^ def __init__(self):$/;" kind:member line:4 +__init__ ..\\pythonFiles\\definition\\one.py /^ def __init__(self, file_path=None, file_contents=None):$/;" kind:member line:14 +__init__ ..\\pythonFiles\\formatting\\fileToFormat.py /^ def __init__ ( self, bar ):$/;" kind:member line:13 +__init__ ..\\pythonFiles\\jupyter\\cells.py /^ def __init__(self, mean=0.0, std=1, size=1000):$/;" kind:member line:104 +__init__ ..\\pythonFiles\\linting\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\pythonFiles\\linting\\flake8config\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\pythonFiles\\linting\\pep8config\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\pythonFiles\\linting\\pylintconfig\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def __init__(self):$/;" kind:member line:164 +__init__ ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def __init__(self, filePath, fileMode=ChangeType.EDIT, diff=""):$/;" kind:member line:48 +__init__ ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def __init__(self, name='Task Name', message=None, percent=0):$/;" kind:member line:26 +__init__ ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def __init__(self, project, resource, name="Extract Method", progressCallback=None, startOff/;" kind:member line:146 +__init__ ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def __init__(self, project, resource, name="Extract Variable", progressCallback=None, startO/;" kind:member line:122 +__init__ ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def __init__(self, project, resource, name="Refactor", progressCallback=None):$/;" kind:member line:59 +__init__ ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def __init__(self, project, resource, name="Rename", progressCallback=None, startOffset=None/;" kind:member line:103 +__init__ ..\\pythonFiles\\symbolFiles\\childFile.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\pythonFiles\\symbolFiles\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\pythonFiles\\symbolFiles\\workspace2File.py /^ def __init__(self):$/;" kind:member line:8 +__init__.py ..\\pythonFiles\\autoimport\\two\\__init__.py 1;" kind:file line:1 +__initialized ..\\pythonFiles\\autocomp\\misc.py /^ __initialized = False$/;" kind:variable line:646 +__reduce__ ..\\pythonFiles\\autocomp\\misc.py /^ def __reduce__(self):$/;" kind:member line:1428 +__repr__ ..\\pythonFiles\\autocomp\\misc.py /^ def __repr__(self):$/;" kind:member line:138 +__repr__ ..\\pythonFiles\\autocomp\\misc.py /^ def __repr__(self):$/;" kind:member line:291 +__repr__ ..\\pythonFiles\\autocomp\\misc.py /^ def __repr__(self):$/;" kind:member line:713 +__revision__ ..\\multiRootWkspc\\disableLinters\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\multiRootWkspc\\parent\\child\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\multiRootWkspc\\workspace1\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\multiRootWkspc\\workspace2\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\multiRootWkspc\\workspace3\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\pythonFiles\\linting\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\pythonFiles\\linting\\flake8config\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\pythonFiles\\linting\\pep8config\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\pythonFiles\\linting\\pylintconfig\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\pythonFiles\\symbolFiles\\childFile.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\pythonFiles\\symbolFiles\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\pythonFiles\\symbolFiles\\workspace2File.py /^__revision__ = None$/;" kind:variable line:3 +__setstate__ ..\\pythonFiles\\autocomp\\misc.py /^ def __setstate__(self, state): # for pickle$/;" kind:member line:1425 +__stop ..\\pythonFiles\\autocomp\\misc.py /^ def __stop(self):$/;" kind:member line:866 +_acquire_restore ..\\pythonFiles\\autocomp\\misc.py /^ def _acquire_restore(self, count_owner):$/;" kind:member line:220 +_acquire_restore ..\\pythonFiles\\autocomp\\misc.py /^ def _acquire_restore(self, x):$/;" kind:member line:297 +_active ..\\pythonFiles\\autocomp\\misc.py /^_active = {} # maps thread id to Thread object$/;" kind:variable line:634 +_active_limbo_lock ..\\pythonFiles\\autocomp\\misc.py /^_active_limbo_lock = _allocate_lock()$/;" kind:variable line:633 +_after_fork ..\\pythonFiles\\autocomp\\misc.py /^def _after_fork():$/;" kind:function line:1211 +_allocate_lock ..\\pythonFiles\\autocomp\\misc.py /^_allocate_lock = thread.allocate_lock$/;" kind:variable line:36 +_block ..\\pythonFiles\\autocomp\\misc.py /^ def _block(self):$/;" kind:member line:705 +_count ..\\pythonFiles\\autocomp\\misc.py /^from itertools import count as _count$/;" kind:unknown line:14 +_counter ..\\pythonFiles\\autocomp\\misc.py /^_counter = _count().next$/;" kind:variable line:627 +_deque ..\\pythonFiles\\autocomp\\misc.py /^from collections import deque as _deque$/;" kind:unknown line:13 +_deserialize ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def _deserialize(self, request):$/;" kind:member line:204 +_enumerate ..\\pythonFiles\\autocomp\\misc.py /^def _enumerate():$/;" kind:function line:1179 +_exitfunc ..\\pythonFiles\\autocomp\\misc.py /^ def _exitfunc(self):$/;" kind:member line:1100 +_extractMethod ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def _extractMethod(self, filePath, start, end, newName):$/;" kind:member line:183 +_extractVariable ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def _extractVariable(self, filePath, start, end, newName):$/;" kind:member line:168 +_figure_data ..\\pythonFiles\\jupyter\\cells.py /^ def _figure_data(self, format):$/;" kind:member line:112 +_format_exc ..\\pythonFiles\\autocomp\\misc.py /^from traceback import format_exc as _format_exc$/;" kind:unknown line:16 +_get_ident ..\\pythonFiles\\autocomp\\misc.py /^_get_ident = thread.get_ident$/;" kind:variable line:37 +_is_owned ..\\pythonFiles\\autocomp\\misc.py /^ def _is_owned(self):$/;" kind:member line:238 +_is_owned ..\\pythonFiles\\autocomp\\misc.py /^ def _is_owned(self):$/;" kind:member line:300 +_limbo ..\\pythonFiles\\autocomp\\misc.py /^_limbo = {}$/;" kind:variable line:635 +_newname ..\\pythonFiles\\autocomp\\misc.py /^def _newname(template="Thread-%d"):$/;" kind:function line:629 +_note ..\\pythonFiles\\autocomp\\misc.py /^ def _note(self, *args):$/;" kind:member line:82 +_note ..\\pythonFiles\\autocomp\\misc.py /^ def _note(self, format, *args):$/;" kind:member line:64 +_pickSomeNonDaemonThread ..\\pythonFiles\\autocomp\\misc.py /^def _pickSomeNonDaemonThread():$/;" kind:function line:1113 +_process_request ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def _process_request(self, request):$/;" kind:member line:215 +_profile_hook ..\\pythonFiles\\autocomp\\misc.py /^_profile_hook = None$/;" kind:variable line:87 +_randbelow ..\\pythonFiles\\autocomp\\misc.py /^ def _randbelow(self, n, int=int, maxsize=1< int:$/;" kind:function line:6 +after.py ..\\pythonFiles\\sorting\\noconfig\\after.py 1;" kind:file line:1 +after.py ..\\pythonFiles\\sorting\\withconfig\\after.py 1;" kind:file line:1 +ask_ok ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^ def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):$/;" kind:member line:263 +ask_ok ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):$/;" kind:function line:124 +ask_ok ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^ def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):$/;" kind:member line:263 +ask_ok ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):$/;" kind:function line:124 +ask_ok ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^ def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):$/;" kind:member line:263 +ask_ok ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):$/;" kind:function line:124 +await.test.py ..\\pythonFiles\\definition\\await.test.py 1;" kind:file line:1 +ax ..\\pythonFiles\\jupyter\\cells.py /^fig, ax = plt.subplots(subplot_kw=dict(axisbg='#EEEEEE'))$/;" kind:variable line:39 +b ..\\pythonFiles\\autocomp\\pep526.py /^ b: int = 0$/;" kind:variable line:18 +b ..\\pythonFiles\\typeFormatFiles\\elseBlocksFirstLine2.py /^ b = 3$/;" kind:variable line:3 +b ..\\pythonFiles\\typeFormatFiles\\elseBlocksFirstLine4.py /^ b = 3$/;" kind:variable line:3 +b ..\\pythonFiles\\typeFormatFiles\\elseBlocksFirstLineTab.py /^ b = 3$/;" kind:variable line:3 +bar ..\\pythonFiles\\autocomp\\four.py /^ def bar():$/;" kind:member line:11 +bar ..\\pythonFiles\\definition\\four.py /^ def bar():$/;" kind:member line:11 +before.1.py ..\\pythonFiles\\sorting\\withconfig\\before.1.py 1;" kind:file line:1 +before.py ..\\pythonFiles\\sorting\\noconfig\\before.py 1;" kind:file line:1 +before.py ..\\pythonFiles\\sorting\\withconfig\\before.py 1;" kind:file line:1 +betavariate ..\\pythonFiles\\autocomp\\misc.py /^ def betavariate(self, alpha, beta):$/;" kind:member line:1862 +calculate_cash_flows ..\\pythonFiles\\definition\\decorators.py /^def calculate_cash_flows(remaining_loan_term, remaining_io_term,$/;" kind:function line:20 +cancel ..\\pythonFiles\\autocomp\\misc.py /^ def cancel(self):$/;" kind:member line:1075 +cells.py ..\\pythonFiles\\jupyter\\cells.py 1;" kind:file line:1 +childFile.py ..\\pythonFiles\\symbolFiles\\childFile.py 1;" kind:file line:1 +choice ..\\pythonFiles\\autocomp\\misc.py /^ def choice(self, seq):$/;" kind:member line:1513 +clear ..\\pythonFiles\\autocomp\\misc.py /^ def clear(self):$/;" kind:member line:590 +content ..\\pythonFiles\\autocomp\\doc.py /^ content = line.upper()$/;" kind:variable line:6 +ct ..\\pythonFiles\\autocomp\\two.py /^class ct:$/;" kind:class line:1 +ct ..\\pythonFiles\\definition\\two.py /^class ct:$/;" kind:class line:1 +currentThread ..\\pythonFiles\\autocomp\\misc.py /^def currentThread():$/;" kind:function line:1152 +current_thread ..\\pythonFiles\\autocomp\\misc.py /^current_thread = currentThread$/;" kind:variable line:1165 +daemon ..\\pythonFiles\\autocomp\\misc.py /^ def daemon(self):$/;" kind:member line:1009 +daemon ..\\pythonFiles\\autocomp\\misc.py /^ def daemon(self, daemonic):$/;" kind:member line:1025 +deco.py ..\\pythonFiles\\autocomp\\deco.py 1;" kind:file line:1 +decorators.py ..\\pythonFiles\\definition\\decorators.py 1;" kind:file line:1 +description ..\\pythonFiles\\autocomp\\one.py /^ description = "Run isort on modules registered in setuptools"$/;" kind:variable line:11 +description ..\\pythonFiles\\definition\\one.py /^ description = "Run isort on modules registered in setuptools"$/;" kind:variable line:11 +df ..\\pythonFiles\\jupyter\\cells.py /^df = df.cumsum()$/;" kind:variable line:87 +df ..\\pythonFiles\\jupyter\\cells.py /^df = pd.DataFrame(np.random.randn(1000, 4), index=ts.index,$/;" kind:variable line:85 +divide ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^ def divide(x, y):$/;" kind:member line:329 +divide ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^def divide(x, y):$/;" kind:function line:190 +divide ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^ def divide(x, y):$/;" kind:member line:329 +divide ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^def divide(x, y):$/;" kind:function line:190 +divide ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^ def divide(x, y):$/;" kind:member line:329 +divide ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^def divide(x, y):$/;" kind:function line:190 +divide ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^def divide(x, y):$/;" kind:function line:188 +divide ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^def divide(x, y):$/;" kind:function line:199 +divide ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^def divide(x, y):$/;" kind:function line:188 +divide ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^def divide(x, y):$/;" kind:function line:199 +divide ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^def divide(x, y):$/;" kind:function line:188 +divide ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^def divide(x, y):$/;" kind:function line:199 +doc.py ..\\pythonFiles\\autocomp\\doc.py 1;" kind:file line:1 +dummy.py ..\\pythonFiles\\dummy.py 1;" kind:file line:1 +elseBlocks2.py ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py 1;" kind:file line:1 +elseBlocks4.py ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py 1;" kind:file line:1 +elseBlocksFirstLine2.py ..\\pythonFiles\\typeFormatFiles\\elseBlocksFirstLine2.py 1;" kind:file line:1 +elseBlocksFirstLine4.py ..\\pythonFiles\\typeFormatFiles\\elseBlocksFirstLine4.py 1;" kind:file line:1 +elseBlocksFirstLineTab.py ..\\pythonFiles\\typeFormatFiles\\elseBlocksFirstLineTab.py 1;" kind:file line:1 +elseBlocksTab.py ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py 1;" kind:file line:1 +enumerate ..\\pythonFiles\\autocomp\\misc.py /^def enumerate():$/;" kind:function line:1183 +example1 ..\\pythonFiles\\formatting\\fileToFormat.py /^def example1():$/;" kind:function line:3 +example2 ..\\pythonFiles\\formatting\\fileToFormat.py /^def example2(): return {'has_key() is deprecated':True}.has_key({'f':2}.has_key(''));$/;" kind:function line:11 +expovariate ..\\pythonFiles\\autocomp\\misc.py /^ def expovariate(self, lambd):$/;" kind:member line:1670 +fig ..\\pythonFiles\\jupyter\\cells.py /^fig, ax = plt.subplots(subplot_kw=dict(axisbg='#EEEEEE'))$/;" kind:variable line:39 +file.py ..\\multiRootWkspc\\disableLinters\\file.py 1;" kind:file line:1 +file.py ..\\multiRootWkspc\\parent\\child\\file.py 1;" kind:file line:1 +file.py ..\\multiRootWkspc\\workspace1\\file.py 1;" kind:file line:1 +file.py ..\\multiRootWkspc\\workspace2\\file.py 1;" kind:file line:1 +file.py ..\\multiRootWkspc\\workspace3\\file.py 1;" kind:file line:1 +file.py ..\\pythonFiles\\linting\\file.py 1;" kind:file line:1 +file.py ..\\pythonFiles\\linting\\flake8config\\file.py 1;" kind:file line:1 +file.py ..\\pythonFiles\\linting\\pep8config\\file.py 1;" kind:file line:1 +file.py ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py 1;" kind:file line:1 +file.py ..\\pythonFiles\\linting\\pylintconfig\\file.py 1;" kind:file line:1 +file.py ..\\pythonFiles\\symbolFiles\\file.py 1;" kind:file line:1 +fileToFormat.py ..\\pythonFiles\\formatting\\fileToFormat.py 1;" kind:file line:1 +five.py ..\\pythonFiles\\autocomp\\five.py 1;" kind:file line:1 +five.py ..\\pythonFiles\\definition\\five.py 1;" kind:file line:1 +four.py ..\\pythonFiles\\autocomp\\four.py 1;" kind:file line:1 +four.py ..\\pythonFiles\\definition\\four.py 1;" kind:file line:1 +fun ..\\pythonFiles\\autocomp\\two.py /^ def fun():$/;" kind:member line:2 +fun ..\\pythonFiles\\definition\\two.py /^ def fun():$/;" kind:member line:2 +function1 ..\\pythonFiles\\definition\\one.py /^def function1():$/;" kind:function line:33 +function2 ..\\pythonFiles\\definition\\one.py /^def function2():$/;" kind:function line:37 +function3 ..\\pythonFiles\\definition\\one.py /^def function3():$/;" kind:function line:40 +function4 ..\\pythonFiles\\definition\\one.py /^def function4():$/;" kind:function line:43 +gammavariate ..\\pythonFiles\\autocomp\\misc.py /^ def gammavariate(self, alpha, beta):$/;" kind:member line:1737 +gauss ..\\pythonFiles\\autocomp\\misc.py /^ def gauss(self, mu, sigma):$/;" kind:member line:1809 +get ..\\pythonFiles\\autocomp\\misc.py /^ def get(self):$/;" kind:member line:1271 +getName ..\\pythonFiles\\autocomp\\misc.py /^ def getName(self):$/;" kind:member line:1038 +getstate ..\\pythonFiles\\autocomp\\misc.py /^ def getstate(self):$/;" kind:member line:1388 +greeting ..\\pythonFiles\\autocomp\\pep484.py /^def greeting(name: str) -> str:$/;" kind:function line:2 +hoverTest.py ..\\pythonFiles\\autocomp\\hoverTest.py 1;" kind:file line:1 +ident ..\\pythonFiles\\autocomp\\misc.py /^ def ident(self):$/;" kind:member line:984 +identity ..\\pythonFiles\\definition\\decorators.py /^def identity(ob):$/;" kind:function line:1 +imp.py ..\\pythonFiles\\autocomp\\imp.py 1;" kind:file line:1 +instant_print ..\\pythonFiles\\autocomp\\lamb.py /^instant_print = lambda x: [print(x), sys.stdout.flush(), sys.stderr.flush()]$/;" kind:function line:1 +isAlive ..\\pythonFiles\\autocomp\\misc.py /^ def isAlive(self):$/;" kind:member line:995 +isDaemon ..\\pythonFiles\\autocomp\\misc.py /^ def isDaemon(self):$/;" kind:member line:1032 +isSet ..\\pythonFiles\\autocomp\\misc.py /^ def isSet(self):$/;" kind:member line:570 +is_alive ..\\pythonFiles\\autocomp\\misc.py /^ is_alive = isAlive$/;" kind:variable line:1006 +is_set ..\\pythonFiles\\autocomp\\misc.py /^ is_set = isSet$/;" kind:variable line:574 +join ..\\pythonFiles\\autocomp\\misc.py /^ def join(self, timeout=None):$/;" kind:member line:1146 +join ..\\pythonFiles\\autocomp\\misc.py /^ def join(self, timeout=None):$/;" kind:member line:911 +lamb.py ..\\pythonFiles\\autocomp\\lamb.py 1;" kind:file line:1 +local ..\\pythonFiles\\autocomp\\misc.py /^ from thread import _local as local$/;" kind:unknown line:1206 +lognormvariate ..\\pythonFiles\\autocomp\\misc.py /^ def lognormvariate(self, mu, sigma):$/;" kind:member line:1658 +meth1 ..\\multiRootWkspc\\disableLinters\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1 ..\\multiRootWkspc\\parent\\child\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1 ..\\multiRootWkspc\\workspace1\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1 ..\\multiRootWkspc\\workspace2\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1 ..\\multiRootWkspc\\workspace3\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1 ..\\pythonFiles\\linting\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1 ..\\pythonFiles\\linting\\flake8config\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1 ..\\pythonFiles\\linting\\pep8config\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1 ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1 ..\\pythonFiles\\linting\\pylintconfig\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1 ..\\pythonFiles\\symbolFiles\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1OfChild ..\\pythonFiles\\symbolFiles\\childFile.py /^ def meth1OfChild(self, arg):$/;" kind:member line:11 +meth1OfWorkspace2 ..\\pythonFiles\\symbolFiles\\workspace2File.py /^ def meth1OfWorkspace2(self, arg):$/;" kind:member line:11 +meth2 ..\\multiRootWkspc\\disableLinters\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth2 ..\\multiRootWkspc\\parent\\child\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth2 ..\\multiRootWkspc\\workspace1\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth2 ..\\multiRootWkspc\\workspace2\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth2 ..\\multiRootWkspc\\workspace3\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth2 ..\\pythonFiles\\linting\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth2 ..\\pythonFiles\\linting\\flake8config\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth2 ..\\pythonFiles\\linting\\pep8config\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth2 ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth2 ..\\pythonFiles\\linting\\pylintconfig\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth2 ..\\pythonFiles\\symbolFiles\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 ..\\multiRootWkspc\\disableLinters\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth3 ..\\multiRootWkspc\\parent\\child\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth3 ..\\multiRootWkspc\\workspace1\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth3 ..\\multiRootWkspc\\workspace2\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth3 ..\\multiRootWkspc\\workspace3\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth3 ..\\pythonFiles\\linting\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth3 ..\\pythonFiles\\linting\\flake8config\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth3 ..\\pythonFiles\\linting\\pep8config\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth3 ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth3 ..\\pythonFiles\\linting\\pylintconfig\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth3 ..\\pythonFiles\\symbolFiles\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 ..\\multiRootWkspc\\disableLinters\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth4 ..\\multiRootWkspc\\parent\\child\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth4 ..\\multiRootWkspc\\workspace1\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth4 ..\\multiRootWkspc\\workspace2\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth4 ..\\multiRootWkspc\\workspace3\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth4 ..\\pythonFiles\\linting\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth4 ..\\pythonFiles\\linting\\flake8config\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth4 ..\\pythonFiles\\linting\\pep8config\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth4 ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth4 ..\\pythonFiles\\linting\\pylintconfig\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth4 ..\\pythonFiles\\symbolFiles\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 ..\\multiRootWkspc\\disableLinters\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth5 ..\\multiRootWkspc\\parent\\child\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth5 ..\\multiRootWkspc\\workspace1\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth5 ..\\multiRootWkspc\\workspace2\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth5 ..\\multiRootWkspc\\workspace3\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth5 ..\\pythonFiles\\linting\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth5 ..\\pythonFiles\\linting\\flake8config\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth5 ..\\pythonFiles\\linting\\pep8config\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth5 ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth5 ..\\pythonFiles\\linting\\pylintconfig\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth5 ..\\pythonFiles\\symbolFiles\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 ..\\multiRootWkspc\\disableLinters\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth6 ..\\multiRootWkspc\\parent\\child\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth6 ..\\multiRootWkspc\\workspace1\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth6 ..\\multiRootWkspc\\workspace2\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth6 ..\\multiRootWkspc\\workspace3\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth6 ..\\pythonFiles\\linting\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth6 ..\\pythonFiles\\linting\\flake8config\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth6 ..\\pythonFiles\\linting\\pep8config\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth6 ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth6 ..\\pythonFiles\\linting\\pylintconfig\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth6 ..\\pythonFiles\\symbolFiles\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 ..\\multiRootWkspc\\disableLinters\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth7 ..\\multiRootWkspc\\parent\\child\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth7 ..\\multiRootWkspc\\workspace1\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth7 ..\\multiRootWkspc\\workspace2\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth7 ..\\multiRootWkspc\\workspace3\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth7 ..\\pythonFiles\\linting\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth7 ..\\pythonFiles\\linting\\flake8config\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth7 ..\\pythonFiles\\linting\\pep8config\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth7 ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth7 ..\\pythonFiles\\linting\\pylintconfig\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth7 ..\\pythonFiles\\symbolFiles\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 ..\\multiRootWkspc\\disableLinters\\file.py /^ def meth8(self):$/;" kind:member line:80 +meth8 ..\\multiRootWkspc\\parent\\child\\file.py /^ def meth8(self):$/;" kind:member line:80 +meth8 ..\\multiRootWkspc\\workspace1\\file.py /^ def meth8(self):$/;" kind:member line:80 +meth8 ..\\multiRootWkspc\\workspace2\\file.py /^ def meth8(self):$/;" kind:member line:80 +meth8 ..\\multiRootWkspc\\workspace3\\file.py /^ def meth8(self):$/;" kind:member line:80 +meth8 ..\\pythonFiles\\linting\\file.py /^ def meth8(self):$/;" kind:member line:80 +meth8 ..\\pythonFiles\\linting\\flake8config\\file.py /^ def meth8(self):$/;" kind:member line:80 +meth8 ..\\pythonFiles\\linting\\pep8config\\file.py /^ def meth8(self):$/;" kind:member line:80 +meth8 ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^ def meth8(self):$/;" kind:member line:80 +meth8 ..\\pythonFiles\\linting\\pylintconfig\\file.py /^ def meth8(self):$/;" kind:member line:80 +meth8 ..\\pythonFiles\\symbolFiles\\file.py /^ def meth8(self):$/;" kind:member line:80 +method1 ..\\pythonFiles\\autocomp\\one.py /^ def method1(self):$/;" kind:member line:18 +method1 ..\\pythonFiles\\definition\\one.py /^ def method1(self):$/;" kind:member line:18 +method2 ..\\pythonFiles\\autocomp\\one.py /^ def method2(self):$/;" kind:member line:24 +method2 ..\\pythonFiles\\definition\\one.py /^ def method2(self):$/;" kind:member line:24 +minus ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^ def minus():$/;" kind:member line:287 +minus ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^def minus():$/;" kind:function line:148 +minus ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^ def minus():$/;" kind:member line:287 +minus ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^def minus():$/;" kind:function line:148 +minus ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^ def minus():$/;" kind:member line:287 +minus ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^def minus():$/;" kind:function line:148 +minus ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^def minus():$/;" kind:function line:100 +minus ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^def minus():$/;" kind:function line:91 +minus ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^def minus():$/;" kind:function line:100 +minus ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^def minus():$/;" kind:function line:91 +minus ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^def minus():$/;" kind:function line:100 +minus ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^def minus():$/;" kind:function line:91 +misc.py ..\\pythonFiles\\autocomp\\misc.py 1;" kind:file line:1 +mpl ..\\pythonFiles\\jupyter\\cells.py /^import matplotlib as mpl$/;" kind:namespace line:4 +mpl ..\\pythonFiles\\jupyter\\cells.py /^import matplotlib as mpl$/;" kind:namespace line:94 +myfunc ..\\pythonFiles\\definition\\decorators.py /^def myfunc():$/;" kind:function line:5 +name ..\\pythonFiles\\autocomp\\misc.py /^ def name(self):$/;" kind:member line:968 +name ..\\pythonFiles\\autocomp\\misc.py /^ def name(self, name):$/;" kind:member line:979 +non_parametrized_username ..\\pythonFiles\\testFiles\\standard\\tests\\test_another_pytest.py /^def non_parametrized_username(request):$/;" kind:function line:10 +non_parametrized_username ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^def non_parametrized_username(request):$/;" kind:function line:33 +non_parametrized_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_pytest.py /^def non_parametrized_username(request):$/;" kind:function line:33 +non_parametrized_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_another_pytest.py /^def non_parametrized_username(request):$/;" kind:function line:10 +non_parametrized_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_pytest.py /^def non_parametrized_username(request):$/;" kind:function line:33 +normalvariate ..\\pythonFiles\\autocomp\\misc.py /^ def normalvariate(self, mu, sigma):$/;" kind:member line:1633 +notify ..\\pythonFiles\\autocomp\\misc.py /^ def notify(self, n=1):$/;" kind:member line:373 +notifyAll ..\\pythonFiles\\autocomp\\misc.py /^ def notifyAll(self):$/;" kind:member line:400 +notify_all ..\\pythonFiles\\autocomp\\misc.py /^ notify_all = notifyAll$/;" kind:variable line:409 +np ..\\pythonFiles\\jupyter\\cells.py /^import numpy as np$/;" kind:namespace line:34 +np ..\\pythonFiles\\jupyter\\cells.py /^import numpy as np$/;" kind:namespace line:5 +np ..\\pythonFiles\\jupyter\\cells.py /^import numpy as np$/;" kind:namespace line:63 +np ..\\pythonFiles\\jupyter\\cells.py /^import numpy as np$/;" kind:namespace line:78 +np ..\\pythonFiles\\jupyter\\cells.py /^import numpy as np$/;" kind:namespace line:97 +obj ..\\pythonFiles\\autocomp\\one.py /^obj = Class1()$/;" kind:variable line:30 +obj ..\\pythonFiles\\definition\\one.py /^obj = Class1()$/;" kind:variable line:30 +onRefactor ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def onRefactor(self):$/;" kind:member line:109 +onRefactor ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def onRefactor(self):$/;" kind:member line:131 +onRefactor ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def onRefactor(self):$/;" kind:member line:149 +onRefactor ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def onRefactor(self):$/;" kind:member line:94 +one ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^def one():$/;" kind:function line:134 +one ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^def one():$/;" kind:function line:150 +one ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^def one():$/;" kind:function line:134 +one ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^def one():$/;" kind:function line:150 +one ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^def one():$/;" kind:function line:134 +one ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^def one():$/;" kind:function line:150 +one.py ..\\pythonFiles\\autocomp\\one.py 1;" kind:file line:1 +one.py ..\\pythonFiles\\autoimport\\one.py 1;" kind:file line:1 +one.py ..\\pythonFiles\\definition\\one.py 1;" kind:file line:1 +one.py ..\\pythonFiles\\docstrings\\one.py 1;" kind:file line:1 +original.1.py ..\\pythonFiles\\sorting\\withconfig\\original.1.py 1;" kind:file line:1 +original.py ..\\pythonFiles\\sorting\\noconfig\\original.py 1;" kind:file line:1 +original.py ..\\pythonFiles\\sorting\\withconfig\\original.py 1;" kind:file line:1 +p1 ..\\pythonFiles\\jupyter\\cells.py /^p1 = figure(title="Legend Example", tools=TOOLS)$/;" kind:variable line:70 +parametrized_username ..\\pythonFiles\\testFiles\\standard\\tests\\test_another_pytest.py /^def parametrized_username():$/;" kind:function line:6 +parametrized_username ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^def parametrized_username():$/;" kind:function line:29 +parametrized_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_pytest.py /^def parametrized_username():$/;" kind:function line:29 +parametrized_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_another_pytest.py /^def parametrized_username():$/;" kind:function line:6 +parametrized_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_pytest.py /^def parametrized_username():$/;" kind:function line:29 +paretovariate ..\\pythonFiles\\autocomp\\misc.py /^ def paretovariate(self, alpha):$/;" kind:member line:1880 +pd ..\\pythonFiles\\jupyter\\cells.py /^import pandas as pd$/;" kind:namespace line:77 +pep484.py ..\\pythonFiles\\autocomp\\pep484.py 1;" kind:file line:1 +pep526.py ..\\pythonFiles\\autocomp\\pep526.py 1;" kind:file line:1 +plain.py ..\\pythonFiles\\shebang\\plain.py 1;" kind:file line:1 +plt ..\\pythonFiles\\jupyter\\cells.py /^from matplotlib import pyplot as plt$/;" kind:unknown line:80 +plt ..\\pythonFiles\\jupyter\\cells.py /^import matplotlib.pyplot as plt$/;" kind:namespace line:3 +plt ..\\pythonFiles\\jupyter\\cells.py /^import matplotlib.pyplot as plt$/;" kind:namespace line:33 +plt ..\\pythonFiles\\jupyter\\cells.py /^import matplotlib.pyplot as plt$/;" kind:namespace line:93 +print_hello ..\\pythonFiles\\hover\\stringFormat.py /^def print_hello(name):$/;" kind:function line:2 +put ..\\pythonFiles\\autocomp\\misc.py /^ def put(self, item):$/;" kind:member line:1260 +randint ..\\pythonFiles\\autocomp\\misc.py /^ def randint(self, a, b):$/;" kind:member line:1477 +randrange ..\\pythonFiles\\autocomp\\misc.py /^ def randrange(self, start, stop=None, step=1, _int=int):$/;" kind:member line:1433 +refactor ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def refactor(self):$/;" kind:member line:87 +refactor.py ..\\pythonFiles\\refactoring\\standAlone\\refactor.py 1;" kind:file line:1 +release ..\\pythonFiles\\autocomp\\misc.py /^ def release(self):$/;" kind:member line:187 +release ..\\pythonFiles\\autocomp\\misc.py /^ def release(self):$/;" kind:member line:479 +release ..\\pythonFiles\\autocomp\\misc.py /^ def release(self):$/;" kind:member line:525 +rnd ..\\pythonFiles\\autocomp\\hoverTest.py /^rnd = random.Random()$/;" kind:variable line:7 +rnd2 ..\\pythonFiles\\autocomp\\hoverTest.py /^rnd2 = misc.Random()$/;" kind:variable line:12 +run ..\\pythonFiles\\autocomp\\misc.py /^ def run(self):$/;" kind:member line:1289 +run ..\\pythonFiles\\autocomp\\misc.py /^ def run(self):$/;" kind:member line:1305 +run ..\\pythonFiles\\autocomp\\misc.py /^ def run(self):$/;" kind:member line:1079 +run ..\\pythonFiles\\autocomp\\misc.py /^ def run(self):$/;" kind:member line:752 +sample ..\\pythonFiles\\autocomp\\misc.py /^ def sample(self, population, k):$/;" kind:member line:1543 +scatter ..\\pythonFiles\\jupyter\\cells.py /^scatter = ax.scatter(np.random.normal(size=N),$/;" kind:variable line:43 +seed ..\\pythonFiles\\autocomp\\misc.py /^ def seed(self, a=None, version=2):$/;" kind:member line:1356 +set ..\\pythonFiles\\autocomp\\misc.py /^ def set(self):$/;" kind:member line:576 +setDaemon ..\\pythonFiles\\autocomp\\misc.py /^ def setDaemon(self, daemonic):$/;" kind:member line:1035 +setName ..\\pythonFiles\\autocomp\\misc.py /^ def setName(self, name):$/;" kind:member line:1041 +setprofile ..\\pythonFiles\\autocomp\\misc.py /^def setprofile(func):$/;" kind:function line:90 +setstate ..\\pythonFiles\\autocomp\\misc.py /^ def setstate(self, state):$/;" kind:member line:1392 +settrace ..\\pythonFiles\\autocomp\\misc.py /^def settrace(func):$/;" kind:function line:100 +shebang.py ..\\pythonFiles\\shebang\\shebang.py 1;" kind:file line:1 +shebangEnv.py ..\\pythonFiles\\shebang\\shebangEnv.py 1;" kind:file line:1 +shebangInvalid.py ..\\pythonFiles\\shebang\\shebangInvalid.py 1;" kind:file line:1 +showMessage ..\\pythonFiles\\autocomp\\four.py /^def showMessage():$/;" kind:function line:19 +showMessage ..\\pythonFiles\\definition\\four.py /^def showMessage():$/;" kind:function line:19 +shuffle ..\\pythonFiles\\autocomp\\misc.py /^ def shuffle(self, x, random=None):$/;" kind:member line:1521 +start ..\\pythonFiles\\autocomp\\misc.py /^ def start(self):$/;" kind:member line:726 +stop ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def stop(self):$/;" kind:member line:84 +stringFormat.py ..\\pythonFiles\\hover\\stringFormat.py 1;" kind:file line:1 +t ..\\pythonFiles\\autocomp\\hoverTest.py /^t = misc.Thread()$/;" kind:variable line:15 +test ..\\pythonFiles\\definition\\await.test.py /^ async def test(self):$/;" kind:member line:7 +test ..\\pythonFiles\\sorting\\noconfig\\after.py /^def test():$/;" kind:function line:15 +test ..\\pythonFiles\\sorting\\noconfig\\before.py /^def test():$/;" kind:function line:12 +test ..\\pythonFiles\\sorting\\noconfig\\original.py /^def test():$/;" kind:function line:12 +test ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^ def test():$/;" kind:member line:201 +test ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^def test():$/;" kind:function line:62 +test ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^ def test():$/;" kind:member line:201 +test ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^def test():$/;" kind:function line:62 +test ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^ def test():$/;" kind:member line:201 +test ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^def test():$/;" kind:function line:62 +test2 ..\\pythonFiles\\definition\\await.test.py /^ async def test2(self):$/;" kind:member line:10 +test_1_1_1 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_one.py /^ def test_1_1_1(self):$/;" kind:member line:4 +test_1_1_1 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_two.py /^ def test_1_1_1(self):$/;" kind:member line:4 +test_1_1_2 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_one.py /^ def test_1_1_2(self):$/;" kind:member line:7 +test_1_1_2 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_two.py /^ def test_1_1_2(self):$/;" kind:member line:7 +test_1_1_3 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_one.py /^ def test_1_1_3(self):$/;" kind:member line:11 +test_1_1_3 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_two.py /^ def test_1_1_3(self):$/;" kind:member line:11 +test_1_2_1 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_one.py /^ def test_1_2_1(self):$/;" kind:member line:15 +test_222A2 ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^ def test_222A2(self):$/;" kind:member line:18 +test_222A2 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_two.py /^ def test_222A2(self):$/;" kind:member line:18 +test_222A2wow ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^ def test_222A2wow(self):$/;" kind:member line:25 +test_222A2wow ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_two.py /^ def test_222A2wow(self):$/;" kind:member line:25 +test_222B2 ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^ def test_222B2(self):$/;" kind:member line:21 +test_222B2 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_two.py /^ def test_222B2(self):$/;" kind:member line:21 +test_222B2wow ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^ def test_222B2wow(self):$/;" kind:member line:28 +test_222B2wow ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_two.py /^ def test_222B2wow(self):$/;" kind:member line:28 +test_2_1_1 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_two.py /^ def test_2_1_1(self):$/;" kind:member line:15 +test_A ..\\pythonFiles\\testFiles\\single\\tests\\test_one.py /^ def test_A(self):$/;" kind:member line:7 +test_A ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_one.py /^ def test_A(self):$/;" kind:member line:7 +test_A ..\\pythonFiles\\testFiles\\standard\\tests\\unittest_three_test.py /^ def test_A(self):$/;" kind:member line:5 +test_A ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_unittest_one.py /^ def test_A(self):$/;" kind:member line:7 +test_A ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_one.py /^ def test_A(self):$/;" kind:member line:7 +test_A ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\unittest_three_test.py /^ def test_A(self):$/;" kind:member line:5 +test_A2 ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^ def test_A2(self):$/;" kind:member line:4 +test_A2 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_two.py /^ def test_A2(self):$/;" kind:member line:4 +test_B ..\\pythonFiles\\testFiles\\single\\tests\\test_one.py /^ def test_B(self):$/;" kind:member line:10 +test_B ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_one.py /^ def test_B(self):$/;" kind:member line:10 +test_B ..\\pythonFiles\\testFiles\\standard\\tests\\unittest_three_test.py /^ def test_B(self):$/;" kind:member line:8 +test_B ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_unittest_one.py /^ def test_B(self):$/;" kind:member line:10 +test_B ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_one.py /^ def test_B(self):$/;" kind:member line:10 +test_B ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\unittest_three_test.py /^ def test_B(self):$/;" kind:member line:8 +test_B2 ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^ def test_B2(self):$/;" kind:member line:7 +test_B2 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_two.py /^ def test_B2(self):$/;" kind:member line:7 +test_C2 ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^ def test_C2(self):$/;" kind:member line:10 +test_C2 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_two.py /^ def test_C2(self):$/;" kind:member line:10 +test_D2 ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^ def test_D2(self):$/;" kind:member line:13 +test_D2 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_two.py /^ def test_D2(self):$/;" kind:member line:13 +test_Root_A ..\\pythonFiles\\testFiles\\single\\test_root.py /^ def test_Root_A(self):$/;" kind:member line:7 +test_Root_A ..\\pythonFiles\\testFiles\\standard\\test_root.py /^ def test_Root_A(self):$/;" kind:member line:7 +test_Root_A ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\test_root.py /^ def test_Root_A(self):$/;" kind:member line:7 +test_Root_B ..\\pythonFiles\\testFiles\\single\\test_root.py /^ def test_Root_B(self):$/;" kind:member line:10 +test_Root_B ..\\pythonFiles\\testFiles\\standard\\test_root.py /^ def test_Root_B(self):$/;" kind:member line:10 +test_Root_B ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\test_root.py /^ def test_Root_B(self):$/;" kind:member line:10 +test_Root_c ..\\pythonFiles\\testFiles\\single\\test_root.py /^ def test_Root_c(self):$/;" kind:member line:14 +test_Root_c ..\\pythonFiles\\testFiles\\standard\\test_root.py /^ def test_Root_c(self):$/;" kind:member line:14 +test_Root_c ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\test_root.py /^ def test_Root_c(self):$/;" kind:member line:14 +test_another_pytest.py ..\\pythonFiles\\testFiles\\standard\\tests\\test_another_pytest.py 1;" kind:file line:1 +test_another_pytest.py ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_another_pytest.py 1;" kind:file line:1 +test_c ..\\pythonFiles\\testFiles\\single\\tests\\test_one.py /^ def test_c(self):$/;" kind:member line:14 +test_c ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_one.py /^ def test_c(self):$/;" kind:member line:14 +test_c ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_unittest_one.py /^ def test_c(self):$/;" kind:member line:14 +test_c ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_one.py /^ def test_c(self):$/;" kind:member line:14 +test_complex_check ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^ def test_complex_check(self):$/;" kind:member line:10 +test_complex_check ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_pytest.py /^ def test_complex_check(self):$/;" kind:member line:10 +test_complex_check ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_pytest.py /^ def test_complex_check(self):$/;" kind:member line:10 +test_complex_check2 ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^ def test_complex_check2(self):$/;" kind:member line:24 +test_complex_check2 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_pytest.py /^ def test_complex_check2(self):$/;" kind:member line:24 +test_complex_check2 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_pytest.py /^ def test_complex_check2(self):$/;" kind:member line:24 +test_cwd ..\\pythonFiles\\testFiles\\cwd\\src\\tests\\test_cwd.py /^ def test_cwd(self):$/;" kind:member line:7 +test_cwd.py ..\\pythonFiles\\testFiles\\cwd\\src\\tests\\test_cwd.py 1;" kind:file line:1 +test_d ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^ def test_d(self):$/;" kind:member line:17 +test_d ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_pytest.py /^ def test_d(self):$/;" kind:member line:17 +test_d ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_pytest.py /^ def test_d(self):$/;" kind:member line:17 +test_nested_class_methodB ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^ def test_nested_class_methodB(self):$/;" kind:member line:14 +test_nested_class_methodB ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_pytest.py /^ def test_nested_class_methodB(self):$/;" kind:member line:14 +test_nested_class_methodB ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_pytest.py /^ def test_nested_class_methodB(self):$/;" kind:member line:14 +test_nested_class_methodC ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^ def test_nested_class_methodC(self):$/;" kind:member line:19 +test_nested_class_methodC ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_pytest.py /^ def test_nested_class_methodC(self):$/;" kind:member line:19 +test_nested_class_methodC ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_pytest.py /^ def test_nested_class_methodC(self):$/;" kind:member line:19 +test_one.py ..\\pythonFiles\\testFiles\\single\\tests\\test_one.py 1;" kind:file line:1 +test_parametrized_username ..\\pythonFiles\\testFiles\\standard\\tests\\test_another_pytest.py /^def test_parametrized_username(non_parametrized_username):$/;" kind:function line:16 +test_parametrized_username ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^def test_parametrized_username(non_parametrized_username):$/;" kind:function line:39 +test_parametrized_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_pytest.py /^def test_parametrized_username(non_parametrized_username):$/;" kind:function line:39 +test_parametrized_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_another_pytest.py /^def test_parametrized_username(non_parametrized_username):$/;" kind:function line:16 +test_parametrized_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_pytest.py /^def test_parametrized_username(non_parametrized_username):$/;" kind:function line:39 +test_pytest.py ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py 1;" kind:file line:1 +test_pytest.py ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_pytest.py 1;" kind:file line:1 +test_pytest.py ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_pytest.py 1;" kind:file line:1 +test_root.py ..\\pythonFiles\\testFiles\\single\\test_root.py 1;" kind:file line:1 +test_root.py ..\\pythonFiles\\testFiles\\standard\\test_root.py 1;" kind:file line:1 +test_root.py ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\test_root.py 1;" kind:file line:1 +test_simple_check ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^ def test_simple_check(self):$/;" kind:member line:8 +test_simple_check ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_pytest.py /^ def test_simple_check(self):$/;" kind:member line:8 +test_simple_check ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_pytest.py /^ def test_simple_check(self):$/;" kind:member line:8 +test_simple_check2 ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^ def test_simple_check2(self):$/;" kind:member line:22 +test_simple_check2 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_pytest.py /^ def test_simple_check2(self):$/;" kind:member line:22 +test_simple_check2 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_pytest.py /^ def test_simple_check2(self):$/;" kind:member line:22 +test_unittest_one.py ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_one.py 1;" kind:file line:1 +test_unittest_one.py ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_one.py 1;" kind:file line:1 +test_unittest_one.py ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_unittest_one.py 1;" kind:file line:1 +test_unittest_one.py ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_one.py 1;" kind:file line:1 +test_unittest_two.py ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_two.py 1;" kind:file line:1 +test_unittest_two.py ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py 1;" kind:file line:1 +test_unittest_two.py ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_two.py 1;" kind:file line:1 +test_username ..\\pythonFiles\\testFiles\\standard\\tests\\test_another_pytest.py /^def test_username(parametrized_username):$/;" kind:function line:13 +test_username ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^def test_username(parametrized_username):$/;" kind:function line:36 +test_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\other\\test_pytest.py /^def test_username(parametrized_username):$/;" kind:function line:36 +test_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_another_pytest.py /^def test_username(parametrized_username):$/;" kind:function line:13 +test_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_pytest.py /^def test_username(parametrized_username):$/;" kind:function line:36 +testthis ..\\pythonFiles\\definition\\await.test.py /^async def testthis():$/;" kind:function line:13 +three.py ..\\pythonFiles\\autocomp\\three.py 1;" kind:file line:1 +three.py ..\\pythonFiles\\autoimport\\two\\three.py 1;" kind:file line:1 +three.py ..\\pythonFiles\\definition\\three.py 1;" kind:file line:1 +triangular ..\\pythonFiles\\autocomp\\misc.py /^ def triangular(self, low=0.0, high=1.0, mode=None):$/;" kind:member line:1611 +tryBlocks2.py ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py 1;" kind:file line:1 +tryBlocks4.py ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py 1;" kind:file line:1 +tryBlocksTab.py ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py 1;" kind:file line:1 +ts ..\\pythonFiles\\jupyter\\cells.py /^ts = pd.Series(np.random.randn(1000),$/;" kind:variable line:82 +ts ..\\pythonFiles\\jupyter\\cells.py /^ts = ts.cumsum()$/;" kind:variable line:84 +two ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^ def two():$/;" kind:member line:308 +two ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^def two():$/;" kind:function line:169 +two ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^ def two():$/;" kind:member line:308 +two ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^def two():$/;" kind:function line:169 +two ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^ def two():$/;" kind:member line:308 +two ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^def two():$/;" kind:function line:169 +two ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^def two():$/;" kind:function line:166 +two ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^def two():$/;" kind:function line:177 +two ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^def two():$/;" kind:function line:166 +two ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^def two():$/;" kind:function line:177 +two ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^def two():$/;" kind:function line:166 +two ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^def two():$/;" kind:function line:177 +two.py ..\\pythonFiles\\autocomp\\two.py 1;" kind:file line:1 +two.py ..\\pythonFiles\\definition\\two.py 1;" kind:file line:1 +uniform ..\\pythonFiles\\autocomp\\misc.py /^ def uniform(self, a, b):$/;" kind:member line:1605 +unittest_three_test.py ..\\pythonFiles\\testFiles\\standard\\tests\\unittest_three_test.py 1;" kind:file line:1 +unittest_three_test.py ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\unittest_three_test.py 1;" kind:file line:1 +user_options ..\\pythonFiles\\autocomp\\one.py /^ user_options = []$/;" kind:variable line:12 +user_options ..\\pythonFiles\\definition\\one.py /^ user_options = []$/;" kind:variable line:12 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^var = 100$/;" kind:variable line:1 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^var = 100$/;" kind:variable line:15 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^var = 100$/;" kind:variable line:29 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^var = 100$/;" kind:variable line:339 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^var = 100$/;" kind:variable line:353 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^ var = 100$/;" kind:variable line:339 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^var = 100$/;" kind:variable line:1 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^var = 100$/;" kind:variable line:15 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^var = 100$/;" kind:variable line:29 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^ var = 100$/;" kind:variable line:339 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^var = 100$/;" kind:variable line:1 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^var = 100$/;" kind:variable line:15 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^var = 100$/;" kind:variable line:29 +vonmisesvariate ..\\pythonFiles\\autocomp\\misc.py /^ def vonmisesvariate(self, mu, kappa):$/;" kind:member line:1689 +wait ..\\pythonFiles\\autocomp\\misc.py /^ def wait(self, timeout=None):$/;" kind:member line:309 +wait ..\\pythonFiles\\autocomp\\misc.py /^ def wait(self, timeout=None):$/;" kind:member line:603 +watch ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def watch(self):$/;" kind:member line:234 +weibullvariate ..\\pythonFiles\\autocomp\\misc.py /^ def weibullvariate(self, alpha, beta):$/;" kind:member line:1889 +workspace2File.py ..\\pythonFiles\\symbolFiles\\workspace2File.py 1;" kind:file line:1 +x ..\\pythonFiles\\jupyter\\cells.py /^x = Gaussian(2.0, 1.0)$/;" kind:variable line:131 +x ..\\pythonFiles\\jupyter\\cells.py /^x = np.linspace(0, 20, 100)$/;" kind:variable line:7 +x ..\\pythonFiles\\jupyter\\cells.py /^x = np.linspace(0, 4 * np.pi, 100)$/;" kind:variable line:65 +y ..\\pythonFiles\\jupyter\\cells.py /^y = np.sin(x)$/;" kind:variable line:66 +zero ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^def zero():$/;" kind:function line:110 +zero ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^def zero():$/;" kind:function line:122 +zero ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^def zero():$/;" kind:function line:110 +zero ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^def zero():$/;" kind:function line:122 +zero ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^def zero():$/;" kind:function line:110 +zero ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^def zero():$/;" kind:function line:122 diff --git a/src/test/autocomplete/base.test.ts b/src/test/autocomplete/base.test.ts new file mode 100644 index 000000000000..a5398b7beb43 --- /dev/null +++ b/src/test/autocomplete/base.test.ts @@ -0,0 +1,176 @@ +// Note: This example test is leveraging the Mocha test framework. +// Please refer to their documentation on https://mochajs.org/ for help. + + +// The module 'assert' provides assertion methods from node +import * as assert from 'assert'; +import { EOL } from 'os'; +// You can import and use all API from the 'vscode' module +// as well as import your extension to test it +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as settings from '../../client/common/configSettings'; +import { initialize, closeActiveWindows, initializeTest } from '../initialize'; +import { execPythonFile } from '../../client/common/utils'; +import { PythonSettings } from '../../client/common/configSettings'; +import { rootWorkspaceUri } from '../common'; + +const autoCompPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'autocomp'); +const fileOne = path.join(autoCompPath, 'one.py'); +const fileImport = path.join(autoCompPath, 'imp.py'); +const fileDoc = path.join(autoCompPath, 'doc.py'); +const fileLambda = path.join(autoCompPath, 'lamb.py'); +const fileDecorator = path.join(autoCompPath, 'deco.py'); +const fileEncoding = path.join(autoCompPath, 'four.py'); +const fileEncodingUsed = path.join(autoCompPath, 'five.py'); + +suite('Autocomplete', () => { + let isPython3: Promise; + suiteSetup(async () => { + await initialize(); + const version = await execPythonFile(rootWorkspaceUri, PythonSettings.getInstance(rootWorkspaceUri).pythonPath, ['--version'], __dirname, true); + isPython3 = Promise.resolve(version.indexOf('3.') >= 0); + }); + setup(() => initializeTest()); + suiteTeardown(() => closeActiveWindows()); + teardown(() => closeActiveWindows()); + + test('For "sys."', done => { + let textEditor: vscode.TextEditor; + let textDocument: vscode.TextDocument; + vscode.workspace.openTextDocument(fileOne).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + textEditor = editor; + const position = new vscode.Position(3, 10); + return vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + }).then(list => { + assert.equal(list.items.filter(item => item.label === 'api_version').length, 1, 'api_version not found'); + }).then(done, done); + }); + + // https://github.com/DonJayamanne/pythonVSCode/issues/975 + test('For "import *"', async () => { + const textDocument = await vscode.workspace.openTextDocument(fileImport); + await vscode.window.showTextDocument(textDocument); + const position = new vscode.Position(1, 4); + const list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + assert.equal(list.items.filter(item => item.label === 'fstat').length, 1, 'fstat not found'); + }); + + // https://github.com/DonJayamanne/pythonVSCode/issues/898 + test('For "f.readlines()"', async () => { + const textDocument = await vscode.workspace.openTextDocument(fileDoc); + await vscode.window.showTextDocument(textDocument); + const position = new vscode.Position(5, 27); + const list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + // These are not known to work, jedi issue + // assert.equal(list.items.filter(item => item.label === 'capitalize').length, 1, 'capitalize not found (known not to work, Jedi issue)'); + // assert.notEqual(list.items.filter(item => item.label === 'upper').length, 1, 'upper not found'); + // assert.notEqual(list.items.filter(item => item.label === 'lower').length, 1, 'lower not found'); + }); + + // https://github.com/DonJayamanne/pythonVSCode/issues/265 + test('For "lambda"', async () => { + if (!await isPython3) { + return; + } + const textDocument = await vscode.workspace.openTextDocument(fileLambda); + await vscode.window.showTextDocument(textDocument); + const position = new vscode.Position(1, 19); + const list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + assert.notEqual(list.items.filter(item => item.label === 'append').length, 0, 'append not found'); + assert.notEqual(list.items.filter(item => item.label === 'clear').length, 0, 'clear not found'); + assert.notEqual(list.items.filter(item => item.label === 'count').length, 0, 'cound not found'); + }); + + // https://github.com/DonJayamanne/pythonVSCode/issues/630 + test('For "abc.decorators"', async () => { + const textDocument = await vscode.workspace.openTextDocument(fileDecorator); + await vscode.window.showTextDocument(textDocument); + let position = new vscode.Position(3, 9); + let list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + assert.notEqual(list.items.filter(item => item.label === 'ABCMeta').length, 0, 'ABCMeta not found'); + assert.notEqual(list.items.filter(item => item.label === 'abstractmethod').length, 0, 'abstractmethod not found'); + + position = new vscode.Position(4, 9); + list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + assert.notEqual(list.items.filter(item => item.label === 'ABCMeta').length, 0, 'ABCMeta not found'); + assert.notEqual(list.items.filter(item => item.label === 'abstractmethod').length, 0, 'abstractmethod not found'); + + position = new vscode.Position(2, 30); + list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + assert.notEqual(list.items.filter(item => item.label === 'ABCMeta').length, 0, 'ABCMeta not found'); + assert.notEqual(list.items.filter(item => item.label === 'abstractmethod').length, 0, 'abstractmethod not found'); + }); + + // https://github.com/DonJayamanne/pythonVSCode/issues/727 + // https://github.com/DonJayamanne/pythonVSCode/issues/746 + // https://github.com/davidhalter/jedi/issues/859 + test('For "time.slee"', async () => { + const textDocument = await vscode.workspace.openTextDocument(fileDoc); + await vscode.window.showTextDocument(textDocument); + const position = new vscode.Position(10, 9); + const list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + assert.notEqual(list.items.filter(item => item.label === 'sleep').length, 0, 'sleep not found'); + assert.notEqual(list.items.filter(item => item.documentation.toString().startsWith("Delay execution for a given number of seconds. The argument may be")).length, 0, 'Documentation incorrect'); + }); + + test('For custom class', done => { + let textEditor: vscode.TextEditor; + let textDocument: vscode.TextDocument; + vscode.workspace.openTextDocument(fileOne).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + textEditor = editor; + const position = new vscode.Position(30, 4); + return vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + }).then(list => { + assert.notEqual(list.items.filter(item => item.label === 'method1').length, 0, 'method1 not found'); + assert.notEqual(list.items.filter(item => item.label === 'method2').length, 0, 'method2 not found'); + }).then(done, done); + }); + + test('With Unicode Characters', done => { + let textEditor: vscode.TextEditor; + let textDocument: vscode.TextDocument; + vscode.workspace.openTextDocument(fileEncoding).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + textEditor = editor; + const position = new vscode.Position(25, 4); + return vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + }).then(list => { + assert.equal(list.items.filter(item => item.label === 'bar').length, 1, 'bar not found'); + const documentation = `่ฏดๆ˜Ž - keep this line, it works${EOL}delete following line, it works${EOL}ๅฆ‚ๆžœๅญ˜ๅœจ้œ€่ฆ็ญ‰ๅพ…ๅฎกๆ‰นๆˆ–ๆญฃๅœจๆ‰ง่กŒ็š„ไปปๅŠก๏ผŒๅฐ†ไธๅˆทๆ–ฐ้กต้ข`; + assert.equal(list.items.filter(item => item.label === 'bar')[0].documentation, documentation, 'unicode documentation is incorrect'); + }).then(done, done); + }); + + test('Across files With Unicode Characters', done => { + let textEditor: vscode.TextEditor; + let textDocument: vscode.TextDocument; + vscode.workspace.openTextDocument(fileEncodingUsed).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + textEditor = editor; + const position = new vscode.Position(1, 5); + return vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + }).then(list => { + assert.equal(list.items.filter(item => item.label === 'Foo').length, 1, 'Foo not found'); + assert.equal(list.items.filter(item => item.label === 'Foo')[0].documentation, '่ฏดๆ˜Ž', 'Foo unicode documentation is incorrect'); + + assert.equal(list.items.filter(item => item.label === 'showMessage').length, 1, 'showMessage not found'); + const documentation = `ะšัŽะผ ัƒั‚ ะถัะผะฟัั€ ะฟะพัˆะถะธะผ ะปัŒะฐะฑะพั€ัะถ, ะบะพะผะผัŽะฝั‹ ัะฝั‚ัั€ััั‰ัั‚ ะฝะฐะผ ะตะด, ะดะตะบั‚ะฐ ะธะณะฝะพั‚ะฐ ะฝั‹ะผะพั€ั ะถัั‚ ัะธ. ${EOL}ะจัะฐ ะดะตะบะฐะผ ัะบัˆั‹ั€ะบะธ ัะธ, ัะธ ะทั‹ะด ัั€ั€ัะผ ะดะพะบัะฝะดั‘, ะฒะตะบะถ ั„ะฐะบัั‚ั ะฟัั€ั‡ั‹ะบะฒัŽัั€ั‘ะถ ะบัƒ.`; + assert.equal(list.items.filter(item => item.label === 'showMessage')[0].documentation, documentation, 'showMessage unicode documentation is incorrect'); + }).then(done, done); + }); +}); diff --git a/src/test/autocomplete/pep484.test.ts b/src/test/autocomplete/pep484.test.ts new file mode 100644 index 000000000000..c0c327bdf7d1 --- /dev/null +++ b/src/test/autocomplete/pep484.test.ts @@ -0,0 +1,58 @@ + +// Note: This example test is leveraging the Mocha test framework. +// Please refer to their documentation on https://mochajs.org/ for help. + + +// The module 'assert' provides assertion methods from node +import * as assert from 'assert'; +// You can import and use all API from the 'vscode' module +// as well as import your extension to test it +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as settings from '../../client/common/configSettings'; +import { execPythonFile } from '../../client/common/utils'; +import { initialize, closeActiveWindows, initializeTest } from '../initialize'; +import { PythonSettings } from '../../client/common/configSettings'; +import { rootWorkspaceUri } from '../common'; + +const autoCompPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'autocomp'); +const filePep484 = path.join(autoCompPath, 'pep484.py'); + +suite('Autocomplete PEP 484', () => { + let isPython3: Promise; + suiteSetup(async () => { + await initialize(); + const version = await execPythonFile(rootWorkspaceUri, PythonSettings.getInstance(rootWorkspaceUri).pythonPath, ['--version'], __dirname, true); + isPython3 = Promise.resolve(version.indexOf('3.') >= 0); + }); + setup(() => initializeTest()); + suiteTeardown(() => closeActiveWindows()); + teardown(() => closeActiveWindows()); + + test('argument', async () => { + if (!await isPython3) { + return; + } + let textDocument = await vscode.workspace.openTextDocument(filePep484); + await vscode.window.showTextDocument(textDocument); + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(2, 27); + let list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + assert.notEqual(list.items.filter(item => item.label === 'capitalize').length, 0, 'capitalize not found'); + assert.notEqual(list.items.filter(item => item.label === 'upper').length, 0, 'upper not found'); + assert.notEqual(list.items.filter(item => item.label === 'lower').length, 0, 'lower not found'); + }); + + test('return value', async () => { + if (!await isPython3) { + return; + } + let textDocument = await vscode.workspace.openTextDocument(filePep484); + await vscode.window.showTextDocument(textDocument); + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(8, 6); + let list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + assert.notEqual(list.items.filter(item => item.label === 'bit_length').length, 0, 'bit_length not found'); + assert.notEqual(list.items.filter(item => item.label === 'from_bytes').length, 0, 'from_bytes not found'); + }); +}); diff --git a/src/test/autocomplete/pep526.test.ts b/src/test/autocomplete/pep526.test.ts new file mode 100644 index 000000000000..01dc988c000f --- /dev/null +++ b/src/test/autocomplete/pep526.test.ts @@ -0,0 +1,101 @@ + +// Note: This example test is leveraging the Mocha test framework. +// Please refer to their documentation on https://mochajs.org/ for help. + + +// The module 'assert' provides assertion methods from node +import * as assert from 'assert'; +// You can import and use all API from the 'vscode' module +// as well as import your extension to test it +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as settings from '../../client/common/configSettings'; +import { execPythonFile } from '../../client/common/utils'; +import { initialize, closeActiveWindows, initializeTest } from '../initialize'; +import { PythonSettings } from '../../client/common/configSettings'; +import { rootWorkspaceUri } from '../common'; + +const autoCompPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'autocomp'); +const filePep526 = path.join(autoCompPath, 'pep526.py'); + +suite('Autocomplete PEP 526', () => { + let isPython3: Promise; + suiteSetup(async () => { + await initialize(); + const version = await execPythonFile(rootWorkspaceUri, PythonSettings.getInstance(rootWorkspaceUri).pythonPath, ['--version'], __dirname, true); + isPython3 = Promise.resolve(version.indexOf('3.') >= 0); + }); + setup(() => initializeTest()); + suiteTeardown(() => closeActiveWindows()); + teardown(() => closeActiveWindows()); + + test('variable (abc:str)', async () => { + if (!await isPython3) { + return; + } + let textDocument = await vscode.workspace.openTextDocument(filePep526); + await vscode.window.showTextDocument(textDocument); + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(9, 8); + let list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + assert.notEqual(list.items.filter(item => item.label === 'capitalize').length, 0, 'capitalize not found'); + assert.notEqual(list.items.filter(item => item.label === 'upper').length, 0, 'upper not found'); + assert.notEqual(list.items.filter(item => item.label === 'lower').length, 0, 'lower not found'); + }); + + test('variable (abc: str = "")', async () => { + if (!await isPython3) { + return; + } + let textDocument = await vscode.workspace.openTextDocument(filePep526); + await vscode.window.showTextDocument(textDocument); + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(8, 14); + let list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + assert.notEqual(list.items.filter(item => item.label === 'capitalize').length, 0, 'capitalize not found'); + assert.notEqual(list.items.filter(item => item.label === 'upper').length, 0, 'upper not found'); + assert.notEqual(list.items.filter(item => item.label === 'lower').length, 0, 'lower not found'); + }); + + test('variable (abc = UNKNOWN # type: str)', async () => { + if (!await isPython3) { + return; + } + let textDocument = await vscode.workspace.openTextDocument(filePep526); + await vscode.window.showTextDocument(textDocument); + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(7, 14); + let list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + assert.notEqual(list.items.filter(item => item.label === 'capitalize').length, 0, 'capitalize not found'); + assert.notEqual(list.items.filter(item => item.label === 'upper').length, 0, 'upper not found'); + assert.notEqual(list.items.filter(item => item.label === 'lower').length, 0, 'lower not found'); + }); + + test('class methods', async () => { + if (!await isPython3) { + return; + } + let textDocument = await vscode.workspace.openTextDocument(filePep526); + await vscode.window.showTextDocument(textDocument); + assert(vscode.window.activeTextEditor, 'No active editor'); + let position = new vscode.Position(20, 4); + let list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + assert.notEqual(list.items.filter(item => item.label === 'a').length, 0, 'method a not found'); + + position = new vscode.Position(21, 4); + list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + assert.notEqual(list.items.filter(item => item.label === 'b').length, 0, 'method b not found'); + }); + + test('class method types', async () => { + if (!await isPython3) { + return; + } + let textDocument = await vscode.workspace.openTextDocument(filePep526); + await vscode.window.showTextDocument(textDocument); + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(21, 6); + const list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + assert.notEqual(list.items.filter(item => item.label === 'bit_length').length, 0, 'bit_length not found'); + }); +}); diff --git a/src/test/common.ts b/src/test/common.ts new file mode 100644 index 000000000000..fe8bc2b35547 --- /dev/null +++ b/src/test/common.ts @@ -0,0 +1,112 @@ +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { ConfigurationTarget, Uri, workspace } from 'vscode'; +import { PythonSettings } from '../client/common/configSettings'; +import { IS_MULTI_ROOT_TEST } from './initialize'; + +const fileInNonRootWorkspace = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'dummy.py'); +export const rootWorkspaceUri = getWorkspaceRoot(); + +export type PythonSettingKeys = 'workspaceSymbols.enabled' | 'pythonPath' | + 'linting.lintOnSave' | 'linting.lintOnTextChange' | + 'linting.enabled' | 'linting.pylintEnabled' | + 'linting.flake8Enabled' | 'linting.pep8Enabled' | 'linting.pylamaEnabled' | + 'linting.prospectorEnabled' | 'linting.pydocstyleEnabled' | 'linting.mypyEnabled' | + 'unitTest.nosetestArgs' | 'unitTest.pyTestArgs' | 'unitTest.unittestArgs' | + 'formatting.formatOnSave' | 'formatting.provider' | 'sortImports.args' | + 'unitTest.nosetestsEnabled' | 'unitTest.pyTestEnabled' | 'unitTest.unittestEnabled'; + +export async function updateSetting(setting: PythonSettingKeys, value: {}, resource: Uri, configTarget: ConfigurationTarget) { + const settings = workspace.getConfiguration('python', resource); + const currentValue = settings.inspect(setting); + if (currentValue !== undefined && ((configTarget === ConfigurationTarget.Global && currentValue.globalValue === value) || + (configTarget === ConfigurationTarget.Workspace && currentValue.workspaceValue === value) || + (configTarget === ConfigurationTarget.WorkspaceFolder && currentValue.workspaceFolderValue === value))) { + PythonSettings.dispose(); + return; + } + // tslint:disable-next-line:await-promise + await settings.update(setting, value, configTarget); + PythonSettings.dispose(); +} + +function getWorkspaceRoot() { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + return Uri.file(path.join(__dirname, '..', '..', 'src', 'test')); + } + if (workspace.workspaceFolders.length === 1) { + return workspace.workspaceFolders[0].uri; + } + const workspaceFolder = workspace.getWorkspaceFolder(Uri.file(fileInNonRootWorkspace)); + return workspaceFolder ? workspaceFolder.uri : workspace.workspaceFolders[0].uri; +} + +// tslint:disable-next-line:no-any +export function retryAsync(wrapped: Function, retryCount: number = 2) { + // tslint:disable-next-line:no-any + return async (...args: any[]) => { + return new Promise((resolve, reject) => { + // tslint:disable-next-line:no-any + const reasons: any[] = []; + + const makeCall = () => { + // tslint:disable-next-line:no-unsafe-any no-any + // tslint:disable-next-line:no-invalid-this + wrapped.call(this, ...args) + // tslint:disable-next-line:no-unsafe-any no-any + .then(resolve, (reason: any) => { + reasons.push(reason); + if (reasons.length >= retryCount) { + reject(reasons); + } else { + // If failed once, lets wait for some time before trying again. + // tslint:disable-next-line:no-string-based-set-timeout + setTimeout(makeCall, 500); + } + }); + }; + + makeCall(); + }); + }; +} + +async function setPythonPathInWorkspace(resource: string | Uri | undefined, config: ConfigurationTarget, pythonPath?: string) { + if (config === ConfigurationTarget.WorkspaceFolder && !IS_MULTI_ROOT_TEST) { + return; + } + const resourceUri = typeof resource === 'string' ? Uri.file(resource) : resource; + const settings = workspace.getConfiguration('python', resourceUri); + const value = settings.inspect('pythonPath'); + const prop: 'workspaceFolderValue' | 'workspaceValue' = config === ConfigurationTarget.Workspace ? 'workspaceValue' : 'workspaceFolderValue'; + if (value && value[prop] !== pythonPath) { + await settings.update('pythonPath', pythonPath, config); + PythonSettings.dispose(); + } +} +async function restoreGlobalPythonPathSetting(): Promise { + const pythonConfig = workspace.getConfiguration('python'); + const currentGlobalPythonPathSetting = pythonConfig.inspect('pythonPath').globalValue; + if (globalPythonPathSetting !== currentGlobalPythonPathSetting) { + await pythonConfig.update('pythonPath', undefined, true); + } + PythonSettings.dispose(); +} + +export async function deleteDirectory(dir: string) { + const exists = await fs.pathExists(dir); + if (exists) { + await fs.remove(dir); + } +} +export async function deleteFile(file: string) { + const exists = await fs.pathExists(file); + if (exists) { + await fs.remove(file); + } +} + +const globalPythonPathSetting = workspace.getConfiguration('python').inspect('pythonPath').globalValue; +export const clearPythonPathInWorkspaceFolder = async (resource: string | Uri) => retryAsync(setPythonPathInWorkspace)(resource, ConfigurationTarget.WorkspaceFolder); +export const setPythonPathInWorkspaceRoot = async (pythonPath: string) => retryAsync(setPythonPathInWorkspace)(undefined, ConfigurationTarget.Workspace, pythonPath); +export const resetGlobalPythonPathSetting = async () => retryAsync(restoreGlobalPythonPathSetting)(); diff --git a/src/test/common/common.test.ts b/src/test/common/common.test.ts new file mode 100644 index 000000000000..aeccb54166da --- /dev/null +++ b/src/test/common/common.test.ts @@ -0,0 +1,90 @@ +import * as assert from 'assert'; +import { EOL } from 'os'; +import * as vscode from 'vscode'; +import { createDeferred } from '../../client/common/helpers'; +import { execPythonFile, getInterpreterVersion } from '../../client/common/utils'; +import { initialize } from './../initialize'; + +// Defines a Mocha test suite to group tests of similar kind together +suite('ChildProc', () => { + setup(initialize); + teardown(initialize); + test('Standard Response', done => { + execPythonFile(undefined, 'python', ['-c', 'print(1)'], __dirname, false).then(data => { + assert.ok(data === `1${EOL}`); + }).then(done).catch(done); + }); + test('Error Response', done => { + // tslint:disable-next-line:no-any + const def = createDeferred(); + execPythonFile(undefined, 'python', ['-c', 'print(1'], __dirname, false).then(() => { + def.reject('Should have failed'); + }).catch(() => { + def.resolve(); + }); + + def.promise.then(done).catch(done); + }); + + test('Stream Stdout', done => { + const output: string[] = []; + function handleOutput(data: string) { + output.push(data); + } + execPythonFile(undefined, 'python', ['-c', 'print(1)'], __dirname, false, handleOutput).then(() => { + assert.equal(output.length, 1, 'Ouput length incorrect'); + assert.equal(output[0], `1${EOL}`, 'Ouput value incorrect'); + }).then(done).catch(done); + }); + + test('Stream Stdout (Unicode)', async () => { + const output: string[] = []; + function handleOutput(data: string) { + output.push(data); + } + await execPythonFile(undefined, 'python', ['-c', 'print(\'รถรค\')'], __dirname, false, handleOutput); + assert.equal(output.length, 1, 'Ouput length incorrect'); + assert.equal(output[0], `รถรค${EOL}`, 'Ouput value incorrect'); + }); + + test('Stream Stdout with Threads', function (done) { + // tslint:disable-next-line:no-invalid-this + this.timeout(6000); + const output: string[] = []; + function handleOutput(data: string) { + output.push(data); + } + execPythonFile(undefined, 'python', ['-c', 'import sys\nprint(1)\nsys.__stdout__.flush()\nimport time\ntime.sleep(5)\nprint(2)'], __dirname, false, handleOutput).then(() => { + assert.equal(output.length, 2, 'Ouput length incorrect'); + assert.equal(output[0], `1${EOL}`, 'First Ouput value incorrect'); + assert.equal(output[1], `2${EOL}`, 'Second Ouput value incorrect'); + }).then(done).catch(done); + }); + + test('Kill', done => { + // tslint:disable-next-line:no-any + const def = createDeferred(); + const output: string[] = []; + function handleOutput(data: string) { + output.push(data); + } + const cancellation = new vscode.CancellationTokenSource(); + execPythonFile(undefined, 'python', ['-c', 'import sys\nprint(1)\nsys.__stdout__.flush()\nimport time\ntime.sleep(5)\nprint(2)'], __dirname, false, handleOutput, cancellation.token).then(() => { + def.reject('Should not have completed'); + }).catch(() => { + def.resolve(); + }); + + setTimeout(() => { + cancellation.cancel(); + }, 1000); + + def.promise.then(done).catch(done); + }); + + test('Get Python display name', async () => { + const displayName = await getInterpreterVersion('python'); + assert.equal(typeof displayName, 'string', 'Display name not returned'); + assert.notEqual(displayName.length, 0, 'Display name cannot be empty'); + }); +}); diff --git a/src/test/common/configSettings.multiroot.test.ts b/src/test/common/configSettings.multiroot.test.ts new file mode 100644 index 000000000000..dc7ef9590885 --- /dev/null +++ b/src/test/common/configSettings.multiroot.test.ts @@ -0,0 +1,184 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import { ConfigurationTarget, Uri, workspace } from 'vscode'; +import { PythonSettings } from '../../client/common/configSettings'; +import { clearPythonPathInWorkspaceFolder } from '../common'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; + +const multirootPath = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRootWkspc'); + +// tslint:disable-next-line:max-func-body-length +suite('Multiroot Config Settings', () => { + suiteSetup(async function () { + if (!IS_MULTI_ROOT_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await clearPythonPathInWorkspaceFolder(Uri.file(path.join(multirootPath, 'workspace1'))); + await initialize(); + }); + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(async () => { + await closeActiveWindows(); + await clearPythonPathInWorkspaceFolder(Uri.file(path.join(multirootPath, 'workspace1'))); + await initializeTest(); + }); + + async function enableDisableLinterSetting(resource: Uri, configTarget: ConfigurationTarget, setting: string, enabled: boolean | undefined): Promise { + const settings = workspace.getConfiguration('python.linting', resource); + const cfgValue = settings.inspect(setting); + if (configTarget === ConfigurationTarget.Workspace && cfgValue && cfgValue.workspaceValue === enabled) { + return; + } + if (configTarget === ConfigurationTarget.WorkspaceFolder && cfgValue && cfgValue.workspaceFolderValue === enabled) { + return; + } + await settings.update(setting, enabled, configTarget); + PythonSettings.dispose(); + } + + test('Workspace folder should inherit Python Path from workspace root', async () => { + const workspaceUri = Uri.file(path.join(multirootPath, 'workspace1')); + let settings = workspace.getConfiguration('python', workspaceUri); + const pythonPath = `x${new Date().getTime()}`; + await settings.update('pythonPath', pythonPath, ConfigurationTarget.Workspace); + const value = settings.inspect('pythonPath'); + if (value && typeof value.workspaceFolderValue === 'string') { + await settings.update('pythonPath', undefined, ConfigurationTarget.WorkspaceFolder); + } + settings = workspace.getConfiguration('python', workspaceUri); + PythonSettings.dispose(); + const cfgSetting = PythonSettings.getInstance(workspaceUri); + assert.equal(cfgSetting.pythonPath, pythonPath, 'Python Path not inherited from workspace'); + }); + + test('Workspace folder should not inherit Python Path from workspace root', async () => { + const workspaceUri = Uri.file(path.join(multirootPath, 'workspace1')); + const settings = workspace.getConfiguration('python', workspaceUri); + const pythonPath = `x${new Date().getTime()}`; + await settings.update('pythonPath', pythonPath, ConfigurationTarget.Workspace); + const privatePythonPath = `x${new Date().getTime()}`; + await settings.update('pythonPath', privatePythonPath, ConfigurationTarget.WorkspaceFolder); + + const cfgSetting = PythonSettings.getInstance(workspaceUri); + assert.equal(cfgSetting.pythonPath, privatePythonPath, 'Python Path for workspace folder is incorrect'); + }); + + test('Workspace folder should inherit Python Path from workspace root when opening a document', async () => { + const workspaceUri = Uri.file(path.join(multirootPath, 'workspace1')); + const fileToOpen = path.join(multirootPath, 'workspace1', 'file.py'); + + const settings = workspace.getConfiguration('python', workspaceUri); + const pythonPath = `x${new Date().getTime()}`; + await settings.update('pythonPath', pythonPath, ConfigurationTarget.Workspace); + // Update workspace folder to something else so it gets refreshed. + await settings.update('pythonPath', `x${new Date().getTime()}`, ConfigurationTarget.WorkspaceFolder); + await settings.update('pythonPath', undefined, ConfigurationTarget.WorkspaceFolder); + + const document = await workspace.openTextDocument(fileToOpen); + const cfg = PythonSettings.getInstance(document.uri); + assert.equal(cfg.pythonPath, pythonPath, 'Python Path not inherited from workspace'); + }); + + test('Workspace folder should not inherit Python Path from workspace root when opening a document', async () => { + const workspaceUri = Uri.file(path.join(multirootPath, 'workspace1')); + const fileToOpen = path.join(multirootPath, 'workspace1', 'file.py'); + + const settings = workspace.getConfiguration('python', workspaceUri); + const pythonPath = `x${new Date().getTime()}`; + await settings.update('pythonPath', pythonPath, ConfigurationTarget.Workspace); + const privatePythonPath = `x${new Date().getTime()}`; + await settings.update('pythonPath', privatePythonPath, ConfigurationTarget.WorkspaceFolder); + + const document = await workspace.openTextDocument(fileToOpen); + const cfg = PythonSettings.getInstance(document.uri); + assert.equal(cfg.pythonPath, privatePythonPath, 'Python Path for workspace folder is incorrect'); + }); + + test('Enabling/Disabling Pylint in root should be reflected in config settings', async () => { + const workspaceUri = Uri.file(path.join(multirootPath, 'workspace1')); + await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', undefined); + await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', true); + let settings = PythonSettings.getInstance(workspaceUri); + assert.equal(settings.linting.pylintEnabled, true, 'Pylint not enabled when it should be'); + + await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', false); + settings = PythonSettings.getInstance(workspaceUri); + assert.equal(settings.linting.pylintEnabled, false, 'Pylint enabled when it should not be'); + }); + + test('Enabling/Disabling Pylint in root and workspace should be reflected in config settings', async () => { + const workspaceUri = Uri.file(path.join(multirootPath, 'workspace1')); + + await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', false); + await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', true); + + let cfgSetting = PythonSettings.getInstance(workspaceUri); + assert.equal(cfgSetting.linting.pylintEnabled, false, 'Workspace folder pylint setting is true when it should not be'); + PythonSettings.dispose(); + + await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', true); + await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', false); + + cfgSetting = PythonSettings.getInstance(workspaceUri); + assert.equal(cfgSetting.linting.pylintEnabled, true, 'Workspace folder pylint setting is false when it should not be'); + }); + + test('Enabling/Disabling Pylint in root should be reflected in config settings when opening a document', async () => { + const workspaceUri = Uri.file(path.join(multirootPath, 'workspace1')); + const fileToOpen = path.join(multirootPath, 'workspace1', 'file.py'); + + await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', false); + await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', true); + let document = await workspace.openTextDocument(fileToOpen); + let cfg = PythonSettings.getInstance(document.uri); + assert.equal(cfg.linting.pylintEnabled, true, 'Pylint should be enabled in workspace'); + PythonSettings.dispose(); + + await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', true); + await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', false); + document = await workspace.openTextDocument(fileToOpen); + cfg = PythonSettings.getInstance(document.uri); + assert.equal(cfg.linting.pylintEnabled, false, 'Pylint should not be enabled in workspace'); + }); + + test('Enabling/Disabling Pylint in root should be reflected in config settings when opening a document', async () => { + const workspaceUri = Uri.file(path.join(multirootPath, 'workspace1')); + const fileToOpen = path.join(multirootPath, 'workspace1', 'file.py'); + + await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', false); + await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', true); + let document = await workspace.openTextDocument(fileToOpen); + let cfg = PythonSettings.getInstance(document.uri); + assert.equal(cfg.linting.pylintEnabled, true, 'Pylint should be enabled in workspace'); + PythonSettings.dispose(); + + await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', true); + await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', false); + document = await workspace.openTextDocument(fileToOpen); + cfg = PythonSettings.getInstance(document.uri); + assert.equal(cfg.linting.pylintEnabled, false, 'Pylint should not be enabled in workspace'); + }); + + // tslint:disable-next-line:no-invalid-template-strings + test('${workspaceRoot} variable in settings should be replaced with the right value', async () => { + const workspace2Uri = Uri.file(path.join(multirootPath, 'workspace2')); + let fileToOpen = path.join(workspace2Uri.fsPath, 'file.py'); + + let document = await workspace.openTextDocument(fileToOpen); + let cfg = PythonSettings.getInstance(document.uri); + assert.equal(path.dirname(cfg.workspaceSymbols.tagFilePath), workspace2Uri.fsPath, 'ctags file path for workspace2 is incorrect'); + assert.equal(path.basename(cfg.workspaceSymbols.tagFilePath), 'workspace2.tags.file', 'ctags file name for workspace2 is incorrect'); + PythonSettings.dispose(); + + const workspace3Uri = Uri.file(path.join(multirootPath, 'workspace3')); + fileToOpen = path.join(workspace3Uri.fsPath, 'file.py'); + + document = await workspace.openTextDocument(fileToOpen); + cfg = PythonSettings.getInstance(document.uri); + assert.equal(path.dirname(cfg.workspaceSymbols.tagFilePath), workspace3Uri.fsPath, 'ctags file path for workspace3 is incorrect'); + assert.equal(path.basename(cfg.workspaceSymbols.tagFilePath), 'workspace3.tags.file', 'ctags file name for workspace3 is incorrect'); + PythonSettings.dispose(); + }); +}); diff --git a/src/test/extension.common.configSettings.test.ts b/src/test/common/configSettings.test.ts similarity index 60% rename from src/test/extension.common.configSettings.test.ts rename to src/test/common/configSettings.test.ts index 82ba49349bd7..616c29d5afab 100644 --- a/src/test/extension.common.configSettings.test.ts +++ b/src/test/common/configSettings.test.ts @@ -3,40 +3,41 @@ // Please refer to their documentation on https://mochajs.org/ for help. // -// Place this right on top -import { initialize, IS_TRAVIS } from './initialize'; // The module 'assert' provides assertion methods from node import * as assert from 'assert'; // You can import and use all API from the 'vscode' module // as well as import your extension to test it import * as vscode from 'vscode'; -import { PythonSettings } from '../client/common/configSettings'; -import { SystemVariables } from '../client/common/systemVariables'; +import * as path from 'path'; +import { initialize, IS_MULTI_ROOT_TEST, IS_TRAVIS } from './../initialize'; +import { PythonSettings } from '../../client/common/configSettings'; +import { SystemVariables } from '../../client/common/systemVariables'; +import { rootWorkspaceUri } from '../common'; -const pythonSettings = PythonSettings.getInstance(); +const workspaceRoot = path.join(__dirname, '..', '..', '..', 'src', 'test'); // Defines a Mocha test suite to group tests of similar kind together suite('Configuration Settings', () => { - setup(done => { - initialize().then(() => done(), done); - }); - if (!IS_TRAVIS) { + setup(() => initialize()); + + if (!IS_MULTI_ROOT_TEST) { test('Check Values', done => { - const systemVariables: SystemVariables = new SystemVariables(); + const systemVariables: SystemVariables = new SystemVariables(workspaceRoot); const pythonConfig = vscode.workspace.getConfiguration('python'); + const pythonSettings = PythonSettings.getInstance(vscode.Uri.file(workspaceRoot)); Object.keys(pythonSettings).forEach(key => { let settingValue = pythonConfig.get(key, 'Not a config'); if (settingValue === 'Not a config') { return; } - if (typeof settingValue === 'object' && settingValue !== null) { + if (settingValue) { settingValue = systemVariables.resolve(settingValue); } - assert.deepEqual(settingValue, pythonSettings[key], `Setting ${key} not the same`); + assert.deepEqual(settingValue, (pythonSettings as any)[key], `Setting ${key} not the same`); }); done(); }); } -}); \ No newline at end of file +}); diff --git a/src/test/extension.common.helpers.test.ts b/src/test/common/helpers.test.ts similarity index 95% rename from src/test/extension.common.helpers.test.ts rename to src/test/common/helpers.test.ts index e0bcc5771367..c1711a2adef1 100644 --- a/src/test/extension.common.helpers.test.ts +++ b/src/test/common/helpers.test.ts @@ -4,13 +4,13 @@ // // Place this right on top -import { initialize } from './initialize'; +import { initialize } from './../initialize'; // The module 'assert' provides assertion methods from node import * as assert from 'assert'; // You can import and use all API from the 'vscode' module // as well as import your extension to test it -import { createDeferred, isNotInstalledError } from '../client/common/helpers'; +import { createDeferred, isNotInstalledError } from '../../client/common/helpers'; // Defines a Mocha test suite to group tests of similar kind together suite('Deferred', () => { @@ -65,4 +65,4 @@ suite('Deferred', () => { done(); }); -}); \ No newline at end of file +}); diff --git a/src/test/extension.common.idDispenser.test.ts b/src/test/common/idDispenser.test.ts similarity index 93% rename from src/test/extension.common.idDispenser.test.ts rename to src/test/common/idDispenser.test.ts index 65cf76e4b8ec..7fa6b001928d 100644 --- a/src/test/extension.common.idDispenser.test.ts +++ b/src/test/common/idDispenser.test.ts @@ -4,13 +4,13 @@ // // Place this right on top -import { initialize } from './initialize'; +import { initialize } from './../initialize'; // The module 'assert' provides assertion methods from node import * as assert from 'assert'; // You can import and use all API from the 'vscode' module // as well as import your extension to test it -import { IdDispenser } from '../client/common/idDispenser'; +import { IdDispenser } from '../../client/common/idDispenser'; // Defines a Mocha test suite to group tests of similar kind together suite('IdDispenser', () => { @@ -47,4 +47,4 @@ suite('IdDispenser', () => { done(); }); -}); \ No newline at end of file +}); diff --git a/src/test/common/installer.test.ts b/src/test/common/installer.test.ts new file mode 100644 index 000000000000..786ae7bb278e --- /dev/null +++ b/src/test/common/installer.test.ts @@ -0,0 +1,66 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { EnumEx } from '../../client/common/enumUtils'; +import { Installer, Product } from '../../client/common/installer'; +import { closeActiveWindows, initializeTest, IS_MULTI_ROOT_TEST, IS_TRAVIS } from './../initialize'; +import { MockOutputChannel } from './../mockClasses'; + +// TODO: Need to mock the command runner, to check what commands are being sent. +// Instead of altering the environment. + +suite('Installer', () => { + let outputChannel: MockOutputChannel; + let installer: Installer; + const workspaceUri = Uri.file(path.join(__dirname, '..', '..', '..', 'src', 'test')); + const resource = IS_MULTI_ROOT_TEST ? workspaceUri : undefined; + suiteSetup(async () => { + outputChannel = new MockOutputChannel('Installer'); + installer = new Installer(outputChannel); + await initializeTest(); + }); + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(closeActiveWindows); + + async function testUninstallingProduct(product: Product) { + let isInstalled = await installer.isInstalled(product, resource); + if (isInstalled) { + await installer.uninstall(product, resource); + isInstalled = await installer.isInstalled(product, resource); + // Someimtes installation doesn't work on Travis + if (!IS_TRAVIS) { + assert.equal(isInstalled, false, 'Product uninstall failed'); + } + } + } + + EnumEx.getNamesAndValues(Product).forEach(prod => { + test(`${prod.name} : Uninstall`, async () => { + if (prod.value === Product.unittest || prod.value === Product.ctags) { + return; + } + await testUninstallingProduct(prod.value); + }); + }); + + async function testInstallingProduct(product: Product) { + const isInstalled = await installer.isInstalled(product, resource); + if (!isInstalled) { + await installer.install(product, resource); + } + const checkIsInstalledAgain = await installer.isInstalled(product, resource); + // Someimtes installation doesn't work on Travis + if (!IS_TRAVIS) { + assert.notEqual(checkIsInstalledAgain, false, 'Product installation failed'); + } + } + EnumEx.getNamesAndValues(Product).forEach(prod => { + test(`${prod.name} : Install`, async () => { + if (prod.value === Product.unittest || prod.value === Product.ctags) { + return; + } + await testInstallingProduct(prod.value); + }); + }); +}); diff --git a/src/test/extension.common.comms.socketCallbackHandler.test.ts b/src/test/common/socketCallbackHandler.test.ts similarity index 96% rename from src/test/extension.common.comms.socketCallbackHandler.test.ts rename to src/test/common/socketCallbackHandler.test.ts index c30c1a2dc3ec..d32cd43eed26 100644 --- a/src/test/extension.common.comms.socketCallbackHandler.test.ts +++ b/src/test/common/socketCallbackHandler.test.ts @@ -4,17 +4,17 @@ // // Place this right on top -import { initialize } from './initialize'; +import { initialize } from './../initialize'; // The module 'assert' provides assertion methods from node import * as assert from 'assert'; // You can import and use all API from the 'vscode' module // as well as import your extension to test it -import { SocketStream } from '../client/common/comms/SocketStream'; -import { SocketServer } from '../client/common/comms/socketServer'; -import { SocketCallbackHandler } from '../client/common/comms/socketCallbackHandler'; -import { createDeferred, Deferred } from '../client/common/helpers'; -import { IdDispenser } from '../client/common/idDispenser'; +import { SocketStream } from '../../client/common/comms/SocketStream'; +import { SocketServer } from '../../client/common/comms/socketServer'; +import { SocketCallbackHandler } from '../../client/common/comms/socketCallbackHandler'; +import { createDeferred, Deferred } from '../../client/common/helpers'; +import { IdDispenser } from '../../client/common/idDispenser'; import * as net from 'net'; @@ -300,4 +300,4 @@ suite('SocketCallbackHandler', () => { return def.promise; }).then(done).catch(done); }); -}); \ No newline at end of file +}); diff --git a/src/test/extension.common.comms.socketStream.test.ts b/src/test/common/socketStream.test.ts similarity index 98% rename from src/test/extension.common.comms.socketStream.test.ts rename to src/test/common/socketStream.test.ts index c3c9b29a57a5..761196b489e0 100644 --- a/src/test/extension.common.comms.socketStream.test.ts +++ b/src/test/common/socketStream.test.ts @@ -4,14 +4,14 @@ // // Place this right on top -import { initialize } from './initialize'; +import { initialize } from './../initialize'; // The module 'assert' provides assertion methods from node import * as assert from 'assert'; // You can import and use all API from the 'vscode' module // as well as import your extension to test it import * as vscode from 'vscode'; -import { SocketStream } from '../client/common/comms/SocketStream'; +import { SocketStream } from '../../client/common/comms/SocketStream'; import * as net from 'net'; const uint64be = require("uint64be"); @@ -167,4 +167,4 @@ suite('SocketStream', () => { assert.equal(socket.dataWritten, message) done(); }); -}); \ No newline at end of file +}); diff --git a/src/test/common/versionUtils.test.ts b/src/test/common/versionUtils.test.ts new file mode 100644 index 000000000000..a722198b9fa4 --- /dev/null +++ b/src/test/common/versionUtils.test.ts @@ -0,0 +1,20 @@ +import * as assert from 'assert'; +import { VersionUtils } from '../../client/common/versionUtils'; + +suite('Version Utils', () => { + test('Must handle invalid versions', async () => { + const version = 'ABC'; + assert.equal(VersionUtils.convertToSemver(version), `${version}.0.0`, 'Version is incorrect'); + }); + test('Must handle null, empty and undefined', async () => { + assert.equal(VersionUtils.convertToSemver(''), '0.0.0', 'Version is incorrect for empty string'); + assert.equal(VersionUtils.convertToSemver(null), '0.0.0', 'Version is incorrect for null value'); + assert.equal(VersionUtils.convertToSemver(undefined), '0.0.0', 'Version is incorrect for undefined value'); + }); + test('Must be able to compare versions correctly', async () => { + assert.equal(VersionUtils.compareVersion('', '1'), 0, '1. Comparison failed'); + assert.equal(VersionUtils.compareVersion('1', '0.1'), 1, '2. Comparison failed'); + assert.equal(VersionUtils.compareVersion('2.10', '2.9'), 1, '3. Comparison failed'); + assert.equal(VersionUtils.compareVersion('2.99.9', '3'), 0, '4. Comparison failed'); + }); +}); diff --git a/src/test/definitions/code.test.ts b/src/test/definitions/code.test.ts new file mode 100644 index 000000000000..90427872e61c --- /dev/null +++ b/src/test/definitions/code.test.ts @@ -0,0 +1,204 @@ +// // Note: This example test is leveraging the Mocha test framework. +// // Please refer to their documentation on https://mochajs.org/ for help. + + +// // The module 'assert' provides assertion methods from node +// import * as assert from 'assert'; +// // You can import and use all API from the 'vscode' module +// // as well as import your extension to test it +// import * as vscode from 'vscode'; +// import * as path from 'path'; +// import * as settings from '../../client/common/configSettings'; +// import { execPythonFile } from '../../client/common/utils'; +// import { initialize, closeActiveWindows } from '../initialize'; + +// const pythonSettings = settings.PythonSettings.getInstance(); +// const autoCompPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'definition'); +// const fileOne = path.join(autoCompPath, 'one.py'); +// const fileTwo = path.join(autoCompPath, 'two.py'); +// const fileThree = path.join(autoCompPath, 'three.py'); +// const fileDecorator = path.join(autoCompPath, 'decorators.py'); +// const fileAwait = path.join(autoCompPath, 'await.test.py'); +// const fileEncoding = path.join(autoCompPath, 'four.py'); +// const fileEncodingUsed = path.join(autoCompPath, 'five.py'); + + +// suite('Code Definition', () => { +// let isPython3: Promise; +// suiteSetup(async () => { +// await initialize(); +// let version = await execPythonFile(pythonSettings.pythonPath, ['--version'], __dirname, true); +// isPython3 = Promise.resolve(version.indexOf('3.') >= 0); +// }); + +// suiteTeardown(() => closeActiveWindows()); +// teardown(() => closeActiveWindows()); + +// test('Go to method', done => { +// let textEditor: vscode.TextEditor; +// let textDocument: vscode.TextDocument; +// vscode.workspace.openTextDocument(fileOne).then(document => { +// textDocument = document; +// return vscode.window.showTextDocument(textDocument); +// }).then(editor => { +// assert(vscode.window.activeTextEditor, 'No active editor'); +// textEditor = editor; +// const position = new vscode.Position(30, 5); +// return vscode.commands.executeCommand('vscode.executeDefinitionProvider', textDocument.uri, position); +// }).then(def => { +// assert.equal(def.length, 1, 'Definition length is incorrect'); +// assert.equal(def[0].uri.fsPath, fileOne, 'Incorrect file'); +// assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '17,4', 'Start position is incorrect'); +// assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '21,11', 'End position is incorrect'); +// }).then(done, done); +// }); + +// test('Go to function', done => { +// let textEditor: vscode.TextEditor; +// let textDocument: vscode.TextDocument; +// vscode.workspace.openTextDocument(fileOne).then(document => { +// textDocument = document; +// return vscode.window.showTextDocument(textDocument); +// }).then(editor => { +// assert(vscode.window.activeTextEditor, 'No active editor'); +// textEditor = editor; +// const position = new vscode.Position(45, 5); +// return vscode.commands.executeCommand('vscode.executeDefinitionProvider', textDocument.uri, position); +// }).then(def => { +// assert.equal(def.length, 1, 'Definition length is incorrect'); +// assert.equal(def[0].uri.fsPath, fileOne, 'Incorrect file'); +// assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '32,0', 'Start position is incorrect'); +// assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '33,21', 'End position is incorrect'); +// }).then(done, done); +// }); + +// test('Go to function with decorator', async () => { +// const textDocument = await vscode.workspace.openTextDocument(fileDecorator); +// await vscode.window.showTextDocument(textDocument); +// const position = new vscode.Position(7, 2); +// const def = await vscode.commands.executeCommand('vscode.executeDefinitionProvider', textDocument.uri, position); +// assert.equal(def.length, 1, 'Definition length is incorrect'); +// assert.equal(def[0].uri.fsPath, fileDecorator, 'Incorrect file'); +// assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '4,0', 'Start position is incorrect'); +// assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '5,22', 'End position is incorrect'); +// }); + +// test('Go to function with decorator (jit)', async () => { +// const textDocument = await vscode.workspace.openTextDocument(fileDecorator); +// await vscode.window.showTextDocument(textDocument); +// const position = new vscode.Position(27, 2); +// const def = await vscode.commands.executeCommand('vscode.executeDefinitionProvider', textDocument.uri, position); +// assert.equal(def.length, 1, 'Definition length is incorrect'); +// assert.equal(def[0].uri.fsPath, fileDecorator, 'Incorrect file'); +// assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '19,0', 'Start position is incorrect'); +// assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '26,42', 'End position is incorrect'); +// }); + +// test('Go to function with decorator (fabric)', async () => { +// const textDocument = await vscode.workspace.openTextDocument(fileDecorator); +// await vscode.window.showTextDocument(textDocument); +// const position = new vscode.Position(13, 2); +// const def = await vscode.commands.executeCommand('vscode.executeDefinitionProvider', textDocument.uri, position); +// assert.equal(def.length, 1, 'Definition length is incorrect'); +// if (!def[0].uri.fsPath.endsWith('operations.py')) { +// assert.fail(def[0].uri.fsPath, 'operations.py', 'Source of sudo is incorrect', 'file source'); +// } +// assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '1094,0', 'Start position is incorrect (3rd part operations.py could have changed)'); +// assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '1148,4', 'End position is incorrect (3rd part operations.py could have changed)'); +// }); + +// test('Go to function decorator', async () => { +// const textDocument = await vscode.workspace.openTextDocument(fileDecorator); +// await vscode.window.showTextDocument(textDocument); +// const position = new vscode.Position(3, 3); +// const def = await vscode.commands.executeCommand('vscode.executeDefinitionProvider', textDocument.uri, position); +// assert.equal(def.length, 1, 'Definition length is incorrect'); +// assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '0,0', 'Start position is incorrect'); +// assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '1,12', 'End position is incorrect'); +// }); + +// test('Go to async method', async () => { +// if (!await isPython3) { +// return; +// } +// const textDocument = await vscode.workspace.openTextDocument(fileAwait); +// await vscode.window.showTextDocument(textDocument); +// const position = new vscode.Position(10, 22); +// const def = await vscode.commands.executeCommand('vscode.executeDefinitionProvider', textDocument.uri, position); +// assert.equal(def.length, 1, 'Definition length is incorrect (currently not working)'); +// assert.equal(def[0].uri.fsPath, fileAwait, 'Wrong file (currently not working)'); +// assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '6,10', 'Start position is incorrect (currently not working)'); +// assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '1,12', 'End position is incorrect (currently not working)'); +// }); + +// test('Go to async function', async () => { +// if (!await isPython3) { +// return; +// } +// const textDocument = await vscode.workspace.openTextDocument(fileAwait); +// await vscode.window.showTextDocument(textDocument); +// const position = new vscode.Position(18, 12); +// const def = await vscode.commands.executeCommand('vscode.executeDefinitionProvider', textDocument.uri, position); +// assert.equal(def.length, 1, 'Definition length is incorrect (currently not working)'); +// assert.equal(def[0].uri.fsPath, fileAwait, 'Wrong file (currently not working)'); +// assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '6,10', 'Start position is incorrect (currently not working)'); +// assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '1,12', 'End position is incorrect (currently not working)'); +// }); + +// test('Across files', done => { +// let textEditor: vscode.TextEditor; +// let textDocument: vscode.TextDocument; +// vscode.workspace.openTextDocument(fileThree).then(document => { +// textDocument = document; +// return vscode.window.showTextDocument(textDocument); +// }).then(editor => { +// assert(vscode.window.activeTextEditor, 'No active editor'); +// textEditor = editor; +// const position = new vscode.Position(1, 5); +// return vscode.commands.executeCommand('vscode.executeDefinitionProvider', textDocument.uri, position); +// }).then(def => { +// assert.equal(def.length, 1, 'Definition length is incorrect'); +// assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '0,0', 'Start position is incorrect'); +// assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '5,11', 'End position is incorrect'); +// assert.equal(def[0].uri.fsPath, fileTwo, 'File is incorrect'); +// }).then(done, done); +// }); + +// test('With Unicode Characters', done => { +// let textEditor: vscode.TextEditor; +// let textDocument: vscode.TextDocument; +// vscode.workspace.openTextDocument(fileEncoding).then(document => { +// textDocument = document; +// return vscode.window.showTextDocument(textDocument); +// }).then(editor => { +// assert(vscode.window.activeTextEditor, 'No active editor'); +// textEditor = editor; +// const position = new vscode.Position(25, 6); +// return vscode.commands.executeCommand('vscode.executeDefinitionProvider', textDocument.uri, position); +// }).then(def => { +// assert.equal(def.length, 1, 'Definition length is incorrect'); +// assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '10,4', 'Start position is incorrect'); +// assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '16,35', 'End position is incorrect'); +// assert.equal(def[0].uri.fsPath, fileEncoding, 'File is incorrect'); +// }).then(done, done); +// }); + +// test('Across files with Unicode Characters', done => { +// let textEditor: vscode.TextEditor; +// let textDocument: vscode.TextDocument; +// vscode.workspace.openTextDocument(fileEncodingUsed).then(document => { +// textDocument = document; +// return vscode.window.showTextDocument(textDocument); +// }).then(editor => { +// assert(vscode.window.activeTextEditor, 'No active editor'); +// textEditor = editor; +// const position = new vscode.Position(1, 11); +// return vscode.commands.executeCommand('vscode.executeDefinitionProvider', textDocument.uri, position); +// }).then(def => { +// assert.equal(def.length, 1, 'Definition length is incorrect'); +// assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '18,0', 'Start position is incorrect'); +// assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '23,16', 'End position is incorrect'); +// assert.equal(def[0].uri.fsPath, fileEncoding, 'File is incorrect'); +// }).then(done, done); +// }); +// }); diff --git a/src/test/definitions/hover.test.ts b/src/test/definitions/hover.test.ts new file mode 100644 index 000000000000..07f06b8843ca --- /dev/null +++ b/src/test/definitions/hover.test.ts @@ -0,0 +1,293 @@ +// Note: This example test is leveraging the Mocha test framework. +// Please refer to their documentation on https://mochajs.org/ for help. + + +// The module 'assert' provides assertion methods from node +import * as assert from 'assert'; +import { EOL } from 'os'; +// You can import and use all API from the 'vscode' module +// as well as import your extension to test it +import * as vscode from 'vscode'; +import * as path from 'path'; +import { initialize, closeActiveWindows, initializeTest } from '../initialize'; +import { normalizeMarkedString } from '../textUtils'; + +const autoCompPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'autocomp'); +const hoverPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'hover'); +const fileOne = path.join(autoCompPath, 'one.py'); +const fileThree = path.join(autoCompPath, 'three.py'); +const fileEncoding = path.join(autoCompPath, 'four.py'); +const fileEncodingUsed = path.join(autoCompPath, 'five.py'); +const fileHover = path.join(autoCompPath, 'hoverTest.py'); +const fileStringFormat = path.join(hoverPath, 'stringFormat.py'); + +suite('Hover Definition', () => { + suiteSetup(() => initialize()); + setup(() => initializeTest()); + suiteTeardown(() => closeActiveWindows()); + teardown(() => closeActiveWindows()); + + test('Method', done => { + let textEditor: vscode.TextEditor; + let textDocument: vscode.TextDocument; + vscode.workspace.openTextDocument(fileOne).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + textEditor = editor; + const position = new vscode.Position(30, 5); + return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); + }).then(def => { + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '30,4', 'Start position is incorrect'); + assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '30,11', 'End position is incorrect'); + assert.equal(def[0].contents.length, 1, 'Invalid content items'); + const expectedContent = '```python' + EOL + 'def method1()' + EOL + '```' + EOL + 'This is method1'; + assert.equal(normalizeMarkedString(def[0].contents[0]), expectedContent, 'function signature incorrect'); + }).then(done, done); + }); + + test('Across files', done => { + let textEditor: vscode.TextEditor; + let textDocument: vscode.TextDocument; + vscode.workspace.openTextDocument(fileThree).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + textEditor = editor; + const position = new vscode.Position(1, 12); + return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); + }).then(def => { + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '1,9', 'Start position is incorrect'); + assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '1,12', 'End position is incorrect'); + assert.equal(normalizeMarkedString(def[0].contents[0]), '```python' + EOL + 'def fun()' + EOL + '```' + EOL + 'This is fun', 'Invalid conents'); + }).then(done, done); + }); + + test('With Unicode Characters', done => { + let textEditor: vscode.TextEditor; + let textDocument: vscode.TextDocument; + vscode.workspace.openTextDocument(fileEncoding).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + textEditor = editor; + const position = new vscode.Position(25, 6); + return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); + }).then(def => { + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '25,4', 'Start position is incorrect'); + assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '25,7', 'End position is incorrect'); + assert.equal(normalizeMarkedString(def[0].contents[0]), '```python' + EOL + 'def bar()' + EOL + '```' + EOL + + '่ฏดๆ˜Ž - keep this line, it works' + EOL + 'delete following line, it works' + + EOL + 'ๅฆ‚ๆžœๅญ˜ๅœจ้œ€่ฆ็ญ‰ๅพ…ๅฎกๆ‰นๆˆ–ๆญฃๅœจๆ‰ง่กŒ็š„ไปปๅŠก๏ผŒๅฐ†ไธๅˆทๆ–ฐ้กต้ข', 'Invalid conents'); + }).then(done, done); + }); + + test('Across files with Unicode Characters', done => { + let textEditor: vscode.TextEditor; + let textDocument: vscode.TextDocument; + vscode.workspace.openTextDocument(fileEncodingUsed).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + textEditor = editor; + const position = new vscode.Position(1, 11); + return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); + }).then(def => { + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '1,5', 'Start position is incorrect'); + assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '1,16', 'End position is incorrect'); + assert.equal(normalizeMarkedString(def[0].contents[0]), '```python' + EOL + + 'def showMessage()' + EOL + + '```' + EOL + + 'ะšัŽะผ ัƒั‚ ะถัะผะฟัั€ ะฟะพัˆะถะธะผ ะปัŒะฐะฑะพั€ัะถ, ะบะพะผะผัŽะฝั‹ ัะฝั‚ัั€ััั‰ัั‚ ะฝะฐะผ ะตะด, ะดะตะบั‚ะฐ ะธะณะฝะพั‚ะฐ ะฝั‹ะผะพั€ั ะถัั‚ ัะธ. ' + EOL + + 'ะจัะฐ ะดะตะบะฐะผ ัะบัˆั‹ั€ะบะธ ัะธ, ัะธ ะทั‹ะด ัั€ั€ัะผ ะดะพะบัะฝะดั‘, ะฒะตะบะถ ั„ะฐะบัั‚ั ะฟัั€ั‡ั‹ะบะฒัŽัั€ั‘ะถ ะบัƒ.', 'Invalid conents'); + }).then(done, done); + }); + + test('Nothing for keywords (class)', done => { + let textEditor: vscode.TextEditor; + let textDocument: vscode.TextDocument; + vscode.workspace.openTextDocument(fileOne).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + textEditor = editor; + const position = new vscode.Position(5, 1); + return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); + }).then(def => { + assert.equal(def.length, 0, 'Definition length is incorrect'); + }).then(done, done); + }); + + test('Nothing for keywords (for)', done => { + let textEditor: vscode.TextEditor; + let textDocument: vscode.TextDocument; + vscode.workspace.openTextDocument(fileHover).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + textEditor = editor; + const position = new vscode.Position(3, 1); + return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); + }).then(def => { + assert.equal(def.length, 0, 'Definition length is incorrect'); + }).then(done, done); + }); + + test('Highlighting Class', done => { + let textEditor: vscode.TextEditor; + let textDocument: vscode.TextDocument; + vscode.workspace.openTextDocument(fileHover).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + textEditor = editor; + const position = new vscode.Position(11, 15); + return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); + }).then(def => { + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '11,12', 'Start position is incorrect'); + assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '11,18', 'End position is incorrect'); + let documentation = "```python" + EOL + + "class Random(x=None)" + EOL + + "```" + EOL + + "Random number generator base class used by bound module functions." + EOL + + "" + EOL + + "Used to instantiate instances of Random to get generators that don't" + EOL + + "share state." + EOL + + "" + EOL + + "Class Random can also be subclassed if you want to use a different basic" + EOL + + "generator of your own devising: in that case, override the following" + EOL + EOL + + "`methods` random(), seed(), getstate(), and setstate()." + EOL + EOL + + "Optionally, implement a getrandbits() method so that randrange()" + EOL + + "can cover arbitrarily large ranges."; + + assert.equal(normalizeMarkedString(def[0].contents[0]), documentation, 'Invalid conents'); + }).then(done, done); + }); + + test('Highlight Method', done => { + let textEditor: vscode.TextEditor; + let textDocument: vscode.TextDocument; + vscode.workspace.openTextDocument(fileHover).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + textEditor = editor; + const position = new vscode.Position(12, 10); + return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); + }).then(def => { + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '12,5', 'Start position is incorrect'); + assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '12,12', 'End position is incorrect'); + assert.equal(normalizeMarkedString(def[0].contents[0]), '```python' + EOL + + 'def randint(a, b)' + EOL + + '```' + EOL + + 'Return random integer in range [a, b], including both end points.', 'Invalid conents'); + }).then(done, done); + }); + + test('Highlight Function', done => { + let textEditor: vscode.TextEditor; + let textDocument: vscode.TextDocument; + vscode.workspace.openTextDocument(fileHover).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + textEditor = editor; + const position = new vscode.Position(8, 14); + return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); + }).then(def => { + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '8,11', 'Start position is incorrect'); + assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '8,15', 'End position is incorrect'); + assert.equal(normalizeMarkedString(def[0].contents[0]), '```python' + EOL + + 'def acos(x)' + EOL + + '```' + EOL + + 'Return the arc cosine (measured in radians) of x.', 'Invalid conents'); + }).then(done, done); + }); + + test('Highlight Multiline Method Signature', done => { + let textEditor: vscode.TextEditor; + let textDocument: vscode.TextDocument; + vscode.workspace.openTextDocument(fileHover).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + textEditor = editor; + const position = new vscode.Position(14, 14); + return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); + }).then(def => { + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '14,9', 'Start position is incorrect'); + assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '14,15', 'End position is incorrect'); + assert.equal(normalizeMarkedString(def[0].contents[0]), '```python' + EOL + + 'class Thread(group=None, target=None, name=None, args=(), kwargs=None, verbose=None)' + EOL + + '```' + EOL + + 'Thread(self, group=None, target=None, name=None,' + EOL + + 'args=(), kwargs=None, verbose=None)' + EOL + + '' + EOL + + 'A class that represents a thread of control.' + EOL + + '' + EOL + + 'This class can be safely subclassed in a limited fashion.', 'Invalid content items'); + }).then(done, done); + }); + + test('Variable', done => { + let textEditor: vscode.TextEditor; + let textDocument: vscode.TextDocument; + vscode.workspace.openTextDocument(fileHover).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + textEditor = editor; + const position = new vscode.Position(6, 2); + return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); + }).then(def => { + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(def[0].contents.length, 1, 'Only expected one result'); + const contents = normalizeMarkedString(def[0].contents[0]); + if (contents.indexOf("```python") === -1) { + assert.fail(contents, "", "First line is incorrect", "compare"); + } + if (contents.indexOf("Random number generator base class used by bound module functions.") === -1) { + assert.fail(contents, "", "'Random number generator' message missing", "compare"); + } + if (contents.indexOf("Class Random can also be subclassed if you want to use a different basic") === -1) { + assert.fail(contents, "", "'Class Random message' missing", "compare"); + } + }).then(done, done); + }); + + test('format().capitalize()', async () => { + const textDocument = await vscode.workspace.openTextDocument(fileStringFormat); + await vscode.window.showTextDocument(textDocument); + const position = new vscode.Position(5, 41); + const def = await vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(def[0].contents.length, 1, 'Only expected one result'); + const contents = normalizeMarkedString(def[0].contents[0]); + if (contents.indexOf("def capitalize") === -1) { + assert.fail(contents, "", "'def capitalize' is missing", "compare"); + } + if (contents.indexOf("Return a capitalized version of S") === -1 && + contents.indexOf("Return a copy of the string S with only its first character") === -1) { + assert.fail(contents, "", "'Return a capitalized version of S/Return a copy of the string S with only its first character' message missing", "compare"); + } + }); +}); diff --git a/src/test/definitions/parallel.test.ts b/src/test/definitions/parallel.test.ts new file mode 100644 index 000000000000..f70edb4a9910 --- /dev/null +++ b/src/test/definitions/parallel.test.ts @@ -0,0 +1,40 @@ +import * as assert from 'assert'; +import { EOL } from 'os'; +import * as vscode from 'vscode'; +import * as path from 'path'; +import { initialize, closeActiveWindows } from '../initialize'; +import { normalizeMarkedString } from '../textUtils'; + +const autoCompPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'autocomp'); +const fileOne = path.join(autoCompPath, 'one.py'); + +suite('Code, Hover Definition and Intellisense', () => { + suiteSetup(() => initialize()); + suiteTeardown(() => closeActiveWindows()); + teardown(() => closeActiveWindows()); + + test('All three together', async () => { + const textDocument = await vscode.workspace.openTextDocument(fileOne); + const editor = await vscode.window.showTextDocument(textDocument); + + let position = new vscode.Position(30, 5); + const hoverDef = await vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); + const codeDef = await vscode.commands.executeCommand('vscode.executeDefinitionProvider', textDocument.uri, position); + position = new vscode.Position(3, 10); + const list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + + assert.equal(list.items.filter(item => item.label === 'api_version').length, 1, 'api_version not found'); + + assert.equal(codeDef.length, 1, 'Definition length is incorrect'); + assert.equal(codeDef[0].uri.fsPath, fileOne, 'Incorrect file'); + assert.equal(`${codeDef[0].range.start.line},${codeDef[0].range.start.character}`, '17,4', 'Start position is incorrect'); + assert.equal(`${codeDef[0].range.end.line},${codeDef[0].range.end.character}`, '21,11', 'End position is incorrect'); + + assert.equal(hoverDef.length, 1, 'Definition length is incorrect'); + assert.equal(`${hoverDef[0].range.start.line},${hoverDef[0].range.start.character}`, '30,4', 'Start position is incorrect'); + assert.equal(`${hoverDef[0].range.end.line},${hoverDef[0].range.end.character}`, '30,11', 'End position is incorrect'); + assert.equal(hoverDef[0].contents.length, 1, 'Invalid content items'); + const expectedContent = '```python' + EOL + 'def method1()' + EOL + '```' + EOL + 'This is method1'; + assert.equal(normalizeMarkedString(hoverDef[0].contents[0]), expectedContent, 'function signature incorrect'); + }); +}); diff --git a/src/test/extension.autocomplete.test.ts b/src/test/extension.autocomplete.test.ts deleted file mode 100644 index e6e92b4e642e..000000000000 --- a/src/test/extension.autocomplete.test.ts +++ /dev/null @@ -1,429 +0,0 @@ - -// Note: This example test is leveraging the Mocha test framework. -// Please refer to their documentation on https://mochajs.org/ for help. - - -// Place this right on top -import { initialize, PYTHON_PATH, closeActiveWindows } from './initialize'; -// The module 'assert' provides assertion methods from node -import * as assert from 'assert'; -import { EOL } from 'os'; -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it -import * as vscode from 'vscode'; -import * as path from 'path'; -import * as settings from '../client/common/configSettings'; - -let pythonSettings = settings.PythonSettings.getInstance(); -let autoCompPath = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'autocomp'); -const fileOne = path.join(autoCompPath, 'one.py'); -const fileTwo = path.join(autoCompPath, 'two.py'); -const fileThree = path.join(autoCompPath, 'three.py'); -const fileEncoding = path.join(autoCompPath, 'four.py'); -const fileEncodingUsed = path.join(autoCompPath, 'five.py'); -const fileHover = path.join(autoCompPath, 'hoverTest.py'); - -suite('Autocomplete', () => { - suiteSetup(done => { - initialize().then(() => { - pythonSettings.pythonPath = PYTHON_PATH; - done(); - }, done); - }); - - suiteTeardown(done => { - closeActiveWindows().then(done, done); - }); - teardown(done => { - closeActiveWindows().then(done, done); - }); - - test('For "sys."', done => { - let textEditor: vscode.TextEditor; - let textDocument: vscode.TextDocument; - return vscode.workspace.openTextDocument(fileOne).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; - const position = new vscode.Position(3, 10); - return vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); - }).then((list: { isIncomplete: boolean, items: vscode.CompletionItem[] }) => { - assert.notEqual(list.items.filter(item => item.label === 'api_version').length, 0, 'api_version not found'); - assert.notEqual(list.items.filter(item => item.label === 'argv').length, 0, 'argv not found'); - assert.notEqual(list.items.filter(item => item.label === 'prefix').length, 0, 'prefix not found'); - }).then(done, done); - }); - - test('For custom class', done => { - let textEditor: vscode.TextEditor; - let textDocument: vscode.TextDocument; - return vscode.workspace.openTextDocument(fileOne).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; - const position = new vscode.Position(30, 4); - return vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); - }).then((list: { isIncomplete: boolean, items: vscode.CompletionItem[] }) => { - assert.notEqual(list.items.filter(item => item.label === 'method1').length, 0, 'method1 not found'); - assert.notEqual(list.items.filter(item => item.label === 'method2').length, 0, 'method2 not found'); - }).then(done, done); - }); - - test('With Unicode Characters', done => { - let textEditor: vscode.TextEditor; - let textDocument: vscode.TextDocument; - return vscode.workspace.openTextDocument(fileEncoding).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; - const position = new vscode.Position(25, 4); - return vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); - }).then((list: { isIncomplete: boolean, items: vscode.CompletionItem[] }) => { - assert.equal(list.items.filter(item => item.label === 'bar').length, 1, 'bar not found'); - const documentation = `่ฏดๆ˜Ž - keep this line, it works${EOL}delete following line, it works${EOL}ๅฆ‚ๆžœๅญ˜ๅœจ้œ€่ฆ็ญ‰ๅพ…ๅฎกๆ‰นๆˆ–ๆญฃๅœจๆ‰ง่กŒ็š„ไปปๅŠก๏ผŒๅฐ†ไธๅˆทๆ–ฐ้กต้ข`; - assert.equal(list.items.filter(item => item.label === 'bar')[0].documentation, documentation, 'unicode documentation is incorrect'); - }).then(done, done); - }); - - test('Across files With Unicode Characters', done => { - let textEditor: vscode.TextEditor; - let textDocument: vscode.TextDocument; - return vscode.workspace.openTextDocument(fileEncodingUsed).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; - const position = new vscode.Position(1, 5); - return vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); - }).then((list: { isIncomplete: boolean, items: vscode.CompletionItem[] }) => { - assert.equal(list.items.filter(item => item.label === 'Foo').length, 1, 'Foo not found'); - assert.equal(list.items.filter(item => item.label === 'Foo')[0].documentation, '่ฏดๆ˜Ž', 'Foo unicode documentation is incorrect'); - - assert.equal(list.items.filter(item => item.label === 'showMessage').length, 1, 'showMessage not found'); - const documentation = `ะšัŽะผ ัƒั‚ ะถัะผะฟัั€ ะฟะพัˆะถะธะผ ะปัŒะฐะฑะพั€ัะถ, ะบะพะผะผัŽะฝั‹ ัะฝั‚ัั€ััั‰ัั‚ ะฝะฐะผ ะตะด, ะดะตะบั‚ะฐ ะธะณะฝะพั‚ะฐ ะฝั‹ะผะพั€ั ะถัั‚ ัะธ. ${EOL}ะจัะฐ ะดะตะบะฐะผ ัะบัˆั‹ั€ะบะธ ัะธ, ัะธ ะทั‹ะด ัั€ั€ัะผ ะดะพะบัะฝะดั‘, ะฒะตะบะถ ั„ะฐะบัั‚ั ะฟัั€ั‡ั‹ะบะฒัŽัั€ั‘ะถ ะบัƒ.`; - assert.equal(list.items.filter(item => item.label === 'showMessage')[0].documentation, documentation, 'showMessage unicode documentation is incorrect'); - }).then(done, done); - }); -}); - -suite('Code Definition', () => { - suiteSetup(done => { - initialize().then(() => { - pythonSettings.pythonPath = PYTHON_PATH; - done(); - }, done); - }); - - suiteTeardown(done => { - closeActiveWindows().then(done, done); - }); - teardown(done => { - closeActiveWindows().then(done, done); - }); - - test('Go to method', done => { - let textEditor: vscode.TextEditor; - let textDocument: vscode.TextDocument; - return vscode.workspace.openTextDocument(fileOne).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; - const position = new vscode.Position(30, 5); - return vscode.commands.executeCommand('vscode.executeDefinitionProvider', textDocument.uri, position); - }).then((def: [{ range: vscode.Range, uri: vscode.Uri }]) => { - assert.equal(def.length, 1, 'Definition lenght is incorrect'); - assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '17,8', 'Start position is incorrect'); - assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '17,8', 'End position is incorrect'); - }).then(done, done); - }); - - test('Across files', done => { - let textEditor: vscode.TextEditor; - let textDocument: vscode.TextDocument; - return vscode.workspace.openTextDocument(fileThree).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; - const position = new vscode.Position(1, 5); - return vscode.commands.executeCommand('vscode.executeDefinitionProvider', textDocument.uri, position); - }).then((def: [{ range: vscode.Range, uri: vscode.Uri }]) => { - assert.equal(def.length, 1, 'Definition lenght is incorrect'); - assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '0,6', 'Start position is incorrect'); - assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '0,6', 'End position is incorrect'); - assert.equal(def[0].uri.fsPath, fileTwo, 'File is incorrect'); - }).then(done, done); - }); - - test('With Unicode Characters', done => { - let textEditor: vscode.TextEditor; - let textDocument: vscode.TextDocument; - return vscode.workspace.openTextDocument(fileEncoding).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; - const position = new vscode.Position(25, 6); - return vscode.commands.executeCommand('vscode.executeDefinitionProvider', textDocument.uri, position); - }).then((def: [{ range: vscode.Range, uri: vscode.Uri }]) => { - assert.equal(def.length, 1, 'Definition lenght is incorrect'); - assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '10,8', 'Start position is incorrect'); - assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '10,8', 'End position is incorrect'); - assert.equal(def[0].uri.fsPath, fileEncoding, 'File is incorrect'); - }).then(done, done); - }); - - test('Across files with Unicode Characters', done => { - let textEditor: vscode.TextEditor; - let textDocument: vscode.TextDocument; - return vscode.workspace.openTextDocument(fileEncodingUsed).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; - const position = new vscode.Position(1, 11); - return vscode.commands.executeCommand('vscode.executeDefinitionProvider', textDocument.uri, position); - }).then((def: [{ range: vscode.Range, uri: vscode.Uri }]) => { - assert.equal(def.length, 1, 'Definition lenght is incorrect'); - assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '18,4', 'Start position is incorrect'); - assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '18,4', 'End position is incorrect'); - assert.equal(def[0].uri.fsPath, fileEncoding, 'File is incorrect'); - }).then(done, done); - }); -}); - -suite('Hover Definition', () => { - suiteSetup(done => { - initialize().then(() => { - pythonSettings.pythonPath = PYTHON_PATH; - done(); - }, done); - }); - - suiteTeardown(done => { - closeActiveWindows().then(done, done); - }); - teardown(done => { - closeActiveWindows().then(done, done); - }); - - test('Method', done => { - let textEditor: vscode.TextEditor; - let textDocument: vscode.TextDocument; - return vscode.workspace.openTextDocument(fileOne).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; - const position = new vscode.Position(30, 5); - return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); - }).then((def: [{ range: vscode.Range, contents: { language: string, value: string }[] }]) => { - assert.equal(def.length, 1, 'Definition lenght is incorrect'); - assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '30,4', 'Start position is incorrect'); - assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '30,11', 'End position is incorrect'); - assert.equal(def[0].contents.length, 2, 'Invalid content items'); - assert.equal(def[0].contents[0].value, 'def method1(self)', 'function signature incorrect'); - assert.equal(def[0].contents[1], `This is method1`, 'Invalid conents'); - }).then(done, done); - }); - - test('Across files', done => { - let textEditor: vscode.TextEditor; - let textDocument: vscode.TextDocument; - return vscode.workspace.openTextDocument(fileThree).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; - const position = new vscode.Position(1, 12); - return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); - }).then((def: [{ range: vscode.Range, contents: { language: string, value: string }[] }]) => { - assert.equal(def.length, 1, 'Definition lenght is incorrect'); - assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '1,9', 'Start position is incorrect'); - assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '1,12', 'End position is incorrect'); - assert.equal(def[0].contents[0].value, 'def fun()', 'function signature incorrect'); - assert.equal(def[0].contents[1], `This is fun`, 'Invalid conents'); - }).then(done, done); - }); - - test('With Unicode Characters', done => { - let textEditor: vscode.TextEditor; - let textDocument: vscode.TextDocument; - return vscode.workspace.openTextDocument(fileEncoding).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; - const position = new vscode.Position(25, 6); - return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); - }).then((def: [{ range: vscode.Range, contents: { language: string, value: string }[] }]) => { - assert.equal(def.length, 1, 'Definition lenght is incorrect'); - assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '25,4', 'Start position is incorrect'); - assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '25,7', 'End position is incorrect'); - assert.equal(def[0].contents[0].value, 'def bar()', 'function signature incorrect'); - const documentation = `่ฏดๆ˜Ž - keep this line, it works${EOL}delete following line, it works${EOL}ๅฆ‚ๆžœๅญ˜ๅœจ้œ€่ฆ็ญ‰ๅพ…ๅฎกๆ‰นๆˆ–ๆญฃๅœจๆ‰ง่กŒ็š„ไปปๅŠก๏ผŒๅฐ†ไธๅˆทๆ–ฐ้กต้ข`; - assert.equal(def[0].contents[1], documentation, 'Invalid conents'); - }).then(done, done); - }); - - test('Across files with Unicode Characters', done => { - let textEditor: vscode.TextEditor; - let textDocument: vscode.TextDocument; - return vscode.workspace.openTextDocument(fileEncodingUsed).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; - const position = new vscode.Position(1, 11); - return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); - }).then((def: [{ range: vscode.Range, contents: { language: string, value: string }[] }]) => { - assert.equal(def.length, 1, 'Definition lenght is incorrect'); - assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '1,5', 'Start position is incorrect'); - assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '1,16', 'End position is incorrect'); - assert.equal(def[0].contents[0].value, 'def showMessage()', 'Invalid content items'); - const documentation = `ะšัŽะผ ัƒั‚ ะถัะผะฟัั€ ะฟะพัˆะถะธะผ ะปัŒะฐะฑะพั€ัะถ, ะบะพะผะผัŽะฝั‹ ัะฝั‚ัั€ััั‰ัั‚ ะฝะฐะผ ะตะด, ะดะตะบั‚ะฐ ะธะณะฝะพั‚ะฐ ะฝั‹ะผะพั€ั ะถัั‚ ัะธ. ${EOL}ะจัะฐ ะดะตะบะฐะผ ัะบัˆั‹ั€ะบะธ ัะธ, ัะธ ะทั‹ะด ัั€ั€ัะผ ะดะพะบัะฝะดั‘, ะฒะตะบะถ ั„ะฐะบัั‚ั ะฟัั€ั‡ั‹ะบะฒัŽัั€ั‘ะถ ะบัƒ.`; - assert.equal(def[0].contents[1], documentation, 'Invalid conents'); - }).then(done, done); - }); - - test('Nothing for keywords (class)', done => { - let textEditor: vscode.TextEditor; - let textDocument: vscode.TextDocument; - return vscode.workspace.openTextDocument(fileOne).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; - const position = new vscode.Position(5, 1); - return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); - }).then((def: [{ range: vscode.Range, contents: { language: string, value: string }[] }]) => { - assert.equal(def.length, 0, 'Definition lenght is incorrect'); - }).then(done, done); - }); - - test('Nothing for keywords (for)', done => { - let textEditor: vscode.TextEditor; - let textDocument: vscode.TextDocument; - return vscode.workspace.openTextDocument(fileHover).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; - const position = new vscode.Position(3, 1); - return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); - }).then((def: [{ range: vscode.Range, contents: { language: string, value: string }[] }]) => { - assert.equal(def.length, 0, 'Definition lenght is incorrect'); - }).then(done, done); - }); - - test('Highlighting Class', done => { - let textEditor: vscode.TextEditor; - let textDocument: vscode.TextDocument; - return vscode.workspace.openTextDocument(fileHover).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; - const position = new vscode.Position(11, 15); - return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); - }).then((def: [{ range: vscode.Range, contents: { language: string, value: string }[] }]) => { - assert.equal(def.length, 1, 'Definition lenght is incorrect'); - assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '11,12', 'Start position is incorrect'); - assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '11,18', 'End position is incorrect'); - assert.equal(def[0].contents[0].value, 'class Random(self, x=None)', 'Invalid content items'); - const documentation = `Random number generator base class used by bound module functions.${EOL}${EOL}` + - `Used to instantiate instances of Random to get generators that don't${EOL}` + - `share state.${EOL}${EOL}` + - `Class Random can also be subclassed if you want to use a different basic${EOL}` + - `generator of your own devising: in that case, override the following${EOL}` + - `methods: random(), seed(), getstate(), and setstate().${EOL}` + - `Optionally, implement a getrandbits() method so that randrange()${EOL}` + - `can cover arbitrarily large ranges.`; - - assert.equal(def[0].contents[1], documentation, 'Invalid conents'); - }).then(done, done); - }); - - test('Highlight Method', done => { - let textEditor: vscode.TextEditor; - let textDocument: vscode.TextDocument; - return vscode.workspace.openTextDocument(fileHover).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; - const position = new vscode.Position(12, 10); - return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); - }).then((def: [{ range: vscode.Range, contents: { language: string, value: string }[] }]) => { - assert.equal(def.length, 1, 'Definition lenght is incorrect'); - assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '12,5', 'Start position is incorrect'); - assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '12,12', 'End position is incorrect'); - assert.equal(def[0].contents[0].value, 'def randint(self, a, b)', 'Invalid content items'); - const documentation = `Return random integer in range [a, b], including both end points.`; - assert.equal(def[0].contents[1], documentation, 'Invalid conents'); - }).then(done, done); - }); - - test('Highlight Function', done => { - let textEditor: vscode.TextEditor; - let textDocument: vscode.TextDocument; - return vscode.workspace.openTextDocument(fileHover).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; - const position = new vscode.Position(8, 14); - return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); - }).then((def: [{ range: vscode.Range, contents: { language: string, value: string }[] }]) => { - assert.equal(def.length, 1, 'Definition lenght is incorrect'); - assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '8,11', 'Start position is incorrect'); - assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '8,15', 'End position is incorrect'); - assert.equal(def[0].contents[0].value, 'def acos(x)', 'Invalid content items'); - const documentation = `Return the arc cosine (measured in radians) of x.`; - assert.equal(def[0].contents[1], documentation, 'Invalid conents'); - }).then(done, done); - }); - - test('Highlight Multiline Method Signature', done => { - let textEditor: vscode.TextEditor; - let textDocument: vscode.TextDocument; - return vscode.workspace.openTextDocument(fileHover).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; - const position = new vscode.Position(14, 14); - return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); - }).then((def: [{ range: vscode.Range, contents: { language: string, value: string }[] }]) => { - assert.equal(def.length, 1, 'Definition lenght is incorrect'); - assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '14,9', 'Start position is incorrect'); - assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '14,15', 'End position is incorrect'); - const signature = `class Thread(self, group=None, target=None, name=None,${EOL}args=(), kwargs=None, verbose=None)`; - assert.equal(def[0].contents[0].value, signature, 'Invalid content items'); - const documentation = `A class that represents a thread of control.${EOL}${EOL}This class can be safely subclassed in a limited fashion.`; - assert.equal(def[0].contents[1], documentation, 'Invalid conents'); - }).then(done, done); - }); -}); \ No newline at end of file diff --git a/src/test/extension.common.test.ts b/src/test/extension.common.test.ts deleted file mode 100644 index d53a8f4ac552..000000000000 --- a/src/test/extension.common.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -// -// Note: This example test is leveraging the Mocha test framework. -// Please refer to their documentation on https://mochajs.org/ for help. -// - -// Place this right on top -import { initialize } from './initialize'; -// The module 'assert' provides assertion methods from node -import * as assert from 'assert'; - -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it -import { execPythonFile } from '../client/common/utils'; -import { EOL } from 'os'; -import { createDeferred } from '../client/common/helpers'; -import * as vscode from 'vscode'; - -// Defines a Mocha test suite to group tests of similar kind together -suite('ChildProc', () => { - setup(done => { - initialize().then(() => done(), done); - }); - test('Standard Response', done => { - execPythonFile('python', ['-c', 'print(1)'], __dirname, false).then(data => { - assert.ok(data === '1' + EOL); - }).then(done).catch(done); - }); - test('Error Response', done => { - const def = createDeferred(); - execPythonFile('python', ['-c', 'print(1'], __dirname, false).then(() => { - def.reject('Should have failed'); - }).catch(() => { - def.resolve(); - }); - - def.promise.then(done).catch(done); - }); - - test('Stream Stdout', done => { - const output: string[] = []; - function handleOutput(data: string) { - output.push(data); - } - execPythonFile('python', ['-c', 'print(1)'], __dirname, false, handleOutput).then(() => { - assert.equal(output.length, 1, 'Ouput length incorrect'); - assert.equal(output[0], '1' + EOL, 'Ouput value incorrect'); - }).then(done).catch(done); - }); - - test('Stream Stdout with Threads', done => { - const output: string[] = []; - function handleOutput(data: string) { - output.push(data); - } - execPythonFile('python', ['-c', 'import sys\nprint(1)\nsys.__stdout__.flush()\nimport time\ntime.sleep(5)\nprint(2)'], __dirname, false, handleOutput).then(() => { - assert.equal(output.length, 2, 'Ouput length incorrect'); - assert.equal(output[0], '1' + EOL, 'First Ouput value incorrect'); - assert.equal(output[1], '2' + EOL, 'Second Ouput value incorrect'); - }).then(done).catch(done); - }); - - test('Kill', done => { - const def = createDeferred(); - const output: string[] = []; - function handleOutput(data: string) { - output.push(data); - } - const cancellation = new vscode.CancellationTokenSource(); - execPythonFile('python', ['-c', 'import sys\nprint(1)\nsys.__stdout__.flush()\nimport time\ntime.sleep(5)\nprint(2)'], __dirname, false, handleOutput, cancellation.token).then(() => { - def.reject('Should not have completed'); - }).catch(()=>{ - def.resolve(); - }); - - setTimeout(() => { - cancellation.cancel(); - }, 1000); - - def.promise.then(done).catch(done); - }); -}); \ No newline at end of file diff --git a/src/test/extension.format.test.ts b/src/test/extension.format.test.ts deleted file mode 100644 index ebb5a4be734a..000000000000 --- a/src/test/extension.format.test.ts +++ /dev/null @@ -1,121 +0,0 @@ - -// Note: This example test is leveraging the Mocha test framework. -// Please refer to their documentation on https://mochajs.org/ for help. - - -// Place this right on top -import { initialize, IS_TRAVIS, PYTHON_PATH, closeActiveWindows } from './initialize'; -// The module 'assert' provides assertion methods from node -import * as assert from 'assert'; - -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it -import * as vscode from 'vscode'; -import { AutoPep8Formatter } from '../client/formatters/autoPep8Formatter'; -import { YapfFormatter } from '../client/formatters/yapfFormatter'; -import * as path from 'path'; -import * as settings from '../client/common/configSettings'; -import * as fs from 'fs-extra'; -import { execPythonFile } from '../client/common/utils'; - -let pythonSettings = settings.PythonSettings.getInstance(); -let ch = vscode.window.createOutputChannel('Tests'); -let pythoFilesPath = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'formatting'); -const originalUnformattedFile = path.join(pythoFilesPath, 'fileToFormat.py'); - -const autoPep8FileToFormat = path.join(__dirname, 'pythonFiles', 'formatting', 'autoPep8FileToFormat.py'); -const autoPep8FileToAutoFormat = path.join(__dirname, 'pythonFiles', 'formatting', 'autoPep8FileToAutoFormat.py'); -const yapfFileToFormat = path.join(__dirname, 'pythonFiles', 'formatting', 'yapfFileToFormat.py'); -const yapfFileToAutoFormat = path.join(__dirname, 'pythonFiles', 'formatting', 'yapfFileToAutoFormat.py'); - -let formattedYapf = ''; -let formattedAutoPep8 = ''; - -suite('Formatting', () => { - suiteSetup(done => { - initialize().then(() => { - pythonSettings.pythonPath = PYTHON_PATH; - [autoPep8FileToFormat, autoPep8FileToAutoFormat, yapfFileToFormat, yapfFileToAutoFormat].forEach(file => { - if (fs.existsSync(file)) { fs.unlinkSync(file); } - fs.copySync(originalUnformattedFile, file); - }); - - fs.ensureDirSync(path.dirname(autoPep8FileToFormat)); - let yapf = execPythonFile('yapf', [originalUnformattedFile], pythoFilesPath, false); - let autoPep8 = execPythonFile('autopep8', [originalUnformattedFile], pythoFilesPath, false); - return Promise.all([yapf, autoPep8]).then(formattedResults => { - formattedYapf = formattedResults[0]; - formattedAutoPep8 = formattedResults[1]; - }).then(() => { }); - }).then(done).catch(done); - }); - suiteTeardown(done => { - closeActiveWindows().then(done, done); - }); - teardown(done => { - closeActiveWindows().then(done, done); - }); - - function testFormatting(formatter: AutoPep8Formatter | YapfFormatter, formattedContents: string, fileToFormat: string): PromiseLike { - let textEditor: vscode.TextEditor; - let textDocument: vscode.TextDocument; - return vscode.workspace.openTextDocument(fileToFormat).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; - return formatter.formatDocument(textDocument, null, null); - }).then(edits => { - return textEditor.edit(editBuilder => { - edits.forEach(edit => editBuilder.replace(edit.range, edit.newText)); - }); - }).then(edited => { - assert.equal(textEditor.document.getText(), formattedContents, 'Formatted text is not the same'); - }, reason => { - assert.fail(reason, undefined, 'Formatting failed', ''); - }); - } - test('AutoPep8', done => { - testFormatting(new AutoPep8Formatter(ch, pythonSettings, pythoFilesPath), formattedAutoPep8, autoPep8FileToFormat).then(done, done); - }); - - test('Yapf', done => { - testFormatting(new YapfFormatter(ch, pythonSettings, pythoFilesPath), formattedYapf, yapfFileToFormat).then(done, done); - }); - - function testAutoFormatting(formatter: string, formattedContents: string, fileToFormat: string): PromiseLike { - let textDocument: vscode.TextDocument; - pythonSettings.formatting.formatOnSave = true; - pythonSettings.formatting.provider = formatter; - return vscode.workspace.openTextDocument(fileToFormat).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - return editor.edit(editBuilder => { - editBuilder.insert(new vscode.Position(0, 0), '#\n'); - }); - }).then(edited => { - return textDocument.save(); - }).then(saved => { - return new Promise((resolve, reject) => { - setTimeout(() => { - resolve(); - }, 5000); - }); - }).then(() => { - assert.equal(textDocument.getText(), formattedContents, 'Formatted contents are not the same'); - }); - } - test('AutoPep8 autoformat on save', done => { - testAutoFormatting('autopep8', '#\n' + formattedAutoPep8, autoPep8FileToAutoFormat).then(done, done); - }); - - // For some reason doesn't ever work on travis - if (!IS_TRAVIS) { - test('Yapf autoformat on save', done => { - testAutoFormatting('yapf', '#\n' + formattedYapf, yapfFileToAutoFormat).then(done, done); - }); - } -}); \ No newline at end of file diff --git a/src/test/extension.jupyter.comms.jupyterKernelManager.test.ts b/src/test/extension.jupyter.comms.jupyterKernelManager.test.ts deleted file mode 100644 index 2e6e6c3e9a0c..000000000000 --- a/src/test/extension.jupyter.comms.jupyterKernelManager.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -// -// Note: This example test is leveraging the Mocha test framework. -// Please refer to their documentation on https://mochajs.org/ for help. -// -// Place this right on top -import { initialize, IS_TRAVIS, PYTHON_PATH, TEST_TIMEOUT } from './initialize'; -// The module 'assert' provides assertion methods from node -import * as assert from 'assert'; -import * as vscode from 'vscode'; -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it -import { JupyterClientAdapter } from '../client/jupyter/jupyter_client/main'; -import { KernelManagerImpl } from '../client/jupyter/kernel-manager'; -import * as settings from '../client/common/configSettings'; - -let pythonSettings = settings.PythonSettings.getInstance(); - -export class MockOutputChannel implements vscode.OutputChannel { - constructor(name: string) { - this.name = name; - this.output = ''; - this.timeOut = setTimeout(() => { - console.log(this.output); - this.writeToConsole = true; - this.timeOut = null; - }, TEST_TIMEOUT - 1000); - } - private timeOut: number; - name: string; - output: string; - isShown: boolean; - private writeToConsole: boolean; - append(value: string) { - this.output += value; - if (this.writeToConsole) { - console.log(value); - } - } - appendLine(value: string) { - this.append(value); this.append('\n'); - if (this.writeToConsole) { - console.log(value); - console.log('\n'); - } - } - clear() { } - show(preservceFocus?: boolean): void; - show(column?: vscode.ViewColumn, preserveFocus?: boolean): void; - show(x?: any, y?: any): void { - this.isShown = true; - } - hide() { - this.isShown = false; - } - dispose() { - if (this.timeOut) { - clearTimeout(this.timeOut); - this.timeOut = null; - } - } -} - -suite('Kernel Manager', () => { - suiteSetup(done => { - initialize().then(() => { - if (IS_TRAVIS) { - pythonSettings.pythonPath = PYTHON_PATH; - } - done(); - }); - setup(() => { - process.env['PYTHON_DONJAYAMANNE_TEST'] = '0'; - process.env['DEBUG_DJAYAMANNE_IPYTHON'] = '1'; - disposables = []; - output = new MockOutputChannel('Jupyter'); - disposables.push(output); - jupyter = new JupyterClientAdapter(output, __dirname); - disposables.push(jupyter); - // Hack hack hack hack hack :) - cmds.registerCommand = function () { }; - }); - teardown(() => { - process.env['PYTHON_DONJAYAMANNE_TEST'] = '1'; - process.env['DEBUG_DJAYAMANNE_IPYTHON'] = '0'; - output.dispose(); - jupyter.dispose(); - disposables.forEach(d => { - try { - d.dispose(); - } catch (error) { - } - }); - cmds.registerCommand = oldRegisterCommand; - }); - }); - - let output: MockOutputChannel; - let jupyter: JupyterClientAdapter; - let disposables: { dispose: Function }[]; - const cmds = (vscode.commands as any); - const oldRegisterCommand = vscode.commands.registerCommand; - - test('GetAllKernelSpecsFor python', done => { - process.env['PYTHON_DONJAYAMANNE_TEST'] = '0'; - const mgr = new KernelManagerImpl(output, jupyter); - disposables.push(mgr); - mgr.getAllKernelSpecsFor('python').then(specMetadata => { - assert.notEqual(specMetadata.length, 0, 'No spec metatadata'); - done(); - }).catch(reason => { - assert.fail(reason, null, 'Some error', ''); - }); - }); - test('Start a kernel', done => { - const mgr = new KernelManagerImpl(output, jupyter); - disposables.push(mgr); - mgr.getAllKernelSpecsFor('python').then(specMetadata => { - assert.notEqual(specMetadata.length, 0, 'No spec metatadata'); - return mgr.startKernel(specMetadata[0], 'python'); - }).then(kernel => { - assert.equal(typeof kernel === 'object' && kernel !== null, true, 'Kernel instance not returned'); - done(); - }).catch(reason => { - assert.fail(reason, null, 'Some error', ''); - }); - }); - test('Start any kernel for Python', done => { - const mgr = new KernelManagerImpl(output, jupyter); - disposables.push(mgr); - mgr.startKernelFor('python').then(kernel => { - assert.equal(typeof kernel === 'object' && kernel !== null, true, 'Kernel instance not returned'); - done(); - }).catch(reason => { - assert.fail(reason, null, 'Some error', ''); - }); - }); -}); \ No newline at end of file diff --git a/src/test/extension.lint.test.ts b/src/test/extension.lint.test.ts deleted file mode 100644 index e1d8991ae8e8..000000000000 --- a/src/test/extension.lint.test.ts +++ /dev/null @@ -1,264 +0,0 @@ -// -// Note: This example test is leveraging the Mocha test framework. -// Please refer to their documentation on https://mochajs.org/ for help. -// Place this right on top -import { initialize, IS_TRAVIS, PYTHON_PATH, closeActiveWindows } from './initialize'; -// The module \'assert\' provides assertion methods from node -import * as assert from 'assert'; - -// You can import and use all API from the \'vscode\' module -// as well as import your extension to test it -import * as vscode from 'vscode'; -import * as baseLinter from '../client/linters/baseLinter'; -import * as pyLint from '../client/linters/pylint'; -import * as pep8 from '../client/linters/pep8Linter'; -import * as flake8 from '../client/linters/flake8'; -import * as prospector from '../client/linters/prospector'; -import * as pydocstyle from '../client/linters/pydocstyle'; -import * as path from 'path'; -import * as settings from '../client/common/configSettings'; -import * as fs from 'fs-extra'; -import { execPythonFile } from '../client/common/utils'; -let pythonSettings = settings.PythonSettings.getInstance(); - -const pythoFilesPath = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'linting'); -const flake8ConfigPath = path.join(pythoFilesPath, 'flake8config'); -const pep8ConfigPath = path.join(pythoFilesPath, 'pep8config'); -const pydocstyleConfigPath = path.join(pythoFilesPath, 'pydocstyleconfig'); -const pylintConfigPath = path.join(pythoFilesPath, 'pylintconfig'); -const fileToLint = path.join(pythoFilesPath, 'file.py'); -let pylintFileToLintLines: string[] = []; -let isPython3 = true; -class MockOutputChannel implements vscode.OutputChannel { - constructor(name: string) { - this.name = name; - this.output = ''; - } - name: string; - output: string; - append(value: string) { - this.output += value; - } - appendLine(value: string) { this.append(value); this.append('\n'); } - clear() { } - show(preservceFocus?: boolean): void; - show(column?: vscode.ViewColumn, preserveFocus?: boolean): void; - show(x?: any, y?: any): void { } - hide() { } - dispose() { } -} - -let pylintMessagesToBeReturned: baseLinter.ILintMessage[] = [ - { line: 24, column: 0, severity: baseLinter.LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', possibleWord: '', provider: '', type: '' }, - { line: 30, column: 0, severity: baseLinter.LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', possibleWord: '', provider: '', type: '' }, - { line: 34, column: 0, severity: baseLinter.LintMessageSeverity.Information, code: 'I0012', message: 'Locally enabling no-member (E1101)', possibleWord: '', provider: '', type: '' }, - { line: 40, column: 0, severity: baseLinter.LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', possibleWord: '', provider: '', type: '' }, - { line: 44, column: 0, severity: baseLinter.LintMessageSeverity.Information, code: 'I0012', message: 'Locally enabling no-member (E1101)', possibleWord: '', provider: '', type: '' }, - { line: 55, column: 0, severity: baseLinter.LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', possibleWord: '', provider: '', type: '' }, - { line: 59, column: 0, severity: baseLinter.LintMessageSeverity.Information, code: 'I0012', message: 'Locally enabling no-member (E1101)', possibleWord: '', provider: '', type: '' }, - { line: 62, column: 0, severity: baseLinter.LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling undefined-variable (E0602)', possibleWord: '', provider: '', type: '' }, - { line: 70, column: 0, severity: baseLinter.LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', possibleWord: '', provider: '', type: '' }, - { line: 84, column: 0, severity: baseLinter.LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', possibleWord: '', provider: '', type: '' }, - { line: 87, column: 0, severity: baseLinter.LintMessageSeverity.Hint, code: 'C0304', message: 'Final newline missing', possibleWord: '', provider: '', type: '' }, - { line: 11, column: 20, severity: baseLinter.LintMessageSeverity.Warning, code: 'W0613', message: 'Unused argument \'arg\'', possibleWord: '', provider: '', type: '' }, - { line: 26, column: 14, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blop\' member', possibleWord: '', provider: '', type: '' }, - { line: 36, column: 14, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', possibleWord: '', provider: '', type: '' }, - { line: 46, column: 18, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', possibleWord: '', provider: '', type: '' }, - { line: 61, column: 18, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', possibleWord: '', provider: '', type: '' }, - { line: 72, column: 18, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', possibleWord: '', provider: '', type: '' }, - { line: 75, column: 18, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', possibleWord: '', provider: '', type: '' }, - { line: 77, column: 14, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', possibleWord: '', provider: '', type: '' }, - { line: 83, column: 14, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', possibleWord: '', provider: '', type: '' } -]; -let pyLint3MessagesToBeReturned: baseLinter.ILintMessage[] = [ - { line: 13, column: 0, severity: baseLinter.LintMessageSeverity.Error, code: 'E0001', message: 'Missing parentheses in call to \'print\'', possibleWord: '', provider: '', type: '' } -]; -let flake8MessagesToBeReturned: baseLinter.ILintMessage[] = [ - { line: 5, column: 1, severity: baseLinter.LintMessageSeverity.Information, code: 'E302', message: 'expected 2 blank lines, found 1', possibleWord: '', provider: '', type: '' }, - { line: 19, column: 15, severity: baseLinter.LintMessageSeverity.Information, code: 'E127', message: 'continuation line over-indented for visual indent', possibleWord: '', provider: '', type: '' }, - { line: 24, column: 23, severity: baseLinter.LintMessageSeverity.Information, code: 'E261', message: 'at least two spaces before inline comment', possibleWord: '', provider: '', type: '' }, - { line: 62, column: 30, severity: baseLinter.LintMessageSeverity.Information, code: 'E261', message: 'at least two spaces before inline comment', possibleWord: '', provider: '', type: '' }, - { line: 70, column: 22, severity: baseLinter.LintMessageSeverity.Information, code: 'E261', message: 'at least two spaces before inline comment', possibleWord: '', provider: '', type: '' }, - { line: 80, column: 5, severity: baseLinter.LintMessageSeverity.Information, code: 'E303', message: 'too many blank lines (2)', possibleWord: '', provider: '', type: '' }, - { line: 87, column: 24, severity: baseLinter.LintMessageSeverity.Information, code: 'W292', message: 'no newline at end of file', possibleWord: '', provider: '', type: '' } -]; -let pep8MessagesToBeReturned: baseLinter.ILintMessage[] = [ - { line: 5, column: 1, severity: baseLinter.LintMessageSeverity.Information, code: 'E302', message: 'expected 2 blank lines, found 1', possibleWord: '', provider: '', type: '' }, - { line: 19, column: 15, severity: baseLinter.LintMessageSeverity.Information, code: 'E127', message: 'continuation line over-indented for visual indent', possibleWord: '', provider: '', type: '' }, - { line: 24, column: 23, severity: baseLinter.LintMessageSeverity.Information, code: 'E261', message: 'at least two spaces before inline comment', possibleWord: '', provider: '', type: '' }, - { line: 62, column: 30, severity: baseLinter.LintMessageSeverity.Information, code: 'E261', message: 'at least two spaces before inline comment', possibleWord: '', provider: '', type: '' }, - { line: 70, column: 22, severity: baseLinter.LintMessageSeverity.Information, code: 'E261', message: 'at least two spaces before inline comment', possibleWord: '', provider: '', type: '' }, - { line: 80, column: 5, severity: baseLinter.LintMessageSeverity.Information, code: 'E303', message: 'too many blank lines (2)', possibleWord: '', provider: '', type: '' }, - { line: 87, column: 24, severity: baseLinter.LintMessageSeverity.Information, code: 'W292', message: 'no newline at end of file', possibleWord: '', provider: '', type: '' } -]; -let pydocstyleMessagseToBeReturned: baseLinter.ILintMessage[] = [ - { 'code': 'D400', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First line should end with a period (not \'e\')', 'column': 0, 'line': 1, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D400', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First line should end with a period (not \'t\')', 'column': 0, 'line': 5, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D102', severity: baseLinter.LintMessageSeverity.Information, 'message': 'Missing docstring in public method', 'column': 4, 'line': 8, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D401', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First line should be in imperative mood (\'thi\', not \'this\')', 'column': 4, 'line': 11, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D403', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First word of the first line should be properly capitalized (\'This\', not \'this\')', 'column': 4, 'line': 11, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D400', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First line should end with a period (not \'e\')', 'column': 4, 'line': 11, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D403', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First word of the first line should be properly capitalized (\'And\', not \'and\')', 'column': 4, 'line': 15, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D400', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First line should end with a period (not \'t\')', 'column': 4, 'line': 15, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D403', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', 'column': 4, 'line': 21, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D400', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First line should end with a period (not \'g\')', 'column': 4, 'line': 21, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D403', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', 'column': 4, 'line': 28, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D400', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First line should end with a period (not \'g\')', 'column': 4, 'line': 28, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D403', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', 'column': 4, 'line': 38, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D400', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First line should end with a period (not \'g\')', 'column': 4, 'line': 38, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D403', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', 'column': 4, 'line': 53, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D400', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First line should end with a period (not \'g\')', 'column': 4, 'line': 53, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D403', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', 'column': 4, 'line': 68, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D400', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First line should end with a period (not \'g\')', 'column': 4, 'line': 68, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D403', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', 'column': 4, 'line': 80, 'type': '', 'provider': 'pydocstyle' }, - { 'code': 'D400', severity: baseLinter.LintMessageSeverity.Information, 'message': 'First line should end with a period (not \'g\')', 'column': 4, 'line': 80, 'type': '', 'provider': 'pydocstyle' } -]; - -let filteredPylintMessagesToBeReturned: baseLinter.ILintMessage[] = [ - { line: 26, column: 14, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blop\' member', possibleWord: '', provider: '', type: '' }, - { line: 36, column: 14, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', possibleWord: '', provider: '', type: '' }, - { line: 46, column: 18, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', possibleWord: '', provider: '', type: '' }, - { line: 61, column: 18, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', possibleWord: '', provider: '', type: '' }, - { line: 72, column: 18, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', possibleWord: '', provider: '', type: '' }, - { line: 75, column: 18, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', possibleWord: '', provider: '', type: '' }, - { line: 77, column: 14, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', possibleWord: '', provider: '', type: '' }, - { line: 83, column: 14, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', possibleWord: '', provider: '', type: '' } -]; -let filteredPylint3MessagesToBeReturned: baseLinter.ILintMessage[] = [ -]; -let filteredFlake8MessagesToBeReturned: baseLinter.ILintMessage[] = [ - { line: 87, column: 24, severity: baseLinter.LintMessageSeverity.Information, code: 'W292', message: 'no newline at end of file', possibleWord: '', provider: '', type: '' } -]; -let filteredPep88MessagesToBeReturned: baseLinter.ILintMessage[] = [ - { line: 87, column: 24, severity: baseLinter.LintMessageSeverity.Information, code: 'W292', message: 'no newline at end of file', possibleWord: '', provider: '', type: '' } -]; -let fiteredPydocstyleMessagseToBeReturned: baseLinter.ILintMessage[] = [ - { 'code': 'D102', severity: baseLinter.LintMessageSeverity.Information, 'message': 'Missing docstring in public method', 'column': 4, 'line': 8, 'type': '', 'provider': 'pydocstyle' } -]; - -suite('Linting', () => { - suiteSetup(done => { - pylintFileToLintLines = fs.readFileSync(fileToLint).toString('utf-8').split(/\r?\n/g); - pythonSettings.pythonPath = PYTHON_PATH; - initialize().then(() => { - return execPythonFile(pythonSettings.pythonPath, ['--version'], __dirname, true); - }).then(version => { - isPython3 = version.indexOf('3.') >= 0; - }).then(done, done); - }); - setup(() => { - pythonSettings.linting.enabled = true; - pythonSettings.linting.pylintEnabled = true; - pythonSettings.linting.flake8Enabled = true; - pythonSettings.linting.pep8Enabled = true; - pythonSettings.linting.prospectorEnabled = true; - pythonSettings.linting.pydocstyleEnabled = true; - }); - suiteTeardown(done => { - closeActiveWindows().then(done, done); - }); - teardown(done => { - closeActiveWindows().then(done, done); - }); - - function testEnablingDisablingOfLinter(linter: baseLinter.BaseLinter, propertyName: string) { - pythonSettings.linting[propertyName] = true; - assert.equal(true, linter.isEnabled()); - - pythonSettings.linting[propertyName] = false; - assert.equal(false, linter.isEnabled()); - } - test('Enable and Disable Pylint', () => { - let ch = new MockOutputChannel('Lint'); - testEnablingDisablingOfLinter(new pyLint.Linter(ch, pythoFilesPath), 'pylintEnabled'); - }); - test('Enable and Disable Pep8', () => { - let ch = new MockOutputChannel('Lint'); - testEnablingDisablingOfLinter(new pep8.Linter(ch, pythoFilesPath), 'pep8Enabled'); - }); - test('Enable and Disable Flake8', () => { - let ch = new MockOutputChannel('Lint'); - testEnablingDisablingOfLinter(new flake8.Linter(ch, pythoFilesPath), 'flake8Enabled'); - }); - test('Enable and Disable Prospector', () => { - let ch = new MockOutputChannel('Lint'); - testEnablingDisablingOfLinter(new prospector.Linter(ch, pythoFilesPath), 'prospectorEnabled'); - }); - test('Enable and Disable Pydocstyle', () => { - let ch = new MockOutputChannel('Lint'); - testEnablingDisablingOfLinter(new pydocstyle.Linter(ch, pythoFilesPath), 'pydocstyleEnabled'); - }); - - function testLinterMessages(linter: baseLinter.BaseLinter, outputChannel: MockOutputChannel, pythonFile: string, messagesToBeReceived: baseLinter.ILintMessage[]): Thenable { - return vscode.workspace.openTextDocument(pythonFile) - .then(document => vscode.window.showTextDocument(document)) - .then(editor => linter.runLinter(editor.document)) - .then(messages => { - // Different versions of python return different errors, - if (messagesToBeReceived.length === 0) { - assert.equal(messages.length, 0, 'No errors in linter, Output - ' + outputChannel.output); - } - else { - if (outputChannel.output.indexOf('ENOENT') === -1) { - // Pylint for Python Version 2.7 could return 80 linter messages, where as in 3.5 it might only return 1 - // Looks like pylint stops linting as soon as it comes across any ERRORS - assert.notEqual(messages.length, 0, 'No errors in linter, Output - ' + outputChannel.output); - } - else { - assert.ok('Linter not installed', 'Linter not installed'); - } - } - // messagesToBeReceived.forEach(msg => { - // let similarMessages = messages.filter(m => m.code === msg.code && m.column === msg.column && - // m.line === msg.line && m.message === msg.message && m.severity === msg.severity); - // assert.equal(true, similarMessages.length > 0, 'Error not found, ' + JSON.stringify(msg) + '\n, Output - ' + outputChannel.output); - // }); - }, error => { - assert.fail(error, null, 'Linter error, Output - ' + outputChannel.output, ''); - }); - } - test('PyLint', done => { - let ch = new MockOutputChannel('Lint'); - let linter = new pyLint.Linter(ch, pythoFilesPath); - return testLinterMessages(linter, ch, fileToLint, pylintMessagesToBeReturned).then(done, done); - }); - test('Flake8', done => { - let ch = new MockOutputChannel('Lint'); - let linter = new flake8.Linter(ch, pythoFilesPath); - return testLinterMessages(linter, ch, fileToLint, flake8MessagesToBeReturned).then(done, done); - }); - test('Pep8', done => { - let ch = new MockOutputChannel('Lint'); - let linter = new pep8.Linter(ch, pythoFilesPath); - return testLinterMessages(linter, ch, fileToLint, pep8MessagesToBeReturned).then(done, done); - }); - test('Pydocstyle', done => { - let ch = new MockOutputChannel('Lint'); - let linter = new pydocstyle.Linter(ch, pythoFilesPath); - return testLinterMessages(linter, ch, fileToLint, pydocstyleMessagseToBeReturned).then(done, done); - }); - // Version dependenant, will be enabled once we have fixed this - // TODO: Check version of python running and accordingly change the values - if (!IS_TRAVIS) { - test('PyLint with config in root', done => { - let ch = new MockOutputChannel('Lint'); - let linter = new pyLint.Linter(ch, pylintConfigPath); - return testLinterMessages(linter, ch, path.join(pylintConfigPath, 'file.py'), filteredPylintMessagesToBeReturned).then(done, done); - }); - } - test('Flake8 with config in root', done => { - let ch = new MockOutputChannel('Lint'); - let linter = new flake8.Linter(ch, flake8ConfigPath); - return testLinterMessages(linter, ch, path.join(flake8ConfigPath, 'file.py'), filteredFlake8MessagesToBeReturned).then(done, done); - }); - test('Pep8 with config in root', done => { - let ch = new MockOutputChannel('Lint'); - let linter = new pep8.Linter(ch, pep8ConfigPath); - return testLinterMessages(linter, ch, path.join(pep8ConfigPath, 'file.py'), filteredPep88MessagesToBeReturned).then(done, done); - }); - test('Pydocstyle with config in root', done => { - let ch = new MockOutputChannel('Lint'); - let linter = new pydocstyle.Linter(ch, pydocstyleConfigPath); - return testLinterMessages(linter, ch, path.join(pydocstyleConfigPath, 'file.py'), fiteredPydocstyleMessagseToBeReturned).then(done, done); - }); -}); \ No newline at end of file diff --git a/src/test/extension.sort.test.ts b/src/test/extension.sort.test.ts deleted file mode 100644 index 4f8730ddd099..000000000000 --- a/src/test/extension.sort.test.ts +++ /dev/null @@ -1,167 +0,0 @@ - -// Note: This example test is leveraging the Mocha test framework. -// Please refer to their documentation on https://mochajs.org/ for help. - - -// Place this right on top -import { initialize, PYTHON_PATH, IS_TRAVIS, closeActiveWindows } from './initialize'; -// The module 'assert' provides assertion methods from node -import * as assert from 'assert'; - -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it -import * as vscode from 'vscode'; -import { PythonImportSortProvider } from '../client/providers/importSortProvider'; -import * as path from 'path'; -import * as settings from '../client/common/configSettings'; -import * as fs from 'fs'; -import { EOL } from 'os'; - -const pythonSettings = settings.PythonSettings.getInstance(); -const fileToFormatWithoutConfig = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'sorting', 'noconfig', 'before.py'); -const originalFileToFormatWithoutConfig = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'sorting', 'noconfig', 'original.py'); -const fileToFormatWithConfig = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'sorting', 'withconfig', 'before.py'); -const originalFileToFormatWithConfig = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'sorting', 'withconfig', 'original.py'); -const fileToFormatWithConfig1 = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'sorting', 'withconfig', 'before.1.py'); -const originalFileToFormatWithConfig1 = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'sorting', 'withconfig', 'original.1.py'); -const extensionDir = path.join(__dirname, '..', '..'); - -suite('Sorting', () => { - suiteSetup(done => { - initialize().then(() => { - pythonSettings.pythonPath = PYTHON_PATH; - }).then(done, done); - }); - suiteTeardown(done => { - fs.writeFileSync(fileToFormatWithConfig, fs.readFileSync(originalFileToFormatWithConfig)); - fs.writeFileSync(fileToFormatWithConfig1, fs.readFileSync(originalFileToFormatWithConfig1)); - fs.writeFileSync(fileToFormatWithoutConfig, fs.readFileSync(originalFileToFormatWithoutConfig)); - closeActiveWindows().then(done).catch(done); - }); - setup(done => { - pythonSettings.sortImports.args = []; - fs.writeFileSync(fileToFormatWithConfig, fs.readFileSync(originalFileToFormatWithConfig)); - fs.writeFileSync(fileToFormatWithoutConfig, fs.readFileSync(originalFileToFormatWithoutConfig)); - fs.writeFileSync(fileToFormatWithConfig1, fs.readFileSync(originalFileToFormatWithConfig1)); - closeActiveWindows().then(done).catch(done); - }); - - test('Without Config', done => { - let textEditor: vscode.TextEditor; - let textDocument: vscode.TextDocument; - return vscode.workspace.openTextDocument(fileToFormatWithoutConfig).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - textEditor = editor; - assert(vscode.window.activeTextEditor, 'No active editor'); - const sorter = new PythonImportSortProvider(); - return sorter.sortImports(extensionDir, textDocument); - }).then(edits => { - assert.equal(edits.filter(value => value.newText === EOL && value.range.isEqual(new vscode.Range(2, 0, 2, 0))).length, 1, 'EOL not found'); - assert.equal(edits.filter(value => value.newText === '' && value.range.isEqual(new vscode.Range(3, 0, 4, 0))).length, 1, '"" not found'); - assert.equal(edits.filter(value => value.newText === `from rope.base import libutils${EOL}from rope.refactor.extract import ExtractMethod, ExtractVariable${EOL}from rope.refactor.rename import Rename${EOL}` && value.range.isEqual(new vscode.Range(6, 0, 6, 0))).length, 1, 'Text not found'); - assert.equal(edits.filter(value => value.newText === '' && value.range.isEqual(new vscode.Range(13, 0, 18, 0))).length, 1, '"" not found'); - }).then(done, done); - }); - - test('Without Config (via Command)', done => { - let textEditor: vscode.TextEditor; - let textDocument: vscode.TextDocument; - let originalContent = ''; - return vscode.workspace.openTextDocument(fileToFormatWithoutConfig).then(document => { - textDocument = document; - originalContent = textDocument.getText(); - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; - return vscode.commands.executeCommand('python.sortImports'); - }).then(() => { - assert.notEqual(originalContent, textDocument.getText(), 'Contents have not changed'); - }).then(done, done); - }); - - test('With Config', done => { - let textEditor: vscode.TextEditor; - let textDocument: vscode.TextDocument; - return vscode.workspace.openTextDocument(fileToFormatWithConfig).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; - const sorter = new PythonImportSortProvider(); - return sorter.sortImports(extensionDir, textDocument); - }).then(edits => { - const newValue = `from third_party import lib2${EOL}from third_party import lib3${EOL}from third_party import lib4${EOL}from third_party import lib5${EOL}from third_party import lib6${EOL}from third_party import lib7${EOL}from third_party import lib8${EOL}from third_party import lib9${EOL}`; - assert.equal(edits.filter(value => value.newText === newValue && value.range.isEqual(new vscode.Range(0, 0, 3, 0))).length, 1, 'New Text not found'); - }).then(done, done); - }); - - test('With Config (via Command)', done => { - let textEditor: vscode.TextEditor; - let textDocument: vscode.TextDocument; - let originalContent = ''; - return vscode.workspace.openTextDocument(fileToFormatWithConfig).then(document => { - textDocument = document; - originalContent = document.getText(); - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; - return vscode.commands.executeCommand('python.sortImports'); - }).then(() => { - assert.notEqual(originalContent, textDocument.getText(), 'Contents have not changed'); - }).then(done, done); - }); - - // Doesn't always work on Travis !?! - if (!IS_TRAVIS) { - test('With Changes and Config in Args', done => { - let textEditor: vscode.TextEditor; - let textDocument: vscode.TextDocument; - pythonSettings.sortImports.args = ['-sp', path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'sorting', 'withconfig')]; - return vscode.workspace.openTextDocument(fileToFormatWithConfig).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; - return editor.edit(editor => { - editor.insert(new vscode.Position(0, 0), 'from third_party import lib0' + EOL); - }); - }).then(() => { - const sorter = new PythonImportSortProvider(); - return sorter.sortImports(extensionDir, textDocument); - }).then(edits => { - const newValue = `from third_party import lib1${EOL}from third_party import lib2${EOL}from third_party import lib3${EOL}from third_party import lib4${EOL}from third_party import lib5${EOL}from third_party import lib6${EOL}from third_party import lib7${EOL}from third_party import lib8${EOL}from third_party import lib9${EOL}`; - assert.equal(edits.length, 1, 'Incorrect number of edits'); - assert.equal(edits[0].newText, newValue, 'New Value is not the same'); - assert.equal(`${edits[0].range.start.line},${edits[0].range.start.character}`, '1,0', 'Start position is not the same'); - assert.equal(`${edits[0].range.end.line},${edits[0].range.end.character}`, '2,0', 'End position is not the same'); - }).then(done, done); - }); - } - test('With Changes and Config in Args (via Command)', done => { - let textEditor: vscode.TextEditor; - let textDocument: vscode.TextDocument; - let originalContent = ''; - pythonSettings.sortImports.args = ['-sp', path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'sorting', 'withconfig')]; - return vscode.workspace.openTextDocument(fileToFormatWithConfig).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; - return editor.edit(editor => { - editor.insert(new vscode.Position(0, 0), 'from third_party import lib0' + EOL); - }); - }).then(() => { - originalContent = textDocument.getText(); - return vscode.commands.executeCommand('python.sortImports'); - }).then(edits => { - assert.notEqual(originalContent, textDocument.getText(), 'Contents have not changed'); - }).then(done, done); - }); -}); \ No newline at end of file diff --git a/src/test/extension.unittests.nosetest.test.ts b/src/test/extension.unittests.nosetest.test.ts deleted file mode 100644 index 6cf6a5d2445b..000000000000 --- a/src/test/extension.unittests.nosetest.test.ts +++ /dev/null @@ -1,203 +0,0 @@ -/// -// -// Note: This example test is leveraging the Mocha test framework. -// Please refer to their documentation on https://mochajs.org/ for help. -// -// Place this right on top -import { initialize, PYTHON_PATH } from './initialize'; - -// The module \'assert\' provides assertion methods from node -import * as assert from 'assert'; - -// You can import and use all API from the \'vscode\' module -// as well as import your extension to test it -import * as vscode from 'vscode'; -import { TestsToRun } from '../client/unittests/common/contracts'; -import * as nose from '../client/unittests/nosetest/main'; -import { TestResultDisplay } from '../client/unittests/display/main'; -import * as fs from 'fs'; - -import * as path from 'path'; -import * as configSettings from '../client/common/configSettings'; - -let pythonSettings = configSettings.PythonSettings.getInstance(); - -const UNITTEST_TEST_FILES_PATH = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'standard'); -const UNITTEST_SINGLE_TEST_FILE_PATH = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'single'); -const UNITTEST_TEST_ID_FILE_PATH = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'standard', '.noseids'); -class MockOutputChannel implements vscode.OutputChannel { - constructor(name: string) { - this.name = name; - this.output = ''; - } - name: string; - output: string; - append(value: string) { - this.output += value; - } - appendLine(value: string) { this.append(value); this.append('\n'); } - clear() { } - show(preservceFocus?: boolean): void; - show(column?: vscode.ViewColumn, preserveFocus?: boolean): void; - show(x?: any, y?: any): void { } - hide() { } - dispose() { } -} - -suite('Unit Tests (nosetest)', () => { - suiteSetup(done => { - if (fs.existsSync(UNITTEST_TEST_ID_FILE_PATH)) { - fs.unlinkSync(UNITTEST_TEST_ID_FILE_PATH); - } - initialize().then(() => { - pythonSettings.pythonPath = PYTHON_PATH; - done(); - }); - }); - suiteTeardown(done => { - if (fs.existsSync(UNITTEST_TEST_ID_FILE_PATH)) { - fs.unlinkSync(UNITTEST_TEST_ID_FILE_PATH); - } - done(); - }); - setup(() => { - outChannel = new MockOutputChannel('Python Test Log'); - testResultDisplay = new TestResultDisplay(outChannel); - }); - teardown(() => { - outChannel.dispose(); - testManager.dispose(); - testResultDisplay.dispose(); - }); - function createTestManager() { - testManager = new nose.TestManager(rootDirectory, outChannel); - } - const rootDirectory = UNITTEST_TEST_FILES_PATH; - let testManager: nose.TestManager; - let testResultDisplay: TestResultDisplay; - let outChannel: vscode.OutputChannel; - - test('Discover Tests (single test file)', done => { - pythonSettings.unitTest.nosetestArgs = [ - ]; - testManager = new nose.TestManager(UNITTEST_SINGLE_TEST_FILE_PATH, outChannel); - testManager.discoverTests(true, true).then(tests => { - assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 2, 'Incorrect number of test suites'); - assert.equal(tests.testFiles.some(t => t.name === 'tests/test_one.py' && t.nameToRun === t.name), true, 'Test File not found'); - }).then(done).catch(done); - }); - - test('Discover Tests (pattern = test_)', done => { - pythonSettings.unitTest.nosetestArgs = [ - - ]; - createTestManager(); - testManager.discoverTests(true, true).then(tests => { - assert.equal(tests.testFiles.length, 6, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 22, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 6, 'Incorrect number of test suites'); - assert.equal(tests.testFiles.some(t => t.name === 'tests/test_unittest_one.py' && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'tests/test_unittest_two.py' && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'tests/test_pytest.py' && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'tests/test_another_pytest.py' && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'tests/unittest_three_test.py' && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'test_root.py' && t.nameToRun === t.name), true, 'Test File not found'); - }).then(done).catch(done); - }); - - test('Discover Tests (pattern = _test_)', done => { - pythonSettings.unitTest.nosetestArgs = [ - '-m=*test*' - ]; - createTestManager(); - testManager.discoverTests(true, true).then(tests => { - assert.equal(tests.testFiles.length, 6, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 18, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 5, 'Incorrect number of test suites'); - assert.equal(tests.testFiles.some(t => t.name === 'tests/test_unittest_one.py' && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'tests/test_unittest_two.py' && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'tests/test_pytest.py' && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'tests/test_another_pytest.py' && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'tests/unittest_three_test.py' && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'test_root.py' && t.nameToRun === t.name), true, 'Test File not found'); - }).then(done).catch(done); - }); - - test('Run Tests', done => { - pythonSettings.unitTest.nosetestArgs = [ - ]; - createTestManager(); - testManager.runTest().then(results => { - assert.equal(results.summary.errors, 5, 'Errors'); - assert.equal(results.summary.failures, 6, 'Failures'); - assert.equal(results.summary.passed, 8, 'Passed'); - assert.equal(results.summary.skipped, 3, 'skipped'); - }).then(done).catch(done); - }); - - test('Run Failed Tests', done => { - pythonSettings.unitTest.nosetestArgs = [ - ]; - createTestManager(); - testManager.runTest().then(results => { - assert.equal(results.summary.errors, 5, 'Errors'); - assert.equal(results.summary.failures, 6, 'Failures'); - assert.equal(results.summary.passed, 8, 'Passed'); - assert.equal(results.summary.skipped, 3, 'skipped'); - - return testManager.runTest(true).then(tests => { - assert.equal(results.summary.errors, 5, 'Errors again'); - assert.equal(results.summary.failures, 6, 'Failures again'); - assert.equal(results.summary.passed, 0, 'Passed again'); - assert.equal(results.summary.skipped, 0, 'skipped again'); - }); - }).then(done).catch(done); - }); - - test('Run Specific Test File', done => { - pythonSettings.unitTest.nosetestArgs = [ - ]; - createTestManager(); - testManager.discoverTests(true, true).then(tests => { - const testFile: TestsToRun = { testFile: [tests.testFiles[0]], testFolder: [], testFunction: [], testSuite: [] }; - return testManager.runTest(testFile).then(tests => { - assert.equal(tests.summary.errors, 0, 'Errors'); - assert.equal(tests.summary.failures, 1, 'Failures'); - assert.equal(tests.summary.passed, 1, 'Passed'); - assert.equal(tests.summary.skipped, 1, 'skipped'); - }); - }).then(done).catch(done); - }); - - test('Run Specific Test Suite', done => { - pythonSettings.unitTest.nosetestArgs = [ - ]; - createTestManager(); - testManager.discoverTests(true, true).then(tests => { - const testSuite: TestsToRun = { testFile: [], testFolder: [], testFunction: [], testSuite: [tests.testSuits[0].testSuite] }; - return testManager.runTest(testSuite).then(tests => { - assert.equal(tests.summary.errors, 1, 'Errors'); - assert.equal(tests.summary.failures, 0, 'Failures'); - assert.equal(tests.summary.passed, 0, 'Passed'); - assert.equal(tests.summary.skipped, 0, 'skipped'); - }); - }).then(done).catch(done); - }); - - test('Run Specific Test Function', done => { - pythonSettings.unitTest.nosetestArgs = [ - ]; - createTestManager(); - testManager.discoverTests(true, true).then(tests => { - const testFn: TestsToRun = { testFile: [], testFolder: [], testFunction: [tests.testFunctions[0].testFunction], testSuite: [] }; - return testManager.runTest(testFn).then(tests => { - assert.equal(tests.summary.errors, 0, 'Errors'); - assert.equal(tests.summary.failures, 1, 'Failures'); - assert.equal(tests.summary.passed, 0, 'Passed'); - assert.equal(tests.summary.skipped, 0, 'skipped'); - }); - }).then(done).catch(done); - }); -}); \ No newline at end of file diff --git a/src/test/extension.unittests.pytest.test.ts b/src/test/extension.unittests.pytest.test.ts deleted file mode 100644 index 2cb126c959c2..000000000000 --- a/src/test/extension.unittests.pytest.test.ts +++ /dev/null @@ -1,223 +0,0 @@ -/// -// -// Note: This example test is leveraging the Mocha test framework. -// Please refer to their documentation on https://mochajs.org/ for help. -// -// Place this right on top -import { initialize, PYTHON_PATH } from './initialize'; - -// The module \'assert\' provides assertion methods from node -import * as assert from 'assert'; - -// You can import and use all API from the \'vscode\' module -// as well as import your extension to test it -import * as vscode from 'vscode'; -import { TestsToRun, TestFile, TestFunction, TestSuite } from '../client/unittests/common/contracts'; -import * as pytest from '../client/unittests/pytest/main'; -import { TestResultDisplay } from '../client/unittests/display/main'; - - -import * as path from 'path'; -import * as configSettings from '../client/common/configSettings'; - -let pythonSettings = configSettings.PythonSettings.getInstance(); - -const UNITTEST_TEST_FILES_PATH = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'standard'); -const UNITTEST_SINGLE_TEST_FILE_PATH = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'single'); -const UNITTEST_TEST_FILES_PATH_WITH_CONFIGS = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'unitestsWithConfigs'); - -class MockOutputChannel implements vscode.OutputChannel { - constructor(name: string) { - this.name = name; - this.output = ''; - } - name: string; - output: string; - append(value: string) { - this.output += value; - } - appendLine(value: string) { this.append(value); this.append('\n'); } - clear() { } - show(preservceFocus?: boolean): void; - show(column?: vscode.ViewColumn, preserveFocus?: boolean): void; - show(x?: any, y?: any): void { } - hide() { } - dispose() { } -} - -suite('Unit Tests (PyTest)', () => { - suiteSetup(done => { - initialize().then(() => { - pythonSettings.pythonPath = PYTHON_PATH; - done(); - }); - }); - suiteTeardown(done => { - done(); - }); - setup(() => { - rootDirectory = UNITTEST_TEST_FILES_PATH; - outChannel = new MockOutputChannel('Python Test Log'); - testResultDisplay = new TestResultDisplay(outChannel); - }); - teardown(() => { - outChannel.dispose(); - testManager.dispose(); - testResultDisplay.dispose(); - }); - function createTestManager() { - testManager = new pytest.TestManager(rootDirectory, outChannel); - } - let rootDirectory = UNITTEST_TEST_FILES_PATH; - let testManager: pytest.TestManager; - let testResultDisplay: TestResultDisplay; - let outChannel: vscode.OutputChannel; - - test('Discover Tests (single test file)', done => { - pythonSettings.unitTest.nosetestArgs = [ - ]; - testManager = new pytest.TestManager(UNITTEST_SINGLE_TEST_FILE_PATH, outChannel); - testManager.discoverTests(true, true).then(tests => { - assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 2, 'Incorrect number of test suites'); - assert.equal(tests.testFiles.some(t => t.name === 'tests/test_one.py' && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'test_root.py' && t.nameToRun === t.name), true, 'Test File not found'); - }).then(done).catch(done); - }); - - test('Discover Tests (pattern = test_)', done => { - pythonSettings.unitTest.pyTestArgs = [ - '-k=test_' - ]; - createTestManager(); - testManager.discoverTests(true, true).then(tests => { - assert.equal(tests.testFiles.length, 6, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 29, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 8, 'Incorrect number of test suites'); - assert.equal(tests.testFiles.some(t => t.name === 'tests/test_unittest_one.py' && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'tests/test_unittest_two.py' && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'tests/unittest_three_test.py' && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'tests/test_pytest.py' && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'tests/test_another_pytest.py' && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'test_root.py' && t.nameToRun === t.name), true, 'Test File not found'); - }).then(done).catch(done); - }); - - test('Discover Tests (pattern = _test)', done => { - pythonSettings.unitTest.pyTestArgs = [ - '-k=_test.py' - ]; - createTestManager(); - testManager.discoverTests(true, true).then(tests => { - assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 1, 'Incorrect number of test suites'); - assert.equal(tests.testFiles.some(t => t.name === 'tests/unittest_three_test.py' && t.nameToRun === t.name), true, 'Test File not found'); - }).then(done).catch(done); - }); - - - test('Discover Tests (with config)', done => { - pythonSettings.unitTest.pyTestArgs = []; - rootDirectory = UNITTEST_TEST_FILES_PATH_WITH_CONFIGS; - createTestManager(); - testManager.discoverTests(true, true).then(tests => { - assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 14, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 4, 'Incorrect number of test suites'); - assert.equal(tests.testFiles.some(t => t.name === 'other/test_unittest_one.py' && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'other/test_pytest.py' && t.nameToRun === t.name), true, 'Test File not found'); - }).then(done).catch(done); - }); - - test('Run Tests', done => { - pythonSettings.unitTest.pyTestArgs = [ - '-k=test_' - ]; - createTestManager(); - testManager.runTest().then(results => { - assert.equal(results.summary.errors, 0, 'Errors'); - assert.equal(results.summary.failures, 9, 'Failures'); - assert.equal(results.summary.passed, 17, 'Passed'); - assert.equal(results.summary.skipped, 3, 'skipped'); - }).then(done).catch(done); - }); - - test('Run Failed Tests', done => { - pythonSettings.unitTest.pyTestArgs = [ - '-k=test_' - ]; - createTestManager(); - testManager.runTest().then(results => { - assert.equal(results.summary.errors, 0, 'Errors'); - assert.equal(results.summary.failures, 9, 'Failures'); - assert.equal(results.summary.passed, 17, 'Passed'); - assert.equal(results.summary.skipped, 3, 'skipped'); - - return testManager.runTest(true).then(tests => { - assert.equal(results.summary.errors, 0, 'Failed Errors'); - assert.equal(results.summary.failures, 9, 'Failed Failures'); - assert.equal(results.summary.passed, 0, 'Failed Passed'); - assert.equal(results.summary.skipped, 0, 'Failed skipped'); - }); - }).then(done).catch(done); - }); - - test('Run Specific Test File', done => { - pythonSettings.unitTest.pyTestArgs = [ - '-k=test_' - ]; - createTestManager(); - testManager.discoverTests(true, true).then(tests => { - const testFile: TestFile = { - fullPath: path.join(rootDirectory, 'tests', 'test_another_pytest.py'), - name: 'tests/test_another_pytest.py', - nameToRun: 'tests/test_another_pytest.py', - xmlName: 'tests/test_another_pytest.py', - functions: [], - suites: [], - time: 0 - }; - const testFileToRun: TestsToRun = { testFile: [testFile], testFolder: [], testFunction: [], testSuite: [] }; - return testManager.runTest(testFileToRun).then(tests => { - assert.equal(tests.summary.errors, 0, 'Errors'); - assert.equal(tests.summary.failures, 1, 'Failures'); - assert.equal(tests.summary.passed, 3, 'Passed'); - assert.equal(tests.summary.skipped, 0, 'skipped'); - }); - }).then(done).catch(done); - }); - - test('Run Specific Test Suite', done => { - pythonSettings.unitTest.pyTestArgs = [ - '-k=test_' - ]; - createTestManager(); - testManager.discoverTests(true, true).then(tests => { - const testSuite: TestsToRun = { testFile: [], testFolder: [], testFunction: [], testSuite: [tests.testSuits[0].testSuite] }; - return testManager.runTest(testSuite).then(tests => { - assert.equal(tests.summary.errors, 0, 'Errors'); - assert.equal(tests.summary.failures, 1, 'Failures'); - assert.equal(tests.summary.passed, 1, 'Passed'); - assert.equal(tests.summary.skipped, 1, 'skipped'); - }); - }).then(done).catch(done); - }); - - test('Run Specific Test Function', done => { - pythonSettings.unitTest.pyTestArgs = [ - '-k=test_' - ]; - createTestManager(); - testManager.discoverTests(true, true).then(tests => { - const testFn: TestsToRun = { testFile: [], testFolder: [], testFunction: [tests.testFunctions[0].testFunction], testSuite: [] }; - return testManager.runTest(testFn).then(tests => { - assert.equal(tests.summary.errors, 0, 'Errors'); - assert.equal(tests.summary.failures, 1, 'Failures'); - assert.equal(tests.summary.passed, 0, 'Passed'); - assert.equal(tests.summary.skipped, 0, 'skipped'); - }); - }).then(done).catch(done); - }); -}); \ No newline at end of file diff --git a/src/test/extension.unittests.unittest.test.ts b/src/test/extension.unittests.unittest.test.ts deleted file mode 100644 index ad7194571d7c..000000000000 --- a/src/test/extension.unittests.unittest.test.ts +++ /dev/null @@ -1,217 +0,0 @@ -/// -// -// Note: This example test is leveraging the Mocha test framework. -// Please refer to their documentation on https://mochajs.org/ for help. -// -// Place this right on top -import { initialize, PYTHON_PATH } from './initialize'; - -// The module \'assert\' provides assertion methods from node -import * as assert from 'assert'; - -// You can import and use all API from the \'vscode\' module -// as well as import your extension to test it -import * as vscode from 'vscode'; -import { TestsToRun } from '../client/unittests/common/contracts'; -import * as unittest from '../client/unittests/unittest/main'; -import { TestResultDisplay } from '../client/unittests/display/main'; - - -import * as path from 'path'; -import * as configSettings from '../client/common/configSettings'; - -let pythonSettings = configSettings.PythonSettings.getInstance(); - -const UNITTEST_TEST_FILES_PATH = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'standard'); -const UNITTEST_SINGLE_TEST_FILE_PATH = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'single'); -class MockOutputChannel implements vscode.OutputChannel { - constructor(name: string) { - this.name = name; - this.output = ''; - } - name: string; - output: string; - append(value: string) { - this.output += value; - } - appendLine(value: string) { this.append(value); this.append('\n'); } - clear() { } - show(preservceFocus?: boolean): void; - show(column?: vscode.ViewColumn, preserveFocus?: boolean): void; - show(x?: any, y?: any): void { } - hide() { } - dispose() { } -} - -suite('Unit Tests (unittest)', () => { - suiteSetup(done => { - initialize().then(() => { - pythonSettings.pythonPath = PYTHON_PATH; - done(); - }); - }); - suiteTeardown(done => { - done(); - }); - setup(() => { - outChannel = new MockOutputChannel('Python Test Log'); - testResultDisplay = new TestResultDisplay(outChannel); - }); - teardown(() => { - outChannel.dispose(); - testManager.dispose(); - testResultDisplay.dispose(); - }); - function createTestManager() { - testManager = new unittest.TestManager(rootDirectory, outChannel); - } - const rootDirectory = UNITTEST_TEST_FILES_PATH; - let testManager: unittest.TestManager; - let testResultDisplay: TestResultDisplay; - let outChannel: vscode.OutputChannel; - - test('Discover Tests (single test file)', done => { - pythonSettings.unitTest.unittestArgs = [ - '-s=./tests', - '-p=test_*.py' - ]; - testManager = new unittest.TestManager(UNITTEST_SINGLE_TEST_FILE_PATH, outChannel); - testManager.discoverTests(true, true).then(tests => { - assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 3, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 1, 'Incorrect number of test suites'); - assert.equal(tests.testFiles.some(t => t.name === 'test_one.py' && t.nameToRun === 'Test_test1.test_A'), true, 'Test File not found'); - }).then(done).catch(done); - }); - - test('Discover Tests', done => { - pythonSettings.unitTest.unittestArgs = [ - '-s=./tests', - '-p=test_*.py' - ]; - createTestManager(); - testManager.discoverTests(true, true).then(tests => { - assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 9, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 3, 'Incorrect number of test suites'); - assert.equal(tests.testFiles.some(t => t.name === 'test_unittest_one.py' && t.nameToRun === 'Test_test1.test_A'), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'test_unittest_two.py' && t.nameToRun === 'Test_test2.test_A2'), true, 'Test File not found'); - }).then(done).catch(done); - }); - - test('Discover Tests (pattern = *_test_*.py)', done => { - pythonSettings.unitTest.unittestArgs = [ - '-s=./tests', - '-p=*_test*.py' - ]; - createTestManager(); - testManager.discoverTests(true, true).then(tests => { - assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 1, 'Incorrect number of test suites'); - assert.equal(tests.testFiles.some(t => t.name === 'unittest_three_test.py' && t.nameToRun === 'Test_test3.test_A'), true, 'Test File not found'); - }).then(done).catch(done); - }); - - test('Run Tests', done => { - pythonSettings.unitTest.unittestArgs = [ - '-s=./tests', - '-p=*test*.py' - ]; - createTestManager(); - testManager.runTest().then(results => { - assert.equal(results.summary.errors, 1, 'Errors'); - assert.equal(results.summary.failures, 5, 'Failures'); - assert.equal(results.summary.passed, 4, 'Passed'); - assert.equal(results.summary.skipped, 1, 'skipped'); - }).then(done).catch(done); - }); - - // test('Fail Fast', done => { - // pythonSettings.unitTest.unittestArgs = [ - // '-s=./tests', - // '-p=*test*.py', - // '--failfast' - // ]; - // createTestManager(); - // testManager.runTest().then(results => { - // assert.equal(results.summary.errors, 1, 'Errors'); - // assert.equal(results.summary.failures, 5, 'Failures'); - // assert.equal(results.summary.passed, 4, 'Passed'); - // assert.equal(results.summary.skipped, 1, 'skipped'); - // done(); - // }).catch(done); - // }); - - test('Run Failed Tests', done => { - pythonSettings.unitTest.unittestArgs = [ - '-s=./tests', - '-p=*test*.py' - ]; - createTestManager(); - testManager.runTest().then(results => { - assert.equal(results.summary.errors, 1, 'Errors'); - assert.equal(results.summary.failures, 5, 'Failures'); - assert.equal(results.summary.passed, 4, 'Passed'); - assert.equal(results.summary.skipped, 1, 'skipped'); - - return testManager.runTest(true).then(tests => { - assert.equal(results.summary.errors, 1, 'Failed Errors'); - assert.equal(results.summary.failures, 5, 'Failed Failures'); - assert.equal(results.summary.passed, 0, 'Failed Passed'); - assert.equal(results.summary.skipped, 0, 'Failed skipped'); - }); - }).then(done).catch(done); - }); - - test('Run Specific Test File', done => { - pythonSettings.unitTest.unittestArgs = [ - '-s=./tests', - '-p=*test*.py' - ]; - createTestManager(); - testManager.discoverTests(true, true).then(tests => { - const testFile: TestsToRun = { testFile: [tests.testFiles[0]], testFolder: [], testFunction: [], testSuite: [] }; - return testManager.runTest(testFile).then(tests => { - assert.equal(tests.summary.errors, 0, 'Errors'); - assert.equal(tests.summary.failures, 1, 'Failures'); - assert.equal(tests.summary.passed, 1, 'Passed'); - assert.equal(tests.summary.skipped, 1, 'skipped'); - }); - }).then(done).catch(done); - }); - - test('Run Specific Test Suite', done => { - pythonSettings.unitTest.unittestArgs = [ - '-s=./tests', - '-p=*test*.py' - ]; - createTestManager(); - testManager.discoverTests(true, true).then(tests => { - const testSuite: TestsToRun = { testFile: [], testFolder: [], testFunction: [], testSuite: [tests.testSuits[0].testSuite] }; - return testManager.runTest(testSuite).then(tests => { - assert.equal(tests.summary.errors, 0, 'Errors'); - assert.equal(tests.summary.failures, 1, 'Failures'); - assert.equal(tests.summary.passed, 1, 'Passed'); - assert.equal(tests.summary.skipped, 1, 'skipped'); - }); - }).then(done).catch(done); - }); - - test('Run Specific Test Function', done => { - pythonSettings.unitTest.unittestArgs = [ - '-s=./tests', - '-p=*test*.py' - ]; - createTestManager(); - testManager.discoverTests(true, true).then(tests => { - const testFn: TestsToRun = { testFile: [], testFolder: [], testFunction: [tests.testFunctions[0].testFunction], testSuite: [] }; - return testManager.runTest(testFn).then(tests => { - assert.equal(tests.summary.errors, 0, 'Errors'); - assert.equal(tests.summary.failures, 1, 'Failures'); - assert.equal(tests.summary.passed, 0, 'Passed'); - assert.equal(tests.summary.skipped, 0, 'skipped'); - }); - }).then(done).catch(done); - }); -}); \ No newline at end of file diff --git a/src/test/format/extension.format.test.ts b/src/test/format/extension.format.test.ts new file mode 100644 index 000000000000..ce41edead475 --- /dev/null +++ b/src/test/format/extension.format.test.ts @@ -0,0 +1,119 @@ +import { updateSetting } from '../common'; + +// Note: This example test is leveraging the Mocha test framework. +// Please refer to their documentation on https://mochajs.org/ for help. + + +// The module 'assert' provides assertion methods from node +import * as assert from 'assert'; + +// You can import and use all API from the 'vscode' module +// as well as import your extension to test it +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as fs from 'fs-extra'; +import { EOL } from 'os'; +import { PythonSettings } from '../../client/common/configSettings'; +import { AutoPep8Formatter } from '../../client/formatters/autoPep8Formatter'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST, IS_TRAVIS } from '../initialize'; +import { YapfFormatter } from '../../client/formatters/yapfFormatter'; +import { execPythonFile } from '../../client/common/utils'; + +const ch = vscode.window.createOutputChannel('Tests'); +const pythoFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'formatting'); +const workspaceRootPath = path.join(__dirname, '..', '..', '..', 'src', 'test'); +const originalUnformattedFile = path.join(pythoFilesPath, 'fileToFormat.py'); + +const autoPep8FileToFormat = path.join(pythoFilesPath, 'autoPep8FileToFormat.py'); +const autoPep8FileToAutoFormat = path.join(pythoFilesPath, 'autoPep8FileToAutoFormat.py'); +const yapfFileToFormat = path.join(pythoFilesPath, 'yapfFileToFormat.py'); +const yapfFileToAutoFormat = path.join(pythoFilesPath, 'yapfFileToAutoFormat.py'); + +const configUpdateTarget = IS_MULTI_ROOT_TEST ? vscode.ConfigurationTarget.WorkspaceFolder : vscode.ConfigurationTarget.Workspace; + +let formattedYapf = ''; +let formattedAutoPep8 = ''; + +suite('Formatting', () => { + suiteSetup(async () => { + await initialize(); + [autoPep8FileToFormat, autoPep8FileToAutoFormat, yapfFileToFormat, yapfFileToAutoFormat].forEach(file => { + fs.copySync(originalUnformattedFile, file, { overwrite: true }); + }); + fs.ensureDirSync(path.dirname(autoPep8FileToFormat)); + const yapf = execPythonFile(workspaceRootPath, 'yapf', [originalUnformattedFile], workspaceRootPath, false); + const autoPep8 = execPythonFile(workspaceRootPath, 'autopep8', [originalUnformattedFile], workspaceRootPath, false); + await Promise.all([yapf, autoPep8]).then(formattedResults => { + formattedYapf = formattedResults[0]; + formattedAutoPep8 = formattedResults[1]; + }).then(() => { }); + }); + setup(() => initializeTest()); + suiteTeardown(async () => { + [autoPep8FileToFormat, autoPep8FileToAutoFormat, yapfFileToFormat, yapfFileToAutoFormat].forEach(file => { + if (fs.existsSync(file)) { + fs.unlinkSync(file); + } + }); + await updateSetting('formatting.formatOnSave', false, vscode.Uri.file(pythoFilesPath), configUpdateTarget) + await closeActiveWindows(); + }); + teardown(() => closeActiveWindows()); + + function testFormatting(formatter: AutoPep8Formatter | YapfFormatter, formattedContents: string, fileToFormat: string): PromiseLike { + let textEditor: vscode.TextEditor; + let textDocument: vscode.TextDocument; + return vscode.workspace.openTextDocument(fileToFormat).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + textEditor = editor; + return formatter.formatDocument(textDocument, null, null); + }).then(edits => { + return textEditor.edit(editBuilder => { + edits.forEach(edit => editBuilder.replace(edit.range, edit.newText)); + }); + }).then(edited => { + assert.equal(textEditor.document.getText(), formattedContents, 'Formatted text is not the same'); + }, reason => { + assert.fail(reason, undefined, 'Formatting failed', ''); + }); + } + test('AutoPep8', done => { + testFormatting(new AutoPep8Formatter(ch), formattedAutoPep8, autoPep8FileToFormat).then(done, done); + }); + + test('Yapf', done => { + testFormatting(new YapfFormatter(ch), formattedYapf, yapfFileToFormat).then(done, done); + }); + + async function testAutoFormatting(formatter: string, formattedContents: string, fileToFormat: string): Promise { + await updateSetting('formatting.formatOnSave', true, vscode.Uri.file(fileToFormat), configUpdateTarget); + await updateSetting('formatting.provider', formatter, vscode.Uri.file(fileToFormat), configUpdateTarget); + const textDocument = await vscode.workspace.openTextDocument(fileToFormat); + const editor = await vscode.window.showTextDocument(textDocument); + assert(vscode.window.activeTextEditor, 'No active editor'); + const edited = await editor.edit(editBuilder => { + editBuilder.insert(new vscode.Position(0, 0), '#\n'); + }); + const saved = await textDocument.save(); + await new Promise((resolve, reject) => { + setTimeout(() => { + resolve(); + }, 5000); + }); + const text = textDocument.getText(); + assert.equal(text === formattedContents, true, 'Formatted contents are not the same'); + } + test('AutoPep8 autoformat on save', done => { + testAutoFormatting('autopep8', `#${EOL}` + formattedAutoPep8, autoPep8FileToAutoFormat).then(done, done); + }); + + // For some reason doesn't ever work on travis + if (!IS_TRAVIS) { + test('Yapf autoformat on save', done => { + testAutoFormatting('yapf', `#${EOL}` + formattedYapf, yapfFileToAutoFormat).then(done, done); + }); + } +}); diff --git a/src/test/extension.onTypeFormat.test.ts b/src/test/format/extension.onTypeFormat.test.ts similarity index 81% rename from src/test/extension.onTypeFormat.test.ts rename to src/test/format/extension.onTypeFormat.test.ts index 8ecd73597a69..097922e9010e 100644 --- a/src/test/extension.onTypeFormat.test.ts +++ b/src/test/format/extension.onTypeFormat.test.ts @@ -3,8 +3,6 @@ // Please refer to their documentation on https://mochajs.org/ for help. -// Place this right on top -import { initialize, PYTHON_PATH, closeActiveWindows } from './initialize'; // The module 'assert' provides assertion methods from node import * as assert from 'assert'; @@ -12,12 +10,12 @@ import * as assert from 'assert'; // as well as import your extension to test it import * as vscode from 'vscode'; import * as path from 'path'; -import * as settings from '../client/common/configSettings'; import * as fs from 'fs-extra'; -import { BlockFormatProviders } from '../client/typeFormatters/blockFormatProvider'; -let pythonSettings = settings.PythonSettings.getInstance(); -let srcPythoFilesPath = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'typeFormatFiles'); -let outPythoFilesPath = path.join(__dirname, 'pythonFiles', 'typeFormatFiles'); +import { initialize, closeActiveWindows, initializeTest } from '../initialize'; +import { BlockFormatProviders } from '../../client/typeFormatters/blockFormatProvider'; + +const srcPythoFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'typeFormatFiles'); +const outPythoFilesPath = path.join(__dirname, 'pythonFiles', 'typeFormatFiles'); const tryBlock2OutFilePath = path.join(outPythoFilesPath, 'tryBlocks2.py'); const tryBlock4OutFilePath = path.join(outPythoFilesPath, 'tryBlocks4.py'); @@ -56,24 +54,18 @@ function testFormatting(fileToFormat: string, position: vscode.Position, expecte suite('Else block with if in first line of file', () => { - suiteSetup(done => { - initialize().then(() => { - pythonSettings.pythonPath = PYTHON_PATH; - fs.ensureDirSync(path.dirname(outPythoFilesPath)); - - ['elseBlocksFirstLine2.py', 'elseBlocksFirstLine4.py', 'elseBlocksFirstLineTab.py'].forEach(file => { - const targetFile = path.join(outPythoFilesPath, file); - if (fs.existsSync(targetFile)) { fs.unlinkSync(targetFile); } - fs.copySync(path.join(srcPythoFilesPath, file), targetFile); - }); - }).then(done).catch(done); - }); - suiteTeardown(done => { - closeActiveWindows().then(done, done); - }); - teardown(done => { - closeActiveWindows().then(done, done); + suiteSetup(async () => { + await initialize(); + fs.ensureDirSync(path.dirname(outPythoFilesPath)); + + ['elseBlocksFirstLine2.py', 'elseBlocksFirstLine4.py', 'elseBlocksFirstLineTab.py'].forEach(file => { + const targetFile = path.join(outPythoFilesPath, file); + if (fs.existsSync(targetFile)) { fs.unlinkSync(targetFile); } + fs.copySync(path.join(srcPythoFilesPath, file), targetFile); + }); }); + suiteTeardown(() => closeActiveWindows()); + teardown(() => closeActiveWindows()); interface TestCase { title: string; @@ -124,24 +116,18 @@ suite('Else block with if in first line of file', () => { }); suite('Try blocks with indentation of 2 spaces', () => { - suiteSetup(done => { - initialize().then(() => { - pythonSettings.pythonPath = PYTHON_PATH; - fs.ensureDirSync(path.dirname(outPythoFilesPath)); - - ['tryBlocks2.py'].forEach(file => { - const targetFile = path.join(outPythoFilesPath, file); - if (fs.existsSync(targetFile)) { fs.unlinkSync(targetFile); } - fs.copySync(path.join(srcPythoFilesPath, file), targetFile); - }); - }).then(done).catch(done); - }); - suiteTeardown(done => { - closeActiveWindows().then(done, done); - }); - teardown(done => { - closeActiveWindows().then(done, done); + suiteSetup(async () => { + await initialize(); + fs.ensureDirSync(path.dirname(outPythoFilesPath)); + + ['tryBlocks2.py'].forEach(file => { + const targetFile = path.join(outPythoFilesPath, file); + if (fs.existsSync(targetFile)) { fs.unlinkSync(targetFile); } + fs.copySync(path.join(srcPythoFilesPath, file), targetFile); + }); }); + suiteTeardown(() => closeActiveWindows()); + teardown(() => closeActiveWindows()); interface TestCase { title: string; @@ -231,24 +217,18 @@ suite('Try blocks with indentation of 2 spaces', () => { }); suite('Try blocks with indentation of 4 spaces', () => { - suiteSetup(done => { - initialize().then(() => { - pythonSettings.pythonPath = PYTHON_PATH; - fs.ensureDirSync(path.dirname(outPythoFilesPath)); - - ['tryBlocks4.py'].forEach(file => { - const targetFile = path.join(outPythoFilesPath, file); - if (fs.existsSync(targetFile)) { fs.unlinkSync(targetFile); } - fs.copySync(path.join(srcPythoFilesPath, file), targetFile); - }); - }).then(done).catch(done); - }); - suiteTeardown(done => { - closeActiveWindows().then(done, done); - }); - teardown(done => { - closeActiveWindows().then(done, done); + suiteSetup(async () => { + await initialize(); + fs.ensureDirSync(path.dirname(outPythoFilesPath)); + + ['tryBlocks4.py'].forEach(file => { + const targetFile = path.join(outPythoFilesPath, file); + if (fs.existsSync(targetFile)) { fs.unlinkSync(targetFile); } + fs.copySync(path.join(srcPythoFilesPath, file), targetFile); + }); }); + suiteTeardown(() => closeActiveWindows()); + teardown(() => closeActiveWindows()); interface TestCase { title: string; @@ -338,24 +318,18 @@ suite('Try blocks with indentation of 4 spaces', () => { }); suite('Try blocks with indentation of Tab', () => { - suiteSetup(done => { - initialize().then(() => { - pythonSettings.pythonPath = PYTHON_PATH; - fs.ensureDirSync(path.dirname(outPythoFilesPath)); - - ['tryBlocksTab.py'].forEach(file => { - const targetFile = path.join(outPythoFilesPath, file); - if (fs.existsSync(targetFile)) { fs.unlinkSync(targetFile); } - fs.copySync(path.join(srcPythoFilesPath, file), targetFile); - }); - }).then(done).catch(done); - }); - suiteTeardown(done => { - closeActiveWindows().then(done, done); - }); - teardown(done => { - closeActiveWindows().then(done, done); + suiteSetup(async () => { + await initialize(); + fs.ensureDirSync(path.dirname(outPythoFilesPath)); + + ['tryBlocksTab.py'].forEach(file => { + const targetFile = path.join(outPythoFilesPath, file); + if (fs.existsSync(targetFile)) { fs.unlinkSync(targetFile); } + fs.copySync(path.join(srcPythoFilesPath, file), targetFile); + }); }); + suiteTeardown(() => closeActiveWindows()); + teardown(() => closeActiveWindows()); interface TestCase { title: string; @@ -436,24 +410,18 @@ suite('Try blocks with indentation of Tab', () => { }); suite('Else blocks with indentation of 2 spaces', () => { - suiteSetup(done => { - initialize().then(() => { - pythonSettings.pythonPath = PYTHON_PATH; - fs.ensureDirSync(path.dirname(outPythoFilesPath)); - - ['elseBlocks2.py'].forEach(file => { - const targetFile = path.join(outPythoFilesPath, file); - if (fs.existsSync(targetFile)) { fs.unlinkSync(targetFile); } - fs.copySync(path.join(srcPythoFilesPath, file), targetFile); - }); - }).then(done).catch(done); - }); - suiteTeardown(done => { - closeActiveWindows().then(done, done); - }); - teardown(done => { - closeActiveWindows().then(done, done); + suiteSetup(async () => { + await initialize(); + fs.ensureDirSync(path.dirname(outPythoFilesPath)); + + ['elseBlocks2.py'].forEach(file => { + const targetFile = path.join(outPythoFilesPath, file); + if (fs.existsSync(targetFile)) { fs.unlinkSync(targetFile); } + fs.copySync(path.join(srcPythoFilesPath, file), targetFile); + }); }); + suiteTeardown(() => closeActiveWindows()); + teardown(() => closeActiveWindows()); interface TestCase { title: string; @@ -573,24 +541,18 @@ suite('Else blocks with indentation of 2 spaces', () => { }); suite('Else blocks with indentation of 4 spaces', () => { - suiteSetup(done => { - initialize().then(() => { - pythonSettings.pythonPath = PYTHON_PATH; - fs.ensureDirSync(path.dirname(outPythoFilesPath)); - - ['elseBlocks4.py'].forEach(file => { - const targetFile = path.join(outPythoFilesPath, file); - if (fs.existsSync(targetFile)) { fs.unlinkSync(targetFile); } - fs.copySync(path.join(srcPythoFilesPath, file), targetFile); - }); - }).then(done).catch(done); - }); - suiteTeardown(done => { - closeActiveWindows().then(done, done); - }); - teardown(done => { - closeActiveWindows().then(done, done); + suiteSetup(async () => { + await initialize(); + fs.ensureDirSync(path.dirname(outPythoFilesPath)); + + ['elseBlocks4.py'].forEach(file => { + const targetFile = path.join(outPythoFilesPath, file); + if (fs.existsSync(targetFile)) { fs.unlinkSync(targetFile); } + fs.copySync(path.join(srcPythoFilesPath, file), targetFile); + }); }); + suiteTeardown(() => closeActiveWindows()); + teardown(() => closeActiveWindows()); interface TestCase { title: string; @@ -704,24 +666,19 @@ suite('Else blocks with indentation of 4 spaces', () => { }); suite('Else blocks with indentation of Tab', () => { - suiteSetup(done => { - initialize().then(() => { - pythonSettings.pythonPath = PYTHON_PATH; - fs.ensureDirSync(path.dirname(outPythoFilesPath)); - - ['elseBlocksTab.py'].forEach(file => { - const targetFile = path.join(outPythoFilesPath, file); - if (fs.existsSync(targetFile)) { fs.unlinkSync(targetFile); } - fs.copySync(path.join(srcPythoFilesPath, file), targetFile); - }); - }).then(done).catch(done); - }); - suiteTeardown(done => { - closeActiveWindows().then(done, done); - }); - teardown(done => { - closeActiveWindows().then(done, done); + suiteSetup(async () => { + await initialize(); + fs.ensureDirSync(path.dirname(outPythoFilesPath)); + + ['elseBlocksTab.py'].forEach(file => { + const targetFile = path.join(outPythoFilesPath, file); + if (fs.existsSync(targetFile)) { fs.unlinkSync(targetFile); } + fs.copySync(path.join(srcPythoFilesPath, file), targetFile); + }); }); + setup(() => initializeTest()); + suiteTeardown(() => closeActiveWindows()); + teardown(() => closeActiveWindows()); interface TestCase { title: string; diff --git a/src/test/format/extension.sort.test.ts b/src/test/format/extension.sort.test.ts new file mode 100644 index 000000000000..c8c2f1c6ffc3 --- /dev/null +++ b/src/test/format/extension.sort.test.ts @@ -0,0 +1,97 @@ +import * as assert from 'assert'; +import * as fs from 'fs'; +import { EOL } from 'os'; +import * as path from 'path'; +import { commands, ConfigurationTarget, Position, Range, Uri, window, workspace } from 'vscode'; +import { PythonImportSortProvider } from '../../client/providers/importSortProvider'; +import { updateSetting } from '../common'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; + +const sortingPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'sorting'); +const fileToFormatWithoutConfig = path.join(sortingPath, 'noconfig', 'before.py'); +const originalFileToFormatWithoutConfig = path.join(sortingPath, 'noconfig', 'original.py'); +const fileToFormatWithConfig = path.join(sortingPath, 'withconfig', 'before.py'); +const originalFileToFormatWithConfig = path.join(sortingPath, 'withconfig', 'original.py'); +const fileToFormatWithConfig1 = path.join(sortingPath, 'withconfig', 'before.1.py'); +const originalFileToFormatWithConfig1 = path.join(sortingPath, 'withconfig', 'original.1.py'); +const extensionDir = path.join(__dirname, '..', '..', '..'); + +// tslint:disable-next-line:max-func-body-length +suite('Sorting', () => { + const configTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; + suiteSetup(initialize); + setup(initializeTest); + suiteTeardown(async () => { + fs.writeFileSync(fileToFormatWithConfig, fs.readFileSync(originalFileToFormatWithConfig)); + fs.writeFileSync(fileToFormatWithConfig1, fs.readFileSync(originalFileToFormatWithConfig1)); + fs.writeFileSync(fileToFormatWithoutConfig, fs.readFileSync(originalFileToFormatWithoutConfig)); + await updateSetting('sortImports.args', [], Uri.file(sortingPath), configTarget); + await closeActiveWindows(); + }); + setup(async () => { + fs.writeFileSync(fileToFormatWithConfig, fs.readFileSync(originalFileToFormatWithConfig)); + fs.writeFileSync(fileToFormatWithoutConfig, fs.readFileSync(originalFileToFormatWithoutConfig)); + fs.writeFileSync(fileToFormatWithConfig1, fs.readFileSync(originalFileToFormatWithConfig1)); + await updateSetting('sortImports.args', [], Uri.file(sortingPath), configTarget); + await closeActiveWindows(); + }); + + test('Without Config', async () => { + const textDocument = await workspace.openTextDocument(fileToFormatWithoutConfig); + await window.showTextDocument(textDocument); + const sorter = new PythonImportSortProvider(); + const edits = await sorter.sortImports(extensionDir, textDocument); + assert.equal(edits.filter(value => value.newText === EOL && value.range.isEqual(new Range(2, 0, 2, 0))).length, 1, 'EOL not found'); + assert.equal(edits.filter(value => value.newText === '' && value.range.isEqual(new Range(3, 0, 4, 0))).length, 1, '"" not found'); + assert.equal(edits.filter(value => value.newText === `from rope.base import libutils${EOL}from rope.refactor.extract import ExtractMethod, ExtractVariable${EOL}from rope.refactor.rename import Rename${EOL}` && value.range.isEqual(new Range(6, 0, 6, 0))).length, 1, 'Text not found'); + assert.equal(edits.filter(value => value.newText === '' && value.range.isEqual(new Range(13, 0, 18, 0))).length, 1, '"" not found'); + }); + + test('Without Config (via Command)', async () => { + const textDocument = await workspace.openTextDocument(fileToFormatWithoutConfig); + const originalContent = textDocument.getText(); + await window.showTextDocument(textDocument); + await commands.executeCommand('python.sortImports'); + assert.notEqual(originalContent, textDocument.getText(), 'Contents have not changed'); + }); + + test('With Config', async () => { + const textDocument = await workspace.openTextDocument(fileToFormatWithConfig); + await window.showTextDocument(textDocument); + const sorter = new PythonImportSortProvider(); + const edits = await sorter.sortImports(extensionDir, textDocument); + const newValue = `from third_party import lib2${EOL}from third_party import lib3${EOL}from third_party import lib4${EOL}from third_party import lib5${EOL}from third_party import lib6${EOL}from third_party import lib7${EOL}from third_party import lib8${EOL}from third_party import lib9${EOL}`; + assert.equal(edits.filter(value => value.newText === newValue && value.range.isEqual(new Range(0, 0, 3, 0))).length, 1, 'New Text not found'); + }); + + test('With Config (via Command)', async () => { + const textDocument = await workspace.openTextDocument(fileToFormatWithConfig); + const originalContent = textDocument.getText(); + await window.showTextDocument(textDocument); + await commands.executeCommand('python.sortImports'); + assert.notEqual(originalContent, textDocument.getText(), 'Contents have not changed'); + }); + + test('With Changes and Config in Args', async () => { + await updateSetting('sortImports.args', ['-sp', path.join(sortingPath, 'withconfig')], Uri.file(sortingPath), ConfigurationTarget.Workspace); + const textDocument = await workspace.openTextDocument(fileToFormatWithConfig); + const editor = await window.showTextDocument(textDocument); + await editor.edit(builder => { + builder.insert(new Position(0, 0), `from third_party import lib0${EOL}`); + }); + const sorter = new PythonImportSortProvider(); + const edits = await sorter.sortImports(extensionDir, textDocument); + assert.notEqual(edits.length, 0, 'No edits'); + }); + test('With Changes and Config in Args (via Command)', async () => { + await updateSetting('sortImports.args', ['-sp', path.join(sortingPath, 'withconfig')], Uri.file(sortingPath), configTarget); + const textDocument = await workspace.openTextDocument(fileToFormatWithConfig); + const editor = await window.showTextDocument(textDocument); + await editor.edit(builder => { + builder.insert(new Position(0, 0), `from third_party import lib0${EOL}`); + }); + const originalContent = textDocument.getText(); + await commands.executeCommand('python.sortImports'); + assert.notEqual(originalContent, textDocument.getText(), 'Contents have not changed'); + }); +}); diff --git a/src/test/index.ts b/src/test/index.ts index d2545b0c70a5..f14ba247bb92 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -1,23 +1,13 @@ -// -// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING -// -// This file is providing the test runner to use when running extension tests. -// By default the test runner in use is Mocha based. -// -// You can provide your own test runner if you want to override it by exporting -// a function run(testRoot: string, clb: (error:Error) => void) that the extension -// host can call to run the tests. The test runner is expected to use console.log -// to report the results back to the caller. When the tests are finished, return -// a possible error to the callback or null if none. +import * as testRunner from 'vscode/lib/testrunner'; +import { initializePython, IS_MULTI_ROOT_TEST } from './initialize'; +process.env.VSC_PYTHON_CI_TEST = '1'; -let testRunner = require('vscode/lib/testrunner'); - -// You can directly control Mocha options by uncommenting the following lines -// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info +// You can directly control Mocha options by uncommenting the following lines. +// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info. testRunner.configure({ - ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) - useColors: true, // colored output from test results - timeout: 15000 + ui: 'tdd', + useColors: true, + timeout: 25000, + retries: 3 }); - -module.exports = testRunner; \ No newline at end of file +module.exports = testRunner; diff --git a/src/test/initialize.ts b/src/test/initialize.ts index 36adfd123ab3..67193c2da0bd 100644 --- a/src/test/initialize.ts +++ b/src/test/initialize.ts @@ -1,69 +1,103 @@ -// -// Note: This example test is leveraging the Mocha test framework. -// Please refer to their documentation on https://mochajs.org/ for help. -// +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { PythonSettings } from '../client/common/configSettings'; +import { activated } from '../client/extension'; +import { clearPythonPathInWorkspaceFolder, resetGlobalPythonPathSetting, setPythonPathInWorkspaceRoot } from './common'; -//First thing to be executed -process.env['PYTHON_DONJAYAMANNE_TEST'] = "1"; +const dummyPythonFile = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'dummy.py'); +const multirootPath = path.join(__dirname, '..', '..', 'src', 'testMultiRootWkspc'); +const workspace3Uri = vscode.Uri.file(path.join(multirootPath, 'workspace3')); -// The module 'assert' provides assertion methods from node -import * as assert from "assert"; -import * as fs from 'fs'; +//First thing to be executed. +// tslint:disable-next-line:no-string-literal +process.env['VSC_PYTHON_CI_TEST'] = '1'; + +const PYTHON_PATH = getPythonPath(); +// tslint:disable-next-line:no-string-literal prefer-template +export const IS_TRAVIS = (process.env['TRAVIS'] + '') === 'true'; +export const TEST_TIMEOUT = 25000; +export const IS_MULTI_ROOT_TEST = isMultitrootTest(); -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it -import * as vscode from "vscode"; -import * as path from "path"; -let dummyPythonFile = path.join(__dirname, "..", "..", "src", "test", "pythonFiles", "dummy.py"); +// Ability to use custom python environments for testing +export async function initializePython() { + await resetGlobalPythonPathSetting(); + await clearPythonPathInWorkspaceFolder(dummyPythonFile); + await clearPythonPathInWorkspaceFolder(workspace3Uri); + await setPythonPathInWorkspaceRoot(PYTHON_PATH); +} + +// tslint:disable-next-line:no-any +export async function initialize(): Promise { + await initializePython(); + // Opening a python file activates the extension. + await vscode.workspace.openTextDocument(dummyPythonFile); + await activated; + // Dispose any cached python settings (used only in test env). + PythonSettings.dispose(); +} +// tslint:disable-next-line:no-any +export async function initializeTest(): Promise { + await initializePython(); + await closeActiveWindows(); + // Dispose any cached python settings (used only in test env). + PythonSettings.dispose(); +} -export function initialize(): Promise { - return new Promise((resolve, reject) => { - vscode.workspace.openTextDocument(dummyPythonFile).then(resolve, reject); +export async function wait(timeoutMilliseconds: number) { + return new Promise(resolve => { + // tslint:disable-next-line:no-string-based-set-timeout + setTimeout(resolve, timeoutMilliseconds); }); } +// tslint:disable-next-line:no-any export async function closeActiveWindows(): Promise { // https://github.com/Microsoft/vscode/blob/master/extensions/vscode-api-tests/src/utils.ts - return new Promise((c, e) => { + return new Promise(resolve => { if (vscode.window.visibleTextEditors.length === 0) { - return c(); + return resolve(); } // TODO: the visibleTextEditors variable doesn't seem to be // up to date after a onDidChangeActiveTextEditor event, not // even using a setTimeout 0... so we MUST poll :( - let interval = setInterval(() => { + const interval = setInterval(() => { if (vscode.window.visibleTextEditors.length > 0) { return; } clearInterval(interval); - c(); + resolve(); }, 10); - vscode.commands.executeCommand('workbench.action.closeAllEditors') - .then(() => null, (err: any) => { - clearInterval(interval); - e(err); - }); + setTimeout(() => { + if (vscode.window.visibleTextEditors.length === 0) { + return resolve(); + } + vscode.commands.executeCommand('workbench.action.closeAllEditors') + // tslint:disable-next-line:no-any + .then(() => null, (err: any) => { + clearInterval(interval); + resolve(); + }); + }, 50); + }).then(() => { assert.equal(vscode.window.visibleTextEditors.length, 0); - assert(!vscode.window.activeTextEditor); }); } -export const IS_TRAVIS = (process.env['TRAVIS'] + '') === 'true'; -export const TEST_TIMEOUT = 10000; - function getPythonPath(): string { - const pythonPaths = ['/home/travis/virtualenv/python3.5.2/bin/python', - '/Users/travis/.pyenv/versions/3.5.1/envs/MYVERSION/bin/python']; - for (let counter = 0; counter < pythonPaths.length; counter++) { - if (fs.existsSync(pythonPaths[counter])) { - return pythonPaths[counter]; - } + // tslint:disable-next-line:no-unsafe-any + if (process.env.TRAVIS_PYTHON_PATH && fs.existsSync(process.env.TRAVIS_PYTHON_PATH)) { + // tslint:disable-next-line:no-unsafe-any + return process.env.TRAVIS_PYTHON_PATH; } return 'python'; } -export const PYTHON_PATH = IS_TRAVIS ? getPythonPath() : 'python'; \ No newline at end of file +function isMultitrootTest() { + return Array.isArray(vscode.workspace.workspaceFolders) && vscode.workspace.workspaceFolders.length > 1; +} diff --git a/src/test/interpreters/condaEnvFileService.test.ts b/src/test/interpreters/condaEnvFileService.test.ts new file mode 100644 index 000000000000..88f3ae8934e3 --- /dev/null +++ b/src/test/interpreters/condaEnvFileService.test.ts @@ -0,0 +1,72 @@ +import * as assert from 'assert'; +import * as fs from 'fs-extra'; +import { EOL } from 'os'; +import * as path from 'path'; +import { IS_WINDOWS } from '../../client/common/utils'; +import { + AnacondaCompanyName, + AnacondaCompanyNames, + AnacondaDisplayName, + CONDA_RELATIVE_PY_PATH +} from '../../client/interpreter/locators/services/conda'; +import { CondaEnvFileService } from '../../client/interpreter/locators/services/condaEnvFileService'; +import { initialize, initializeTest } from '../initialize'; +import { MockInterpreterVersionProvider } from './mocks'; + +const environmentsPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'environments'); +const environmentsFilePath = path.join(environmentsPath, 'environments.txt'); + +suite('Interpreters from Conda Environments Text File', () => { + suiteSetup(initialize); + setup(initializeTest); + suiteTeardown(async () => { + // Clear the file so we don't get unwanted changes prompting for a checkin of this file + await updateEnvWithInterpreters([]); + }); + + async function updateEnvWithInterpreters(envs: string[]) { + await fs.writeFile(environmentsFilePath, envs.join(EOL), { flag: 'w' }); + } + test('Must return an empty list for an empty file', async () => { + await updateEnvWithInterpreters([]); + const displayNameProvider = new MockInterpreterVersionProvider('Mock Name'); + const condaFileProvider = new CondaEnvFileService(environmentsFilePath, displayNameProvider); + const interpreters = await condaFileProvider.getInterpreters(); + assert.equal(interpreters.length, 0, 'Incorrect number of entries'); + }); + test('Must return filter files in the list and return valid items', async () => { + const interpreterPaths = [ + path.join(environmentsPath, 'conda', 'envs', 'numpy'), + path.join(environmentsPath, 'path1'), + path.join('Invalid and non existent'), + path.join(environmentsPath, 'path2'), + path.join('Another Invalid and non existent') + ]; + await updateEnvWithInterpreters(interpreterPaths); + const displayNameProvider = new MockInterpreterVersionProvider('Mock Name'); + const condaFileProvider = new CondaEnvFileService(environmentsFilePath, displayNameProvider); + const interpreters = await condaFileProvider.getInterpreters(); + // This is because conda environments will be under 'bin/python' however the folders path1 and path2 do not have such files + const numberOfEnvs = IS_WINDOWS ? 3 : 1; + assert.equal(interpreters.length, numberOfEnvs, 'Incorrect number of entries'); + assert.equal(interpreters[0].displayName, `${AnacondaDisplayName} Mock Name (numpy)`, 'Incorrect display name'); + assert.equal(interpreters[0].companyDisplayName, AnacondaCompanyName, 'Incorrect display name'); + assert.equal(interpreters[0].path, path.join(interpreterPaths[0], ...CONDA_RELATIVE_PY_PATH), 'Incorrect company display name'); + }); + test('Must strip company name from version info', async () => { + const interpreterPaths = [ + path.join(environmentsPath, 'conda', 'envs', 'numpy') + ]; + await updateEnvWithInterpreters(interpreterPaths); + + AnacondaCompanyNames.forEach(async companyDisplayName => { + const displayNameProvider = new MockInterpreterVersionProvider(`Mock Version :: ${companyDisplayName}`); + const condaFileProvider = new CondaEnvFileService(environmentsFilePath, displayNameProvider); + const interpreters = await condaFileProvider.getInterpreters(); + // This is because conda environments will be under 'bin/python' however the folders path1 and path2 do not have such files + const numberOfEnvs = IS_WINDOWS ? 3 : 1; + assert.equal(interpreters.length, numberOfEnvs, 'Incorrect number of entries'); + assert.equal(interpreters[0].displayName, `${AnacondaDisplayName} Mock Version (numpy)`, 'Incorrect display name'); + }); + }); +}); diff --git a/src/test/interpreters/condaEnvService.test.ts b/src/test/interpreters/condaEnvService.test.ts new file mode 100644 index 000000000000..8ba4fcf5d43c --- /dev/null +++ b/src/test/interpreters/condaEnvService.test.ts @@ -0,0 +1,180 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { PythonSettings } from '../../client/common/configSettings'; +import { IS_WINDOWS } from '../../client/common/utils'; +import { PythonInterpreter } from '../../client/interpreter/contracts'; +import { AnacondaCompanyName, AnacondaDisplayName } from '../../client/interpreter/locators/services/conda'; +import { CondaEnvService } from '../../client/interpreter/locators/services/condaEnvService'; +import { initialize, initializeTest } from '../initialize'; +import { MockProvider } from './mocks'; + +const environmentsPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'environments'); +const fileInNonRootWorkspace = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'dummy.py'); + +// tslint:disable-next-line:max-func-body-length +suite('Interpreters from Conda Environments', () => { + suiteSetup(initialize); + setup(initializeTest); + test('Must return an empty list for empty json', async () => { + const condaProvider = new CondaEnvService(); + // tslint:disable-next-line:no-any prefer-type-cast + const interpreters = await condaProvider.parseCondaInfo({} as any); + assert.equal(interpreters.length, 0, 'Incorrect number of entries'); + }); + test('Must extract display name from version info', async () => { + const condaProvider = new CondaEnvService(); + const info = { + envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy'), + path.join(environmentsPath, 'conda', 'envs', 'scipy')], + default_prefix: '', + 'sys.version': '3.6.1 |Anaconda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]' + }; + const interpreters = await condaProvider.parseCondaInfo(info); + assert.equal(interpreters.length, 2, 'Incorrect number of entries'); + + const path1 = path.join(info.envs[0], IS_WINDOWS ? 'python.exe' : 'bin/python'); + assert.equal(interpreters[0].path, path1, 'Incorrect path for first env'); + assert.equal(interpreters[0].displayName, 'Anaconda 4.4.0 (64-bit) (numpy)', 'Incorrect display name for first env'); + assert.equal(interpreters[0].companyDisplayName, AnacondaCompanyName, 'Incorrect company display name for first env'); + + const path2 = path.join(info.envs[1], IS_WINDOWS ? 'python.exe' : 'bin/python'); + assert.equal(interpreters[1].path, path2, 'Incorrect path for first env'); + assert.equal(interpreters[1].displayName, 'Anaconda 4.4.0 (64-bit) (scipy)', 'Incorrect display name for first env'); + assert.equal(interpreters[1].companyDisplayName, AnacondaCompanyName, 'Incorrect company display name for first env'); + }); + test('Must use the default display name if sys.version is invalid', async () => { + const condaProvider = new CondaEnvService(); + const info = { + envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy')], + default_prefix: '', + 'sys.version': '3.6.1 |Anaonda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]' + }; + const interpreters = await condaProvider.parseCondaInfo(info); + assert.equal(interpreters.length, 1, 'Incorrect number of entries'); + + const path1 = path.join(info.envs[0], IS_WINDOWS ? 'python.exe' : 'bin/python'); + assert.equal(interpreters[0].path, path1, 'Incorrect path for first env'); + assert.equal(interpreters[0].displayName, `Anaonda 4.4.0 (64-bit) : ${AnacondaDisplayName} (numpy)`, 'Incorrect display name for first env'); + assert.equal(interpreters[0].companyDisplayName, AnacondaCompanyName, 'Incorrect company display name for first env'); + }); + test('Must use the default display name if sys.version is empty', async () => { + const condaProvider = new CondaEnvService(); + const info = { + envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy')] + }; + const interpreters = await condaProvider.parseCondaInfo(info); + assert.equal(interpreters.length, 1, 'Incorrect number of entries'); + + const path1 = path.join(info.envs[0], IS_WINDOWS ? 'python.exe' : 'bin/python'); + assert.equal(interpreters[0].path, path1, 'Incorrect path for first env'); + assert.equal(interpreters[0].displayName, `${AnacondaDisplayName} (numpy)`, 'Incorrect display name for first env'); + assert.equal(interpreters[0].companyDisplayName, AnacondaCompanyName, 'Incorrect company display name for first env'); + }); + test('Must include the default_prefix into the list of interpreters', async () => { + const condaProvider = new CondaEnvService(); + const info = { + default_prefix: path.join(environmentsPath, 'conda', 'envs', 'numpy') + }; + const interpreters = await condaProvider.parseCondaInfo(info); + assert.equal(interpreters.length, 1, 'Incorrect number of entries'); + + const path1 = path.join(info.default_prefix, IS_WINDOWS ? 'python.exe' : 'bin/python'); + assert.equal(interpreters[0].path, path1, 'Incorrect path for first env'); + assert.equal(interpreters[0].displayName, AnacondaDisplayName, 'Incorrect display name for first env'); + assert.equal(interpreters[0].companyDisplayName, AnacondaCompanyName, 'Incorrect company display name for first env'); + }); + test('Must exclude interpreters that do not exist on disc', async () => { + const condaProvider = new CondaEnvService(); + const info = { + envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy'), + path.join(environmentsPath, 'path0', 'one.exe'), + path.join(environmentsPath, 'path1', 'one.exe'), + path.join(environmentsPath, 'path2', 'one.exe'), + path.join(environmentsPath, 'conda', 'envs', 'scipy'), + path.join(environmentsPath, 'path3', 'three.exe')] + }; + const interpreters = await condaProvider.parseCondaInfo(info); + + assert.equal(interpreters.length, 2, 'Incorrect number of entries'); + // Go up one dir for linux (cuz exe is under a sub directory named 'bin') + let path0 = path.dirname(interpreters[0].path); + path0 = IS_WINDOWS ? path0 : path.dirname(path0); + assert.equal(path0, info.envs[0], 'Incorrect path for first env'); + // Go up one dir for linux (cuz exe is under a sub directory named 'bin') + let path1 = path.dirname(interpreters[1].path); + path1 = IS_WINDOWS ? path1 : path.dirname(path1); + assert.equal(path1, info.envs[4], 'Incorrect path for second env'); + }); + test('Must detect conda environments from a list', async () => { + const registryInterpreters: PythonInterpreter[] = [ + { displayName: 'One', path: 'c:/path1/one.exe', companyDisplayName: 'One 1' }, + { displayName: 'Two', path: PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath, companyDisplayName: 'Two 2' }, + { displayName: 'Three', path: path.join(environmentsPath, 'path1', 'one.exe'), companyDisplayName: 'Three 3' }, + { displayName: 'Anaconda', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3' }, + { displayName: 'xAnaconda', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3' }, + { displayName: 'xnaconda', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'xContinuum Analytics, Inc.' }, + { displayName: 'xnaconda', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Continuum Analytics, Inc.' } + ]; + const mockRegistryProvider = new MockProvider(registryInterpreters); + const condaProvider = new CondaEnvService(mockRegistryProvider); + + assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[0]), false, '1. Identified environment incorrectly'); + assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[1]), false, '2. Identified environment incorrectly'); + assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[2]), false, '3. Identified environment incorrectly'); + assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[3]), true, '4. Failed to identify conda environment when displayName starts with \'Anaconda\''); + assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[4]), true, '5. Failed to identify conda environment when displayName contains text \'Anaconda\''); + assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[5]), true, '6. Failed to identify conda environment when comanyDisplayName contains \'Continuum\''); + assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[6]), true, '7. Failed to identify conda environment when companyDisplayName starts with \'Continuum\''); + }); + test('Correctly identifies latest version when major version is different', async () => { + const registryInterpreters: PythonInterpreter[] = [ + { displayName: 'One', path: path.join(environmentsPath, 'path1', 'one.exe'), companyDisplayName: 'One 1', version: '1' }, + { displayName: 'Two', path: PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath, companyDisplayName: 'Two 2', version: '3.1.3' }, + { displayName: 'Three', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', version: '2.10.1' }, + // tslint:disable-next-line:no-any + { displayName: 'Four', path: path.join(environmentsPath, 'conda', 'envs', 'scipy'), companyDisplayName: 'Three 3', version: null }, + // tslint:disable-next-line:no-any + { displayName: 'Five', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Three 3', version: undefined }, + { displayName: 'Six', path: path.join(environmentsPath, 'conda', 'envs', 'scipy'), companyDisplayName: 'xContinuum Analytics, Inc.', version: '2' }, + { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.' } + ]; + const mockRegistryProvider = new MockProvider(registryInterpreters); + const condaProvider = new CondaEnvService(mockRegistryProvider); + + // tslint:disable-next-line:no-non-null-assertion + assert.equal(condaProvider.getLatestVersion(registryInterpreters)!.displayName, 'Two', 'Failed to identify latest version'); + }); + test('Correctly identifies latest version when major version is same', async () => { + const registryInterpreters: PythonInterpreter[] = [ + { displayName: 'One', path: path.join(environmentsPath, 'path1', 'one.exe'), companyDisplayName: 'One 1', version: '1' }, + { displayName: 'Two', path: PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath, companyDisplayName: 'Two 2', version: '2.11.3' }, + { displayName: 'Three', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', version: '2.10.1' }, + // tslint:disable-next-line:no-any + { displayName: 'Four', path: path.join(environmentsPath, 'conda', 'envs', 'scipy'), companyDisplayName: 'Three 3', version: null }, + // tslint:disable-next-line:no-any + { displayName: 'Five', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Three 3', version: undefined }, + { displayName: 'Six', path: path.join(environmentsPath, 'conda', 'envs', 'scipy'), companyDisplayName: 'xContinuum Analytics, Inc.', version: '2' }, + { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.' } + ]; + const mockRegistryProvider = new MockProvider(registryInterpreters); + const condaProvider = new CondaEnvService(mockRegistryProvider); + + // tslint:disable-next-line:no-non-null-assertion + assert.equal(condaProvider.getLatestVersion(registryInterpreters)!.displayName, 'Two', 'Failed to identify latest version'); + }); + test('Must use Conda env from Registry to locate conda.exe', async () => { + const condaPythonExePath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'environments', 'conda', 'Scripts', 'python.exe'); + const registryInterpreters: PythonInterpreter[] = [ + { displayName: 'One', path: path.join(environmentsPath, 'path1', 'one.exe'), companyDisplayName: 'One 1', version: '1' }, + { displayName: 'Anaconda', path: condaPythonExePath, companyDisplayName: 'Two 2', version: '1.11.0' }, + { displayName: 'Three', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', version: '2.10.1' }, + { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.' } + ]; + const mockRegistryProvider = new MockProvider(registryInterpreters); + const condaProvider = new CondaEnvService(mockRegistryProvider); + + const condaExe = await condaProvider.getCondaFile(); + assert.equal(condaExe, path.join(path.dirname(condaPythonExePath), 'conda.exe'), 'Failed to identify conda.exe'); + }); +}); diff --git a/src/test/interpreters/condaHelper.test.ts b/src/test/interpreters/condaHelper.test.ts new file mode 100644 index 000000000000..45d89c3410df --- /dev/null +++ b/src/test/interpreters/condaHelper.test.ts @@ -0,0 +1,45 @@ +import * as assert from 'assert'; +import { AnacondaDisplayName, CondaInfo } from '../../client/interpreter/locators/services/conda'; +import { CondaHelper } from '../../client/interpreter/locators/services/condaHelper'; +import { initialize, initializeTest } from '../initialize'; + +// tslint:disable-next-line:max-func-body-length +suite('Interpreters display name from Conda Environments', () => { + const condaHelper = new CondaHelper(); + suiteSetup(initialize); + setup(initializeTest); + test('Must return default display name for invalid Conda Info', () => { + assert.equal(condaHelper.getDisplayName(), AnacondaDisplayName, 'Incorrect display name'); + assert.equal(condaHelper.getDisplayName({}), AnacondaDisplayName, 'Incorrect display name'); + }); + test('Must return at least Python Version', () => { + const info: CondaInfo = { + python_version: '3.6.1.final.10' + }; + const displayName = condaHelper.getDisplayName(info); + assert.equal(displayName, `Python 3.6.1 : ${AnacondaDisplayName}`, 'Incorrect display name'); + }); + test('Must return info without first part if not a python version', () => { + const info: CondaInfo = { + 'sys.version': '3.6.1 |Anaconda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]' + }; + const displayName = condaHelper.getDisplayName(info); + assert.equal(displayName, 'Anaconda 4.4.0 (64-bit)', 'Incorrect display name'); + }); + test('Must return info prefixed with word \'Python\'', () => { + const info: CondaInfo = { + python_version: '3.6.1.final.10', + 'sys.version': '3.6.1 |Anaconda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]' + }; + const displayName = condaHelper.getDisplayName(info); + assert.equal(displayName, 'Python 3.6.1 : Anaconda 4.4.0 (64-bit)', 'Incorrect display name'); + }); + test('Must include Ananconda name if Company name not found', () => { + const info: CondaInfo = { + python_version: '3.6.1.final.10', + 'sys.version': '3.6.1 |4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]' + }; + const displayName = condaHelper.getDisplayName(info); + assert.equal(displayName, `Python 3.6.1 : 4.4.0 (64-bit) : ${AnacondaDisplayName}`, 'Incorrect display name'); + }); +}); diff --git a/src/test/interpreters/display.multiroot.test.ts b/src/test/interpreters/display.multiroot.test.ts new file mode 100644 index 000000000000..c2710638b23e --- /dev/null +++ b/src/test/interpreters/display.multiroot.test.ts @@ -0,0 +1,52 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import { ConfigurationTarget, Uri, window, workspace } from 'vscode'; +import { PythonSettings } from '../../client/common/configSettings'; +import { InterpreterDisplay } from '../../client/interpreter/display'; +import { VirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs'; +import { clearPythonPathInWorkspaceFolder } from '../common'; +import { closeActiveWindows, initialize, initializePython, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; +import { MockStatusBarItem } from '../mockClasses'; +import { MockInterpreterVersionProvider } from './mocks'; +import { MockProvider } from './mocks'; + +const multirootPath = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRootWkspc'); +const workspace3Uri = Uri.file(path.join(multirootPath, 'workspace3')); +const fileToOpen = path.join(workspace3Uri.fsPath, 'file.py'); + +// tslint:disable-next-line:max-func-body-length +suite('Multiroot Interpreters Display', () => { + suiteSetup(async function () { + if (!IS_MULTI_ROOT_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await initialize(); + }); + setup(initializeTest); + suiteTeardown(initializePython); + teardown(async () => { + await clearPythonPathInWorkspaceFolder(fileToOpen); + await initialize(); + await closeActiveWindows(); + }); + + test('Must get display name from workspace folder interpreter and not from interpreter in workspace', async () => { + const settings = workspace.getConfiguration('python', Uri.file(fileToOpen)); + const pythonPath = fileToOpen; + await settings.update('pythonPath', pythonPath, ConfigurationTarget.WorkspaceFolder); + PythonSettings.dispose(); + + const document = await workspace.openTextDocument(fileToOpen); + await window.showTextDocument(document); + + const statusBar = new MockStatusBarItem(); + const provider = new MockProvider([]); + const displayName = `${path.basename(pythonPath)} [Environment]`; + const displayNameProvider = new MockInterpreterVersionProvider(displayName); + const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([]), displayNameProvider); + await display.refresh(); + + assert.equal(statusBar.text, displayName, 'Incorrect display name'); + }); +}); diff --git a/src/test/interpreters/display.test.ts b/src/test/interpreters/display.test.ts new file mode 100644 index 000000000000..0cf3271124a3 --- /dev/null +++ b/src/test/interpreters/display.test.ts @@ -0,0 +1,142 @@ +import * as assert from 'assert'; +import * as child_process from 'child_process'; +import { EOL } from 'os'; +import * as path from 'path'; +import { ConfigurationTarget, Uri, window, workspace } from 'vscode'; +import { PythonSettings } from '../../client/common/configSettings'; +import { InterpreterDisplay } from '../../client/interpreter/display'; +import { getFirstNonEmptyLineFromMultilineString } from '../../client/interpreter/helpers'; +import { VirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs'; +import { clearPythonPathInWorkspaceFolder, rootWorkspaceUri, updateSetting } from '../common'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; +import { MockStatusBarItem } from '../mockClasses'; +import { MockInterpreterVersionProvider } from './mocks'; +import { MockProvider, MockVirtualEnv } from './mocks'; + +const fileInNonRootWorkspace = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'dummy.py'); + +// tslint:disable-next-line:max-func-body-length +suite('Interpreters Display', () => { + const configTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; + suiteSetup(initialize); + setup(async () => { + await initializeTest(); + if (IS_MULTI_ROOT_TEST) { + await initializeMultiRoot(); + } + }); + teardown(async () => { + await clearPythonPathInWorkspaceFolder(fileInNonRootWorkspace); + await initialize(); + await closeActiveWindows(); + }); + test('Must have command name', () => { + const statusBar = new MockStatusBarItem(); + const displayNameProvider = new MockInterpreterVersionProvider(''); + // tslint:disable-next-line:no-unused-expression + new InterpreterDisplay(statusBar, new MockProvider([]), new VirtualEnvironmentManager([]), displayNameProvider); + assert.equal(statusBar.command, 'python.setInterpreter', 'Incorrect command name'); + }); + test('Must get display name from interpreter itself', async () => { + const statusBar = new MockStatusBarItem(); + const provider = new MockProvider([]); + const displayName = 'Mock Display Name'; + const displayNameProvider = new MockInterpreterVersionProvider(displayName); + const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([]), displayNameProvider); + await display.refresh(); + + assert.equal(statusBar.text, displayName, 'Incorrect display name'); + }); + test('Must suffix display name with name of interpreter', async () => { + const statusBar = new MockStatusBarItem(); + const provider = new MockProvider([]); + const env1 = new MockVirtualEnv(false, 'Mock 1'); + const env2 = new MockVirtualEnv(true, 'Mock 2'); + const env3 = new MockVirtualEnv(true, 'Mock 3'); + const displayName = 'Mock Display Name'; + const displayNameProvider = new MockInterpreterVersionProvider(displayName); + const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([env1, env2, env3]), displayNameProvider); + await display.refresh(); + assert.equal(statusBar.text, `${displayName} (${env2.name})`, 'Incorrect display name'); + }); + test('Must display default \'Display name\' for unknown interpreter', async () => { + const statusBar = new MockStatusBarItem(); + const provider = new MockProvider([]); + const displayName = 'Mock Display Name'; + const displayNameProvider = new MockInterpreterVersionProvider(displayName, true); + const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([]), displayNameProvider); + // Change interpreter to an invalid value + const pythonPath = 'UnknownInterpreter'; + await updateSetting('pythonPath', pythonPath, rootWorkspaceUri, configTarget); + await display.refresh(); + + const defaultDisplayName = `${path.basename(pythonPath)} [Environment]`; + assert.equal(statusBar.text, defaultDisplayName, 'Incorrect display name'); + }); + test('Must get display name from a list of interpreters', async () => { + const pythonPath = await new Promise(resolve => { + child_process.execFile(PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath, ['-c', 'import sys;print(sys.executable)'], (_, stdout) => { + resolve(getFirstNonEmptyLineFromMultilineString(stdout)); + }); + }).then(value => value.length === 0 ? PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath : value); + const statusBar = new MockStatusBarItem(); + const interpreters = [ + { displayName: 'One', path: 'c:/path1/one.exe', type: 'One 1' }, + { displayName: 'Two', path: pythonPath, type: 'Two 2' }, + { displayName: 'Three', path: 'c:/path3/three.exe', type: 'Three 3' } + ]; + const provider = new MockProvider(interpreters); + const displayName = 'Mock Display Name'; + const displayNameProvider = new MockInterpreterVersionProvider(displayName, true); + const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([]), displayNameProvider); + await display.refresh(); + + assert.equal(statusBar.text, interpreters[1].displayName, 'Incorrect display name'); + }); + test('Must suffix tooltip with the companyDisplayName of interpreter', async () => { + const pythonPath = await new Promise(resolve => { + child_process.execFile(PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath, ['-c', 'import sys;print(sys.executable)'], (_, stdout) => { + resolve(getFirstNonEmptyLineFromMultilineString(stdout)); + }); + }).then(value => value.length === 0 ? PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath : value); + + const statusBar = new MockStatusBarItem(); + const interpreters = [ + { displayName: 'One', path: 'c:/path1/one.exe', companyDisplayName: 'One 1' }, + { displayName: 'Two', path: pythonPath, companyDisplayName: 'Two 2' }, + { displayName: 'Three', path: 'c:/path3/three.exe', companyDisplayName: 'Three 3' } + ]; + const provider = new MockProvider(interpreters); + const displayNameProvider = new MockInterpreterVersionProvider(''); + const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([]), displayNameProvider); + await display.refresh(); + + assert.equal(statusBar.text, interpreters[1].displayName, 'Incorrect display name'); + assert.equal(statusBar.tooltip, `${pythonPath}${EOL}${interpreters[1].companyDisplayName}`, 'Incorrect tooltip'); + }); + test('Will update status prompting user to select an interpreter', async () => { + const statusBar = new MockStatusBarItem(); + const interpreters = [ + { displayName: 'One', path: 'c:/path1/one.exe', companyDisplayName: 'One 1' }, + { displayName: 'Two', path: 'c:/asdf', companyDisplayName: 'Two 2' }, + { displayName: 'Three', path: 'c:/path3/three.exe', companyDisplayName: 'Three 3' } + ]; + const provider = new MockProvider(interpreters); + const displayNameProvider = new MockInterpreterVersionProvider('', true); + const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([]), displayNameProvider); + // Change interpreter to an invalid value + const pythonPath = 'UnknownInterpreter'; + await updateSetting('pythonPath', pythonPath, rootWorkspaceUri, configTarget); + await display.refresh(); + + assert.equal(statusBar.text, '$(alert) Select Python Environment', 'Incorrect display name'); + }); + async function initializeMultiRoot() { + // For multiroot environments, we need a file open to determine the best interpreter that needs to be displayed + await openDummyFile(); + } + async function openDummyFile() { + const document = await workspace.openTextDocument(fileInNonRootWorkspace); + await window.showTextDocument(document); + } +}); diff --git a/src/test/interpreters/interpreterVersion.test.ts b/src/test/interpreters/interpreterVersion.test.ts new file mode 100644 index 000000000000..c0d294fee060 --- /dev/null +++ b/src/test/interpreters/interpreterVersion.test.ts @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { assert, expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import { execPythonFile } from '../../client/common/utils'; +import { getFirstNonEmptyLineFromMultilineString } from '../../client/interpreter/helpers'; +import { InterpreterVersionService } from '../../client/interpreter/interpreterVersion'; +import { initialize, initializeTest } from '../initialize'; + +use(chaiAsPromised); + +suite('Interpreters display version', () => { + const interpreterVersion = new InterpreterVersionService(); + suiteSetup(initialize); + setup(initializeTest); + + test('Must return the Python Version', async () => { + const output = await execPythonFile(undefined, 'python', ['--version'], __dirname, true); + const version = getFirstNonEmptyLineFromMultilineString(output); + const pyVersion = await interpreterVersion.getVersion('python', 'DEFAULT_TEST_VALUE'); + assert.equal(pyVersion, version, 'Incorrect version'); + }); + test('Must return the default value when Python path is invalid', async () => { + const pyVersion = await interpreterVersion.getVersion('INVALID_INTERPRETER', 'DEFAULT_TEST_VALUE'); + assert.equal(pyVersion, 'DEFAULT_TEST_VALUE', 'Incorrect version'); + }); + test('Must return the pip Version', async () => { + const output = await execPythonFile(undefined, 'python', ['-m', 'pip', '--version'], __dirname, true); + // Take the second part, see below example. + // pip 9.0.1 from /Users/donjayamanne/anaconda3/lib/python3.6/site-packages (python 3.6). + const re = new RegExp('\\d\\.\\d(\\.\\d)+', 'g'); + const matches = re.exec(output); + assert.isNotNull(matches, 'No matches for version found'); + // tslint:disable-next-line:no-non-null-assertion + assert.isAtLeast(matches!.length, 1, 'Version number not found'); + + const pipVersionPromise = interpreterVersion.getPipVersion('python'); + // tslint:disable-next-line:no-non-null-assertion + await expect(pipVersionPromise).to.eventually.equal(matches![0].trim()); + }); + test('Must throw an exceptionn when pip version cannot be determine', async () => { + const pipVersionPromise = interpreterVersion.getPipVersion('INVALID_INTERPRETER'); + await expect(pipVersionPromise).to.be.rejectedWith(); + }); +}); diff --git a/src/test/interpreters/mocks.ts b/src/test/interpreters/mocks.ts new file mode 100644 index 000000000000..db4cfed0c474 --- /dev/null +++ b/src/test/interpreters/mocks.ts @@ -0,0 +1,68 @@ +import { Architecture, Hive, IRegistry } from '../../client/common/registry'; +import { IInterpreterLocatorService, PythonInterpreter } from '../../client/interpreter/contracts'; +import { IInterpreterVersionService } from '../../client/interpreter/interpreterVersion'; +import { IVirtualEnvironment } from '../../client/interpreter/virtualEnvs/contracts'; + +export class MockProvider implements IInterpreterLocatorService { + constructor(private suggestions: PythonInterpreter[]) { + } + public getInterpreters(): Promise { + return Promise.resolve(this.suggestions); + } + // tslint:disable-next-line:no-empty + public dispose() { } +} + +export class MockRegistry implements IRegistry { + constructor(private keys: { key: string, hive: Hive, arch?: Architecture, values: string[] }[], + private values: { key: string, hive: Hive, arch?: Architecture, value: string, name?: string }[]) { + } + public getKeys(key: string, hive: Hive, arch?: Architecture): Promise { + const items = this.keys.find(item => { + if (item.arch) { + return item.key === key && item.hive === hive && item.arch === arch; + } + return item.key === key && item.hive === hive; + }); + + return items ? Promise.resolve(items.values) : Promise.resolve([]); + } + public getValue(key: string, hive: Hive, arch?: Architecture, name?: string): Promise { + const items = this.values.find(item => { + if (item.key !== key || item.hive !== hive) { + return false; + } + if (item.arch && item.arch !== arch) { + return false; + } + if (name && item.name !== name) { + return false; + } + return true; + }); + + return items ? Promise.resolve(items.value) : Promise.resolve(null); + } +} + +export class MockVirtualEnv implements IVirtualEnvironment { + constructor(private isDetected: boolean, public name: string) { + } + public detect(pythonPath: string): Promise { + return Promise.resolve(this.isDetected); + } +} + +// tslint:disable-next-line:max-classes-per-file +export class MockInterpreterVersionProvider implements IInterpreterVersionService { + constructor(private displayName: string, private useDefaultDisplayName: boolean = false, + private pipVersionPromise?: Promise) { } + public getVersion(pythonPath: string, defaultDisplayName: string): Promise { + return this.useDefaultDisplayName ? Promise.resolve(defaultDisplayName) : Promise.resolve(this.displayName); + } + public getPipVersion(pythonPath: string): Promise { + return this.pipVersionPromise; + } + // tslint:disable-next-line:no-empty + public dispose() { } +} diff --git a/src/test/interpreters/pythonPathUpdater.multiroot.test.ts b/src/test/interpreters/pythonPathUpdater.multiroot.test.ts new file mode 100644 index 000000000000..05025376fe3d --- /dev/null +++ b/src/test/interpreters/pythonPathUpdater.multiroot.test.ts @@ -0,0 +1,71 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import { ConfigurationTarget, Uri, workspace } from 'vscode'; +import { PythonPathUpdaterService } from '../../client/interpreter/configuration/pythonPathUpdaterService'; +import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory'; +import { WorkspaceFolderPythonPathUpdaterService } from '../../client/interpreter/configuration/services/workspaceFolderUpdaterService'; +import { WorkspacePythonPathUpdaterService } from '../../client/interpreter/configuration/services/workspaceUpdaterService'; +import { InterpreterVersionService } from '../../client/interpreter/interpreterVersion'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; + +const multirootPath = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRootWkspc'); +const workspace3Uri = Uri.file(path.join(multirootPath, 'workspace3')); + +// tslint:disable-next-line:max-func-body-length +suite('Multiroot Python Path Settings Updater', () => { + suiteSetup(async function () { + if (!IS_MULTI_ROOT_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await initialize(); + }); + setup(initializeTest); + suiteTeardown(async () => { + await closeActiveWindows(); + await initializeTest(); + }); + teardown(async () => { + await closeActiveWindows(); + await initializeTest(); + }); + + test('Updating Workspace Folder Python Path should work', async () => { + const workspaceUri = workspace3Uri; + const workspaceUpdater = new WorkspaceFolderPythonPathUpdaterService(workspace.getWorkspaceFolder(workspaceUri).uri); + const pythonPath = `xWorkspacePythonPath${new Date().getMilliseconds()}`; + await workspaceUpdater.updatePythonPath(pythonPath); + const folderValue = workspace.getConfiguration('python', workspace3Uri).inspect('pythonPath').workspaceFolderValue; + assert.equal(folderValue, pythonPath, 'Workspace Python Path not updated'); + }); + + test('Updating Workspace Folder Python Path using the factor service should work', async () => { + const workspaceUri = workspace3Uri; + const factory = new PythonPathUpdaterServiceFactory(); + const workspaceUpdater = factory.getWorkspaceFolderPythonPathConfigurationService(workspace.getWorkspaceFolder(workspaceUri).uri); + const pythonPath = `xWorkspacePythonPathFromFactory${new Date().getMilliseconds()}`; + await workspaceUpdater.updatePythonPath(pythonPath); + const folderValue = workspace.getConfiguration('python', workspace3Uri).inspect('pythonPath').workspaceFolderValue; + assert.equal(folderValue, pythonPath, 'Workspace Python Path not updated'); + }); + + test('Updating Workspace Python Path using the PythonPathUpdaterService should work', async () => { + const workspaceUri = workspace3Uri; + const updaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory(), new InterpreterVersionService()); + const pythonPath = `xWorkspacePythonPathFromUpdater${new Date().getMilliseconds()}`; + await updaterService.updatePythonPath(pythonPath, ConfigurationTarget.WorkspaceFolder, 'ui', workspace.getWorkspaceFolder(workspaceUri).uri); + const folderValue = workspace.getConfiguration('python', workspace3Uri).inspect('pythonPath').workspaceFolderValue; + assert.equal(folderValue, pythonPath, 'Workspace Python Path not updated'); + }); + + test('Python Path should be relative to workspace', async () => { + const workspaceUri = workspace.getWorkspaceFolder(workspace3Uri).uri; + const pythonInterpreter = `xWorkspacePythonPath${new Date().getMilliseconds()}`; + const pythonPath = path.join(workspaceUri.fsPath, 'x', 'y', 'z', pythonInterpreter); + const workspaceUpdater = new WorkspacePythonPathUpdaterService(workspaceUri); + await workspaceUpdater.updatePythonPath(pythonPath); + const workspaceValue = workspace.getConfiguration('python').inspect('pythonPath').workspaceValue; + // tslint:disable-next-line:no-invalid-template-strings + assert.equal(workspaceValue, path.join('${workspaceRoot}', 'x', 'y', 'z', pythonInterpreter), 'Workspace Python Path not updated'); + }); +}); diff --git a/src/test/interpreters/pythonPathUpdater.test.ts b/src/test/interpreters/pythonPathUpdater.test.ts new file mode 100644 index 000000000000..c74aba47ec07 --- /dev/null +++ b/src/test/interpreters/pythonPathUpdater.test.ts @@ -0,0 +1,89 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import { ConfigurationTarget, Uri, workspace } from 'vscode'; +import { PythonPathUpdaterService } from '../../client/interpreter/configuration/pythonPathUpdaterService'; +import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory'; +import { WorkspacePythonPathUpdaterService } from '../../client/interpreter/configuration/services/workspaceUpdaterService'; +import { InterpreterVersionService } from '../../client/interpreter/interpreterVersion'; +import { closeActiveWindows, initialize, initializeTest } from '../initialize'; + +const workspaceRoot = path.join(__dirname, '..', '..', '..', 'src', 'test'); + +// tslint:disable-next-line:max-func-body-length +suite('Python Path Settings Updater', () => { + suiteSetup(initialize); + setup(initializeTest); + suiteTeardown(async () => { + await closeActiveWindows(); + await initializeTest(); + }); + teardown(async () => { + await closeActiveWindows(); + await initializeTest(); + }); + + // Create Github issue VS Code bug (global changes not reflected immediately) + + // test('Updating Global Python Path should work', async () => { + // const globalUpdater = new GlobalPythonPathUpdaterService(); + // const pythonPath = `xGlobalPythonPath${new Date().getMilliseconds()}`; + // await globalUpdater.updatePythonPath(pythonPath); + // const globalPythonValue = workspace.getConfiguration('python').inspect('pythonPath').globalValue; + // assert.equal(globalPythonValue, pythonPath, 'Global Python Path not updated'); + // }); + + // test('Updating Global Python Path using the factory service should work', async () => { + // const globalUpdater = new PythonPathUpdaterServiceFactory().getGlobalPythonPathConfigurationService(); + // const pythonPath = `xGlobalPythonPathFromFactory${new Date().getMilliseconds()}`; + // await globalUpdater.updatePythonPath(pythonPath); + // const globalPythonValue = workspace.getConfiguration('python').inspect('pythonPath').globalValue; + // assert.equal(globalPythonValue, pythonPath, 'Global Python Path not updated'); + // }); + + // test('Updating Global Python Path using the PythonPathUpdaterService should work', async () => { + // const updaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory()); + // const pythonPath = `xGlobalPythonPathFromUpdater${new Date().getMilliseconds()}`; + // await updaterService.updatePythonPath(pythonPath, ConfigurationTarget.Global); + // const globalPythonValue = workspace.getConfiguration('python').inspect('pythonPath').globalValue; + // assert.equal(globalPythonValue, pythonPath, 'Global Python Path not updated'); + // }); + + test('Updating Workspace Python Path should work', async () => { + const workspaceUri = Uri.file(workspaceRoot); + const workspaceUpdater = new WorkspacePythonPathUpdaterService(workspace.getWorkspaceFolder(workspaceUri).uri); + const pythonPath = `xWorkspacePythonPath${new Date().getMilliseconds()}`; + await workspaceUpdater.updatePythonPath(pythonPath); + const workspaceValue = workspace.getConfiguration('python').inspect('pythonPath').workspaceValue; + assert.equal(workspaceValue, pythonPath, 'Workspace Python Path not updated'); + }); + + test('Updating Workspace Python Path using the factor service should work', async () => { + const workspaceUri = Uri.file(workspaceRoot); + const factory = new PythonPathUpdaterServiceFactory(); + const workspaceUpdater = factory.getWorkspacePythonPathConfigurationService(workspace.getWorkspaceFolder(workspaceUri).uri); + const pythonPath = `xWorkspacePythonPathFromFactory${new Date().getMilliseconds()}`; + await workspaceUpdater.updatePythonPath(pythonPath); + const workspaceValue = workspace.getConfiguration('python').inspect('pythonPath').workspaceValue; + assert.equal(workspaceValue, pythonPath, 'Workspace Python Path not updated'); + }); + + test('Updating Workspace Python Path using the PythonPathUpdaterService should work', async () => { + const workspaceUri = Uri.file(workspaceRoot); + const updaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory(), new InterpreterVersionService()); + const pythonPath = `xWorkspacePythonPathFromUpdater${new Date().getMilliseconds()}`; + await updaterService.updatePythonPath(pythonPath, ConfigurationTarget.Workspace, 'ui', workspace.getWorkspaceFolder(workspaceUri).uri); + const workspaceValue = workspace.getConfiguration('python').inspect('pythonPath').workspaceValue; + assert.equal(workspaceValue, pythonPath, 'Workspace Python Path not updated'); + }); + + test('Python Path should be relative to workspace', async () => { + const workspaceUri = workspace.getWorkspaceFolder(Uri.file(workspaceRoot)).uri; + const pythonInterpreter = `xWorkspacePythonPath${new Date().getMilliseconds()}`; + const pythonPath = path.join(workspaceUri.fsPath, 'x', 'y', 'z', pythonInterpreter); + const workspaceUpdater = new WorkspacePythonPathUpdaterService(workspaceUri); + await workspaceUpdater.updatePythonPath(pythonPath); + const workspaceValue = workspace.getConfiguration('python').inspect('pythonPath').workspaceValue; + // tslint:disable-next-line:no-invalid-template-strings + assert.equal(workspaceValue, path.join('${workspaceRoot}', 'x', 'y', 'z', pythonInterpreter), 'Workspace Python Path not updated'); + }); +}); diff --git a/src/test/interpreters/windowsRegistryService.test.ts b/src/test/interpreters/windowsRegistryService.test.ts new file mode 100644 index 000000000000..aedad057a4d0 --- /dev/null +++ b/src/test/interpreters/windowsRegistryService.test.ts @@ -0,0 +1,274 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import { initialize, initializeTest } from '../initialize'; +import { IS_WINDOWS } from '../../client/debugger/Common/Utils'; +import { WindowsRegistryService } from '../../client/interpreter/locators/services/windowsRegistryService'; +import { MockRegistry } from './mocks'; +import { Architecture, Hive } from '../../client/common/registry'; + +const environmentsPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'environments'); + +suite('Interpreters from Windows Registry', () => { + suiteSetup(() => initialize()); + setup(() => initializeTest()); + if (IS_WINDOWS) { + test('Must return an empty list (x86)', async () => { + const registry = new MockRegistry([], []); + const winRegistry = new WindowsRegistryService(registry, false); + + const interpreters = await winRegistry.getInterpreters(); + assert.equal(interpreters.length, 0, 'Incorrect number of entries'); + }); + test('Must return an empty list (x64)', async () => { + const registry = new MockRegistry([], []); + const winRegistry = new WindowsRegistryService(registry, true); + + const interpreters = await winRegistry.getInterpreters(); + assert.equal(interpreters.length, 0, 'Incorrect number of entries'); + }); + test('Must return a single entry', async () => { + const registryKeys = [ + { key: '\\Software\\Python', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One'] }, + { key: '\\Software\\Python\\Company One', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One\\Tag1'] } + ]; + const registryValues = [ + { key: '\\Software\\Python\\Company One', hive: Hive.HKCU, arch: Architecture.x86, value: 'Display Name for Company One', name: 'DisplayName' }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1') }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1', 'one.exe'), name: 'ExecutablePath' }, + { key: '\\Software\\Python\\Company One\\Tag1', hive: Hive.HKCU, arch: Architecture.x86, value: 'Version.Tag1', name: 'Version' }, + { key: '\\Software\\Python\\Company One\\Tag1', hive: Hive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag1', name: 'DisplayName' }, + ]; + const registry = new MockRegistry(registryKeys, registryValues); + const winRegistry = new WindowsRegistryService(registry, false); + + const interpreters = await winRegistry.getInterpreters(); + + assert.equal(interpreters.length, 1, 'Incorrect number of entries'); + assert.equal(interpreters[0].architecture, Architecture.x86, 'Incorrect arhictecture'); + assert.equal(interpreters[0].companyDisplayName, 'Display Name for Company One', 'Incorrect company name'); + assert.equal(interpreters[0].displayName, 'DisplayName.Tag1', 'Incorrect display name'); + assert.equal(interpreters[0].path, path.join(environmentsPath, 'path1', 'one.exe'), 'Incorrect executable path'); + assert.equal(interpreters[0].version, 'Version.Tag1', 'Incorrect version'); + }); + test('Must default names for PythonCore and exe', async () => { + const registryKeys = [ + { key: '\\Software\\Python', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\PythonCore'] }, + { key: '\\Software\\Python\\PythonCore', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\PythonCore\\Tag1'] } + ]; + const registryValues = [ + { key: '\\Software\\Python\\PythonCore\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1') } + ]; + const registry = new MockRegistry(registryKeys, registryValues); + const winRegistry = new WindowsRegistryService(registry, false); + + const interpreters = await winRegistry.getInterpreters(); + + assert.equal(interpreters.length, 1, 'Incorrect number of entries'); + assert.equal(interpreters[0].architecture, Architecture.x86, 'Incorrect arhictecture'); + assert.equal(interpreters[0].companyDisplayName, 'Python Software Foundation', 'Incorrect company name'); + assert.equal(interpreters[0].displayName, undefined, 'Incorrect display name'); + assert.equal(interpreters[0].path, path.join(environmentsPath, 'path1', 'python.exe'), 'Incorrect path'); + assert.equal(interpreters[0].version, 'Tag1', 'Incorrect version'); + }); + test(`Must ignore company 'PyLauncher'`, async () => { + const registryKeys = [ + { key: '\\Software\\Python', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\PyLauncher'] }, + { key: '\\Software\\Python\\PythonCore', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\PyLauncher\\Tag1'] } + ]; + const registryValues = [ + { key: '\\Software\\Python\\PyLauncher\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: 'c:/temp/Install Path Tag1' } + ]; + const registry = new MockRegistry(registryKeys, registryValues); + const winRegistry = new WindowsRegistryService(registry, false); + + const interpreters = await winRegistry.getInterpreters(); + + assert.equal(interpreters.length, 0, 'Incorrect number of entries'); + }); + test('Must return a single entry and when registry contains only the InstallPath', async () => { + const registryKeys = [ + { key: '\\Software\\Python', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One'] }, + { key: '\\Software\\Python\\Company One', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One\\Tag1'] }, + ]; + const registryValues = [ + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1') } + ]; + const registry = new MockRegistry(registryKeys, registryValues); + const winRegistry = new WindowsRegistryService(registry, false); + + const interpreters = await winRegistry.getInterpreters(); + + assert.equal(interpreters.length, 1, 'Incorrect number of entries'); + assert.equal(interpreters[0].architecture, Architecture.x86, 'Incorrect arhictecture'); + assert.equal(interpreters[0].companyDisplayName, 'Company One', 'Incorrect company name'); + assert.equal(interpreters[0].displayName, undefined, 'Incorrect display name'); + assert.equal(interpreters[0].path, path.join(environmentsPath, 'path1', 'python.exe'), 'Incorrect path'); + assert.equal(interpreters[0].version, 'Tag1', 'Incorrect version'); + }); + test('Must return multiple entries', async () => { + const registryKeys = [ + { key: '\\Software\\Python', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One', '\\Software\\Python\\Company Two', '\\Software\\Python\\Company Three'] }, + { key: '\\Software\\Python\\Company One', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One\\Tag1', '\\Software\\Python\\Company One\\Tag2'] }, + { key: '\\Software\\Python\\Company Two', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Two\\Tag A', '\\Software\\Python\\Company Two\\Tag B', '\\Software\\Python\\Company Two\\Tag C'] }, + { key: '\\Software\\Python\\Company Three', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Three\\Tag !'] }, + { key: '\\Software\\Python', hive: Hive.HKLM, arch: Architecture.x86, values: ['A'] }, + { key: '\\Software\\Python\\Company A', hive: Hive.HKLM, arch: Architecture.x86, values: ['Another Tag'] } + ]; + const registryValues = [ + { key: '\\Software\\Python\\Company One', hive: Hive.HKCU, arch: Architecture.x86, value: 'Display Name for Company One', name: 'DisplayName' }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1') }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1', 'python.exe'), name: 'ExecutablePath' }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path2'), name: 'Version' }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag1', name: 'DisplayName' }, + + { key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path2') }, + { key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path2', 'python.exe'), name: 'ExecutablePath' }, + + { key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path3') }, + { key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: 'Version.Tag A', name: 'Version' }, + + { key: '\\Software\\Python\\Company Two\\Tag B\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, + { key: '\\Software\\Python\\Company Two\\Tag B\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag B', name: 'DisplayName' }, + { key: '\\Software\\Python\\Company Two\\Tag C\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'scipy') }, + + { key: '\\Software\\Python\\Company Three\\Tag !\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, + + { key: '\\Software\\Python\\Company A\\Another Tag\\InstallPath', hive: Hive.HKLM, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'scipy', 'python.exe') } + ]; + const registry = new MockRegistry(registryKeys, registryValues); + const winRegistry = new WindowsRegistryService(registry, false); + + const interpreters = await winRegistry.getInterpreters(); + + assert.equal(interpreters.length, 4, 'Incorrect number of entries'); + assert.equal(interpreters[0].architecture, Architecture.x86, 'Incorrect arhictecture'); + assert.equal(interpreters[0].companyDisplayName, 'Display Name for Company One', 'Incorrect company name'); + assert.equal(interpreters[0].displayName, undefined, 'Incorrect display name'); + assert.equal(interpreters[0].path, path.join(environmentsPath, 'path1', 'python.exe'), 'Incorrect path'); + assert.equal(interpreters[0].version, 'Tag1', 'Incorrect version'); + + assert.equal(interpreters[1].architecture, Architecture.x86, 'Incorrect arhictecture'); + assert.equal(interpreters[1].companyDisplayName, 'Display Name for Company One', 'Incorrect company name'); + assert.equal(interpreters[1].displayName, undefined, 'Incorrect display name'); + assert.equal(interpreters[1].path, path.join(environmentsPath, 'path2', 'python.exe'), 'Incorrect path'); + assert.equal(interpreters[1].version, 'Tag2', 'Incorrect version'); + + assert.equal(interpreters[2].architecture, Architecture.x86, 'Incorrect arhictecture'); + assert.equal(interpreters[2].companyDisplayName, 'Company Two', 'Incorrect company name'); + assert.equal(interpreters[2].displayName, undefined, 'Incorrect display name'); + assert.equal(interpreters[2].path, path.join(environmentsPath, 'conda', 'envs', 'numpy', 'python.exe'), 'Incorrect path'); + assert.equal(interpreters[2].version, 'Tag B', 'Incorrect version'); + }); + test('Must return multiple entries excluding the invalid registry items and duplicate paths', async () => { + const registryKeys = [ + { key: '\\Software\\Python', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One', '\\Software\\Python\\Company Two', '\\Software\\Python\\Company Three', '\\Software\\Python\\Company Four', '\\Software\\Python\\Company Five', 'Missing Tag'] }, + { key: '\\Software\\Python\\Company One', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One\\Tag1', '\\Software\\Python\\Company One\\Tag2'] }, + { key: '\\Software\\Python\\Company Two', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Two\\Tag A', '\\Software\\Python\\Company Two\\Tag B', '\\Software\\Python\\Company Two\\Tag C'] }, + { key: '\\Software\\Python\\Company Three', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Three\\Tag !'] }, + { key: '\\Software\\Python\\Company Four', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Four\\Four !'] }, + { key: '\\Software\\Python\\Company Five', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Five\\Five !'] }, + { key: '\\Software\\Python', hive: Hive.HKLM, arch: Architecture.x86, values: ['A'] }, + { key: '\\Software\\Python\\Company A', hive: Hive.HKLM, arch: Architecture.x86, values: ['Another Tag'] } + ]; + const registryValues: { key: string, hive: Hive, arch?: Architecture, value: string, name?: string }[] = [ + { key: '\\Software\\Python\\Company One', hive: Hive.HKCU, arch: Architecture.x86, value: 'Display Name for Company One', name: 'DisplayName' }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy', 'python.exe'), name: 'ExecutablePath' }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: 'Version.Tag1', name: 'Version' }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag1', name: 'DisplayName' }, + + { key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'scipy') }, + { key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'scipy', 'python.exe'), name: 'ExecutablePath' }, + + { key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1') }, + { key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: 'Version.Tag A', name: 'Version' }, + + { key: '\\Software\\Python\\Company Two\\Tag B\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path2') }, + { key: '\\Software\\Python\\Company Two\\Tag B\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag B', name: 'DisplayName' }, + { key: '\\Software\\Python\\Company Two\\Tag C\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, + + { key: '\\Software\\Python\\Company Five\\Five !\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: undefined }, + + { key: '\\Software\\Python\\Company Three\\Tag !\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, + + { key: '\\Software\\Python\\Company A\\Another Tag\\InstallPath', hive: Hive.HKLM, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') } + ]; + const registry = new MockRegistry(registryKeys, registryValues); + const winRegistry = new WindowsRegistryService(registry, false); + + const interpreters = await winRegistry.getInterpreters(); + + assert.equal(interpreters.length, 4, 'Incorrect number of entries'); + assert.equal(interpreters[0].architecture, Architecture.x86, 'Incorrect arhictecture'); + assert.equal(interpreters[0].companyDisplayName, 'Display Name for Company One', 'Incorrect company name'); + assert.equal(interpreters[0].displayName, undefined, 'Incorrect display name'); + assert.equal(interpreters[0].path, path.join(environmentsPath, 'conda', 'envs', 'numpy', 'python.exe'), 'Incorrect path'); + assert.equal(interpreters[0].version, 'Tag1', 'Incorrect version'); + + assert.equal(interpreters[1].architecture, Architecture.x86, 'Incorrect arhictecture'); + assert.equal(interpreters[1].companyDisplayName, 'Display Name for Company One', 'Incorrect company name'); + assert.equal(interpreters[1].displayName, undefined, 'Incorrect display name'); + assert.equal(interpreters[1].path, path.join(environmentsPath, 'conda', 'envs', 'scipy', 'python.exe'), 'Incorrect path'); + assert.equal(interpreters[1].version, 'Tag2', 'Incorrect version'); + + assert.equal(interpreters[2].architecture, Architecture.x86, 'Incorrect arhictecture'); + assert.equal(interpreters[2].companyDisplayName, 'Company Two', 'Incorrect company name'); + assert.equal(interpreters[2].displayName, undefined, 'Incorrect display name'); + assert.equal(interpreters[2].path, path.join(environmentsPath, 'path1', 'python.exe'), 'Incorrect path'); + assert.equal(interpreters[2].version, 'Tag A', 'Incorrect version'); + }); + test('Must return multiple entries excluding the invalid registry items and nonexistent paths', async () => { + const registryKeys = [ + { key: '\\Software\\Python', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One', '\\Software\\Python\\Company Two', '\\Software\\Python\\Company Three', '\\Software\\Python\\Company Four', '\\Software\\Python\\Company Five', 'Missing Tag'] }, + { key: '\\Software\\Python\\Company One', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One\\Tag1', '\\Software\\Python\\Company One\\Tag2'] }, + { key: '\\Software\\Python\\Company Two', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Two\\Tag A', '\\Software\\Python\\Company Two\\Tag B', '\\Software\\Python\\Company Two\\Tag C'] }, + { key: '\\Software\\Python\\Company Three', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Three\\Tag !'] }, + { key: '\\Software\\Python\\Company Four', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Four\\Four !'] }, + { key: '\\Software\\Python\\Company Five', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Five\\Five !'] }, + { key: '\\Software\\Python', hive: Hive.HKLM, arch: Architecture.x86, values: ['A'] }, + { key: '\\Software\\Python\\Company A', hive: Hive.HKLM, arch: Architecture.x86, values: ['Another Tag'] } + ]; + const registryValues: { key: string, hive: Hive, arch?: Architecture, value: string, name?: string }[] = [ + { key: '\\Software\\Python\\Company One', hive: Hive.HKCU, arch: Architecture.x86, value: 'Display Name for Company One', name: 'DisplayName' }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy', 'python.exe'), name: 'ExecutablePath' }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: 'Version.Tag1', name: 'Version' }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag1', name: 'DisplayName' }, + + { key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path', 'envs', 'scipy') }, + { key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path', 'envs', 'scipy', 'python.exe'), name: 'ExecutablePath' }, + + { key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path') }, + { key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: 'Version.Tag A', name: 'Version' }, + + { key: '\\Software\\Python\\Company Two\\Tag B\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path2') }, + { key: '\\Software\\Python\\Company Two\\Tag B\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag B', name: 'DisplayName' }, + { key: '\\Software\\Python\\Company Two\\Tag C\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path', 'envs', 'numpy') }, + + { key: '\\Software\\Python\\Company Five\\Five !\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: undefined }, + + { key: '\\Software\\Python\\Company Three\\Tag !\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path', 'envs', 'numpy') }, + + { key: '\\Software\\Python\\Company A\\Another Tag\\InstallPath', hive: Hive.HKLM, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path', 'envs', 'numpy') } + ]; + const registry = new MockRegistry(registryKeys, registryValues); + const winRegistry = new WindowsRegistryService(registry, false); + + const interpreters = await winRegistry.getInterpreters(); + + assert.equal(interpreters.length, 2, 'Incorrect number of entries'); + + assert.equal(interpreters[0].architecture, Architecture.x86, '1. Incorrect arhictecture'); + assert.equal(interpreters[0].companyDisplayName, 'Display Name for Company One', '1. Incorrect company name'); + assert.equal(interpreters[0].displayName, undefined, '1. Incorrect display name'); + assert.equal(interpreters[0].path, path.join(environmentsPath, 'conda', 'envs', 'numpy', 'python.exe'), '1. Incorrect path'); + assert.equal(interpreters[0].version, 'Tag1', '1. Incorrect version'); + + assert.equal(interpreters[1].architecture, Architecture.x86, '2. Incorrect arhictecture'); + assert.equal(interpreters[1].companyDisplayName, 'Company Two', '2. Incorrect company name'); + assert.equal(interpreters[1].displayName, undefined, '2. Incorrect display name'); + assert.equal(interpreters[1].path, path.join(environmentsPath, 'path2', 'python.exe'), '2. Incorrect path'); + assert.equal(interpreters[1].version, 'Tag B', '2. Incorrect version'); + }); + } +}); diff --git a/src/test/extension.jupyter.comms.jupyter.codeHelper.test.ts b/src/test/jupyter/jupyter.codeHelper.test.ts similarity index 70% rename from src/test/extension.jupyter.comms.jupyter.codeHelper.test.ts rename to src/test/jupyter/jupyter.codeHelper.test.ts index c0f4424acc0c..27b529b72530 100644 --- a/src/test/extension.jupyter.comms.jupyter.codeHelper.test.ts +++ b/src/test/jupyter/jupyter.codeHelper.test.ts @@ -1,42 +1,28 @@ -// -// Note: This example test is leveraging the Mocha test framework. -// Please refer to their documentation on https://mochajs.org/ for help. -// -// Place this right on top -import { initialize, IS_TRAVIS, PYTHON_PATH, closeActiveWindows } from './initialize'; -// The module 'assert' provides assertion methods from node import * as assert from 'assert'; import * as vscode from 'vscode'; -import * as settings from '../client/common/configSettings'; import * as path from 'path'; -import { CodeHelper } from '../client/jupyter/common/codeHelper'; -import { JupyterCodeLensProvider } from '../client/jupyter/editorIntegration/codeLensProvider'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; +import { CodeHelper } from '../../client/jupyter/common/codeHelper'; +import { JupyterCodeLensProvider } from '../../client/jupyter/editorIntegration/codeLensProvider'; -let pythonSettings = settings.PythonSettings.getInstance(); -const FILE_WITH_CELLS = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'jupyter', 'cells.py'); +const FILE_WITH_CELLS = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'jupyter', 'cells.py'); suite('Jupyter Code Helper', () => { - suiteSetup(done => { - initialize().then(() => { - if (IS_TRAVIS) { - pythonSettings.pythonPath = PYTHON_PATH; - } - done(); - }); + suiteSetup(async function () { + if (IS_MULTI_ROOT_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await initialize(); }); - + setup(() => initializeTest()); + teardown(() => closeActiveWindows()); const codeLensProvider = new JupyterCodeLensProvider(); const codeHelper = new CodeHelper(codeLensProvider); - setup(done => { - closeActiveWindows().then(done).catch(done); - }); - teardown(done => { - closeActiveWindows().then(done).catch(done); - }); test('Get Line (without any selection)', done => { let textDocument: vscode.TextDocument; - return vscode.workspace.openTextDocument(FILE_WITH_CELLS).then(document => { + vscode.workspace.openTextDocument(FILE_WITH_CELLS).then(document => { textDocument = document; return vscode.window.showTextDocument(textDocument); }).then(editor => { @@ -50,7 +36,7 @@ suite('Jupyter Code Helper', () => { test('Get Selected Text', done => { let textDocument: vscode.TextDocument; - return vscode.workspace.openTextDocument(FILE_WITH_CELLS).then(document => { + vscode.workspace.openTextDocument(FILE_WITH_CELLS).then(document => { textDocument = document; return vscode.window.showTextDocument(textDocument); }).then(editor => { @@ -64,7 +50,7 @@ suite('Jupyter Code Helper', () => { test('Get Selected Line', done => { let textDocument: vscode.TextDocument; - return vscode.workspace.openTextDocument(FILE_WITH_CELLS).then(document => { + vscode.workspace.openTextDocument(FILE_WITH_CELLS).then(document => { textDocument = document; return vscode.window.showTextDocument(textDocument); }).then(editor => { @@ -79,7 +65,7 @@ suite('Jupyter Code Helper', () => { test('Get Selected Text (multi-line)', done => { let textDocument: vscode.TextDocument; - return vscode.workspace.openTextDocument(FILE_WITH_CELLS).then(document => { + vscode.workspace.openTextDocument(FILE_WITH_CELLS).then(document => { textDocument = document; return vscode.window.showTextDocument(textDocument); }).then(editor => { @@ -93,7 +79,7 @@ suite('Jupyter Code Helper', () => { test('Get Code Block (for in)', done => { let textDocument: vscode.TextDocument; - return vscode.workspace.openTextDocument(FILE_WITH_CELLS).then(document => { + vscode.workspace.openTextDocument(FILE_WITH_CELLS).then(document => { textDocument = document; return vscode.window.showTextDocument(textDocument); }).then(editor => { @@ -112,4 +98,4 @@ suite('Jupyter Code Helper', () => { // Add parameters being broken into multiple lines // e.g. x = doSomething(1,2, // 4,5) -}); \ No newline at end of file +}); diff --git a/src/test/extension.jupyter.comms.jupyterClient.test.ts b/src/test/jupyter/jupyterClient.test.ts similarity index 89% rename from src/test/extension.jupyter.comms.jupyterClient.test.ts rename to src/test/jupyter/jupyterClient.test.ts index 223901cce53e..d867de63b03d 100644 --- a/src/test/extension.jupyter.comms.jupyterClient.test.ts +++ b/src/test/jupyter/jupyterClient.test.ts @@ -1,91 +1,38 @@ -// -// Note: This example test is leveraging the Mocha test framework. -// Please refer to their documentation on https://mochajs.org/ for help. -// -// Place this right on top -import { initialize, IS_TRAVIS, PYTHON_PATH, TEST_TIMEOUT } from './initialize'; import * as assert from 'assert'; -import * as vscode from 'vscode'; -import { JupyterClientAdapter } from '../client/jupyter/jupyter_client/main'; -import { KernelRestartedError, KernelShutdownError } from '../client/jupyter/common/errors'; -import { createDeferred } from '../client/common/helpers'; -import { KernelspecMetadata } from '../client/jupyter/contracts'; -import * as settings from '../client/common/configSettings'; - -let pythonSettings = settings.PythonSettings.getInstance(); - -export class MockOutputChannel implements vscode.OutputChannel { - constructor(name: string) { - this.name = name; - this.output = ''; - this.timeOut = setTimeout(() => { - console.log(this.output); - this.writeToConsole = true; - this.timeOut = null; - }, TEST_TIMEOUT - 1000); - } - private timeOut: number; - name: string; - output: string; - isShown: boolean; - private writeToConsole: boolean; - append(value: string) { - this.output += value; - if (this.writeToConsole) { - console.log(value); - } - } - appendLine(value: string) { - this.append(value); this.append('\n'); - if (this.writeToConsole) { - console.log(value); - console.log('\n'); - } - } - clear() { } - show(preservceFocus?: boolean): void; - show(column?: vscode.ViewColumn, preserveFocus?: boolean): void; - show(x?: any, y?: any): void { - this.isShown = true; - } - hide() { - this.isShown = false; - } - dispose() { - if (this.timeOut) { - clearTimeout(this.timeOut); - this.timeOut = null; - } - } -} +import { MockOutputChannel } from './mocks'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; +import { JupyterClientAdapter } from '../../client/jupyter/jupyter_client/main'; +import { KernelRestartedError, KernelShutdownError } from '../../client/jupyter/common/errors'; +import { createDeferred } from '../../client/common/helpers'; +import { KernelspecMetadata } from '../../client/jupyter/contracts'; suite('JupyterClient', () => { - suiteSetup(done => { - initialize().then(() => { - if (IS_TRAVIS) { - pythonSettings.pythonPath = PYTHON_PATH; - } - done(); - }); - setup(() => { - process.env['PYTHON_DONJAYAMANNE_TEST'] = '0'; - process.env['DEBUG_DJAYAMANNE_IPYTHON'] = '1'; - output = new MockOutputChannel('Jupyter'); - jupyter = new JupyterClientAdapter(output, __dirname); - }); - teardown(() => { - process.env['PYTHON_DONJAYAMANNE_TEST'] = '1'; - process.env['DEBUG_DJAYAMANNE_IPYTHON'] = '0'; - output.dispose(); - jupyter.dispose(); - }); + suiteSetup(async function () { + if (IS_MULTI_ROOT_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await initialize(); + }); + setup(() => { + process.env['VSC_PYTHON_CI_TEST'] = '0'; + process.env['DEBUG_EXTENSION_IPYTHON'] = '1'; + output = new MockOutputChannel('Jupyter'); + jupyter = new JupyterClientAdapter(output, __dirname); + return initializeTest(); + }); + teardown(() => { + process.env['VSC_PYTHON_CI_TEST'] = '1'; + process.env['DEBUG_EXTENSION_IPYTHON'] = '0'; + output.dispose(); + jupyter.dispose(); }); let output: MockOutputChannel; let jupyter: JupyterClientAdapter; test('Ping (Process and Socket)', done => { - jupyter.start({ 'PYTHON_DONJAYAMANNE_TEST': '1', 'DEBUG_DJAYAMANNE_IPYTHON': '1' }).then(() => { + jupyter.start({ 'VSC_PYTHON_CI_TEST': '1', 'DEBUG_EXTENSION_IPYTHON': '1' }).then(() => { done(); }).catch(reason => { assert.fail(reason, undefined, 'Starting Jupyter failed', ''); @@ -141,7 +88,7 @@ suite('JupyterClient', () => { }); test('Start Kernel (without start)', done => { jupyter.getAllKernelSpecs().then(kernelSpecs => { - process.env['PYTHON_DONJAYAMANNE_TEST'] = '0'; + process.env['VSC_PYTHON_CI_TEST'] = '0'; // Ok we got the kernelspecs, now create another new jupyter client // and tell it to start a specific kernel @@ -157,7 +104,7 @@ suite('JupyterClient', () => { done(); }); - process.env['PYTHON_DONJAYAMANNE_TEST'] = '1'; + process.env['VSC_PYTHON_CI_TEST'] = '1'; }).catch(reason => { assert.fail(reason, undefined, 'Failed to retrieve kernelspecs', ''); @@ -467,7 +414,7 @@ suite('JupyterClient', () => { assert.fail(reason, undefined, 'Failed to retrieve kernelspecs', ''); done(); }); - process.env['PYTHON_DONJAYAMANNE_TEST'] = '1'; + process.env['VSC_PYTHON_CI_TEST'] = '1'; }); test('Execute multiple blocks of Code', done => { jupyter.start().then(() => { @@ -541,6 +488,7 @@ suite('JupyterClient', () => { output.push(data); }, reason => { assert.fail(reason, null, 'Code execution failed in jupyter', ''); + done(); }, () => { assert.equal(output.some(d => d.stream === 'pyout' && d.type === 'text' && d.data['text/plain'] === '3'), true, 'pyout not found in output'); assert.equal(output.some(d => d.stream === 'status' && d.type === 'text' && d.data === 'ok'), true, 'status not found in output'); @@ -550,6 +498,7 @@ suite('JupyterClient', () => { }); }).catch(reason => { assert.fail(reason, undefined, 'Failed to retrieve kernelspecs', ''); + done(); }); }); -}); \ No newline at end of file +}); diff --git a/src/test/extension.jupyter.comms.jupyterKernel.test.ts b/src/test/jupyter/jupyterKernel.test.ts similarity index 84% rename from src/test/extension.jupyter.comms.jupyterKernel.test.ts rename to src/test/jupyter/jupyterKernel.test.ts index 054093350a5c..0159df780a22 100644 --- a/src/test/extension.jupyter.comms.jupyterKernel.test.ts +++ b/src/test/jupyter/jupyterKernel.test.ts @@ -1,97 +1,40 @@ -// -// Note: This example test is leveraging the Mocha test framework. -// Please refer to their documentation on https://mochajs.org/ for help. -// -// Place this right on top -import { initialize, IS_TRAVIS, PYTHON_PATH, TEST_TIMEOUT } from './initialize'; -// The module 'assert' provides assertion methods from node import * as assert from 'assert'; - -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it -import { JupyterClientAdapter } from '../client/jupyter/jupyter_client/main'; -import { KernelShutdownError } from '../client/jupyter/common/errors'; -import { createDeferred } from '../client/common/helpers'; -import { JupyterClientKernel } from '../client/jupyter/jupyter_client-Kernel'; -import { KernelspecMetadata } from '../client/jupyter/contracts'; -import * as settings from '../client/common/configSettings'; -import * as vscode from 'vscode'; - -let pythonSettings = settings.PythonSettings.getInstance(); - -export class MockOutputChannel implements vscode.OutputChannel { - constructor(name: string) { - this.name = name; - this.output = ''; - this.timeOut = setTimeout(() => { - console.log(this.output); - this.writeToConsole = true; - this.timeOut = null; - }, TEST_TIMEOUT - 1000); - } - private timeOut: number; - name: string; - output: string; - isShown: boolean; - private writeToConsole: boolean; - append(value: string) { - this.output += value; - if (this.writeToConsole) { - console.log(value); - } - } - appendLine(value: string) { - this.append(value); this.append('\n'); - if (this.writeToConsole) { - console.log(value); - console.log('\n'); - } - } - clear() { } - show(preservceFocus?: boolean): void; - show(column?: vscode.ViewColumn, preserveFocus?: boolean): void; - show(x?: any, y?: any): void { - this.isShown = true; - } - hide() { - this.isShown = false; - } - dispose() { - if (this.timeOut) { - clearTimeout(this.timeOut); - this.timeOut = null; - } - } -} +import { MockOutputChannel } from './mocks'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; +import { JupyterClientAdapter } from '../../client/jupyter/jupyter_client/main'; +import { KernelShutdownError } from '../../client/jupyter/common/errors'; +import { createDeferred } from '../../client/common/helpers'; +import { JupyterClientKernel } from '../../client/jupyter/jupyter_client-Kernel'; +import { KernelspecMetadata } from '../../client/jupyter/contracts'; suite('Jupyter Kernel', () => { - suiteSetup(done => { - initialize().then(() => { - if (IS_TRAVIS) { - pythonSettings.pythonPath = PYTHON_PATH; + suiteSetup(async function () { + if (IS_MULTI_ROOT_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await initialize(); + }); + setup(() => { + process.env['VSC_PYTHON_CI_TEST'] = '0'; + process.env['DEBUG_EXTENSION_IPYTHON'] = '1'; + disposables = []; + output = new MockOutputChannel('Jupyter'); + disposables.push(output); + jupyter = new JupyterClientAdapter(output, __dirname); + disposables.push(jupyter); + return initializeTest(); + }); + teardown(() => { + process.env['VSC_PYTHON_CI_TEST'] = '1'; + process.env['DEBUG_EXTENSION_IPYTHON'] = '0'; + output.dispose(); + jupyter.dispose(); + disposables.forEach(d => { + try { + d.dispose(); + } catch (error) { } - done(); - }); - setup(() => { - process.env['PYTHON_DONJAYAMANNE_TEST'] = '0'; - process.env['DEBUG_DJAYAMANNE_IPYTHON'] = '1'; - disposables = []; - output = new MockOutputChannel('Jupyter'); - disposables.push(output); - jupyter = new JupyterClientAdapter(output, __dirname); - disposables.push(jupyter); - }); - teardown(() => { - process.env['PYTHON_DONJAYAMANNE_TEST'] = '1'; - process.env['DEBUG_DJAYAMANNE_IPYTHON'] = '0'; - output.dispose(); - jupyter.dispose(); - disposables.forEach(d => { - try { - d.dispose(); - } catch (error) { - } - }); }); }); @@ -375,6 +318,7 @@ suite('Jupyter Kernel', () => { output.push(data); }, reason => { assert.fail(reason, null, 'Code execution failed in jupyter', ''); + done(); }, () => { assert.equal(output.some(d => d.stream === 'pyout' && d.type === 'text' && d.data['text/plain'] === '3'), true, 'pyout not found in output'); assert.equal(output.some(d => d.stream === 'status' && d.type === 'text' && d.data === 'ok'), true, 'status not found in output'); @@ -384,6 +328,7 @@ suite('Jupyter Kernel', () => { }); }).catch(reason => { assert.fail(reason, undefined, 'Failed to interrupt Kernel', ''); + done(); }); }); -}); \ No newline at end of file +}); diff --git a/src/test/jupyter/jupyterKernelManager.test.ts b/src/test/jupyter/jupyterKernelManager.test.ts new file mode 100644 index 000000000000..a70aa386a184 --- /dev/null +++ b/src/test/jupyter/jupyterKernelManager.test.ts @@ -0,0 +1,83 @@ +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import { MockOutputChannel } from './mocks'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; +import { JupyterClientAdapter } from '../../client/jupyter/jupyter_client/main'; +import { KernelManagerImpl } from '../../client/jupyter/kernel-manager'; + +suite('Jupyter Kernel Manager', () => { + suiteSetup(async function () { + if (IS_MULTI_ROOT_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await initialize(); + }); + setup(() => { + process.env['VSC_PYTHON_CI_TEST'] = '0'; + process.env['DEBUG_EXTENSION_IPYTHON'] = '1'; + disposables = []; + output = new MockOutputChannel('Jupyter'); + disposables.push(output); + jupyter = new JupyterClientAdapter(output, __dirname); + disposables.push(jupyter); + // Hack hack hack hack hack :) + cmds.registerCommand = function () { }; + return initializeTest(); + }); + teardown(() => { + process.env['VSC_PYTHON_CI_TEST'] = '1'; + process.env['DEBUG_EXTENSION_IPYTHON'] = '0'; + output.dispose(); + jupyter.dispose(); + disposables.forEach(d => { + try { + d.dispose(); + } catch (error) { + } + }); + cmds.registerCommand = oldRegisterCommand; + }); + + let output: MockOutputChannel; + let jupyter: JupyterClientAdapter; + let disposables: { dispose: Function }[]; + const cmds = (vscode.commands as any); + const oldRegisterCommand = vscode.commands.registerCommand; + + test('GetAllKernelSpecsFor python', done => { + process.env['VSC_PYTHON_CI_TEST'] = '0'; + const mgr = new KernelManagerImpl(output, jupyter); + disposables.push(mgr); + mgr.getAllKernelSpecsFor('python').then(specMetadata => { + assert.notEqual(specMetadata.length, 0, 'No spec metatadata'); + done(); + }).catch(reason => { + assert.fail(reason, null, 'Some error', ''); + }); + }); + test('Start a kernel', done => { + const mgr = new KernelManagerImpl(output, jupyter); + disposables.push(mgr); + mgr.getAllKernelSpecsFor('python').then(specMetadata => { + assert.notEqual(specMetadata.length, 0, 'No spec metatadata'); + return mgr.startKernel(specMetadata[0], 'python'); + }).then(kernel => { + assert.equal(typeof kernel === 'object' && kernel !== null, true, 'Kernel instance not returned'); + done(); + }).catch(reason => { + assert.fail(reason, null, 'Some error', ''); + }); + }); + test('Start any kernel for Python', done => { + const mgr = new KernelManagerImpl(output, jupyter); + disposables.push(mgr); + mgr.startKernelFor('python').then(kernel => { + assert.equal(typeof kernel === 'object' && kernel !== null, true, 'Kernel instance not returned'); + done(); + }).catch(reason => { + assert.fail(reason, null, 'Some error', ''); + done(); + }); + }); +}); diff --git a/src/test/jupyter/mocks.ts b/src/test/jupyter/mocks.ts new file mode 100644 index 000000000000..9bcbf3692bf9 --- /dev/null +++ b/src/test/jupyter/mocks.ts @@ -0,0 +1,47 @@ +import * as vscode from 'vscode'; +import { TEST_TIMEOUT } from './../initialize'; + +export class MockOutputChannel implements vscode.OutputChannel { + constructor(name: string) { + this.name = name; + this.output = ''; + this.timeOut = setTimeout(() => { + console.log(this.output); + this.writeToConsole = true; + this.timeOut = null; + }, TEST_TIMEOUT - 1000); + } + private timeOut: number; + name: string; + output: string; + isShown: boolean; + private writeToConsole: boolean; + append(value: string) { + this.output += value; + if (this.writeToConsole) { + console.log(value); + } + } + appendLine(value: string) { + this.append(value); this.append('\n'); + if (this.writeToConsole) { + console.log(value); + console.log('\n'); + } + } + clear() { } + show(preservceFocus?: boolean): void; + show(column?: vscode.ViewColumn, preserveFocus?: boolean): void; + show(x?: any, y?: any): void { + this.isShown = true; + } + hide() { + this.isShown = false; + } + dispose() { + if (this.timeOut) { + clearTimeout(this.timeOut); + this.timeOut = null; + } + } +} diff --git a/src/test/linters/lint.multiroot.test.ts b/src/test/linters/lint.multiroot.test.ts new file mode 100644 index 000000000000..5ad0f11b6cbb --- /dev/null +++ b/src/test/linters/lint.multiroot.test.ts @@ -0,0 +1,84 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import { CancellationTokenSource, ConfigurationTarget, Uri, window, workspace } from 'vscode'; +import { PythonSettings } from '../../client/common/configSettings'; +import * as baseLinter from '../../client/linters/baseLinter'; +import * as flake8 from '../../client/linters/flake8'; +import * as pyLint from '../../client/linters/pylint'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; +import { MockOutputChannel } from '../mockClasses'; + +const multirootPath = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRootWkspc'); + +suite('Multiroot Linting', () => { + suiteSetup(function () { + if (!IS_MULTI_ROOT_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + return initialize(); + }); + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(async () => { + await closeActiveWindows(); + PythonSettings.dispose(); + }); + + async function testLinterInWorkspaceFolder(linter: baseLinter.BaseLinter, workspaceFolderRelativePath: string, mustHaveErrors: boolean) { + const fileToLint = path.join(multirootPath, workspaceFolderRelativePath, 'file.py'); + const cancelToken = new CancellationTokenSource(); + const document = await workspace.openTextDocument(fileToLint); + const editor = await window.showTextDocument(document); + const messages = await linter.lint(editor.document, cancelToken.token); + const errorMessage = mustHaveErrors ? 'No errors returned by linter' : 'Errors returned by linter'; + assert.equal(messages.length > 0, mustHaveErrors, errorMessage); + } + async function enableDisableSetting(workspaceFolder, configTarget: ConfigurationTarget, setting: string, value: boolean) { + const folderUri = Uri.file(workspaceFolder); + const settings = workspace.getConfiguration('python.linting', folderUri); + await settings.update(setting, value, configTarget); + } + + test('Enabling Pylint in root and also in Workspace, should return errors', async () => { + const ch = new MockOutputChannel('Lint'); + await enableDisableSetting(multirootPath, ConfigurationTarget.Workspace, 'pylintEnabled', true); + await enableDisableSetting(path.join(multirootPath, 'workspace1'), ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', true); + await testLinterInWorkspaceFolder(new pyLint.Linter(ch), 'workspace1', true); + }); + + test('Enabling Pylint in root and disabling in Workspace, should not return errors', async () => { + const ch = new MockOutputChannel('Lint'); + await enableDisableSetting(multirootPath, ConfigurationTarget.Workspace, 'pylintEnabled', true); + await enableDisableSetting(path.join(multirootPath, 'workspace1'), ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', false); + await testLinterInWorkspaceFolder(new pyLint.Linter(ch), 'workspace1', false); + }); + + test('Disabling Pylint in root and enabling in Workspace, should return errors', async () => { + const ch = new MockOutputChannel('Lint'); + await enableDisableSetting(multirootPath, ConfigurationTarget.Workspace, 'pylintEnabled', false); + await enableDisableSetting(path.join(multirootPath, 'workspace1'), ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', true); + await testLinterInWorkspaceFolder(new pyLint.Linter(ch), 'workspace1', true); + }); + + test('Enabling Flake8 in root and also in Workspace, should return errors', async () => { + const ch = new MockOutputChannel('Lint'); + await enableDisableSetting(multirootPath, ConfigurationTarget.Workspace, 'flake8Enabled', true); + await enableDisableSetting(path.join(multirootPath, 'workspace1'), ConfigurationTarget.WorkspaceFolder, 'flake8Enabled', true); + await testLinterInWorkspaceFolder(new flake8.Linter(ch), 'workspace1', true); + }); + + test('Enabling Flake8 in root and disabling in Workspace, should not return errors', async () => { + const ch = new MockOutputChannel('Lint'); + await enableDisableSetting(multirootPath, ConfigurationTarget.Workspace, 'flake8Enabled', true); + await enableDisableSetting(path.join(multirootPath, 'workspace1'), ConfigurationTarget.WorkspaceFolder, 'flake8Enabled', false); + await testLinterInWorkspaceFolder(new flake8.Linter(ch), 'workspace1', false); + }); + + test('Disabling Flake8 in root and enabling in Workspace, should return errors', async () => { + const ch = new MockOutputChannel('Lint'); + await enableDisableSetting(multirootPath, ConfigurationTarget.Workspace, 'flake8Enabled', false); + await enableDisableSetting(path.join(multirootPath, 'workspace1'), ConfigurationTarget.WorkspaceFolder, 'flake8Enabled', true); + await testLinterInWorkspaceFolder(new flake8.Linter(ch), 'workspace1', true); + }); +}); diff --git a/src/test/linters/lint.test.ts b/src/test/linters/lint.test.ts new file mode 100644 index 000000000000..6eaa262f398e --- /dev/null +++ b/src/test/linters/lint.test.ts @@ -0,0 +1,277 @@ +import * as assert from 'assert'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { PythonSettings } from '../../client/common/configSettings'; +import { createDeferred } from '../../client/common/helpers'; +import { SettingToDisableProduct } from '../../client/common/installer'; +import { execPythonFile } from '../../client/common/utils'; +import * as baseLinter from '../../client/linters/baseLinter'; +import * as flake8 from '../../client/linters/flake8'; +import * as pep8 from '../../client/linters/pep8Linter'; +import * as prospector from '../../client/linters/prospector'; +import * as pydocstyle from '../../client/linters/pydocstyle'; +import * as pyLint from '../../client/linters/pylint'; +import { PythonSettingKeys, rootWorkspaceUri, updateSetting } from '../common'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; +import { MockOutputChannel } from '../mockClasses'; + +const pythoFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'linting'); +const flake8ConfigPath = path.join(pythoFilesPath, 'flake8config'); +const pep8ConfigPath = path.join(pythoFilesPath, 'pep8config'); +const pydocstyleConfigPath27 = path.join(pythoFilesPath, 'pydocstyleconfig27'); +const pylintConfigPath = path.join(pythoFilesPath, 'pylintconfig'); +const fileToLint = path.join(pythoFilesPath, 'file.py'); +let pylintFileToLintLines: string[] = []; + +const pylintMessagesToBeReturned: baseLinter.ILintMessage[] = [ + { line: 24, column: 0, severity: baseLinter.LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', provider: '', type: '' }, + { line: 30, column: 0, severity: baseLinter.LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', provider: '', type: '' }, + { line: 34, column: 0, severity: baseLinter.LintMessageSeverity.Information, code: 'I0012', message: 'Locally enabling no-member (E1101)', provider: '', type: '' }, + { line: 40, column: 0, severity: baseLinter.LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', provider: '', type: '' }, + { line: 44, column: 0, severity: baseLinter.LintMessageSeverity.Information, code: 'I0012', message: 'Locally enabling no-member (E1101)', provider: '', type: '' }, + { line: 55, column: 0, severity: baseLinter.LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', provider: '', type: '' }, + { line: 59, column: 0, severity: baseLinter.LintMessageSeverity.Information, code: 'I0012', message: 'Locally enabling no-member (E1101)', provider: '', type: '' }, + { line: 62, column: 0, severity: baseLinter.LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling undefined-variable (E0602)', provider: '', type: '' }, + { line: 70, column: 0, severity: baseLinter.LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', provider: '', type: '' }, + { line: 84, column: 0, severity: baseLinter.LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', provider: '', type: '' }, + { line: 87, column: 0, severity: baseLinter.LintMessageSeverity.Hint, code: 'C0304', message: 'Final newline missing', provider: '', type: '' }, + { line: 11, column: 20, severity: baseLinter.LintMessageSeverity.Warning, code: 'W0613', message: 'Unused argument \'arg\'', provider: '', type: '' }, + { line: 26, column: 14, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blop\' member', provider: '', type: '' }, + { line: 36, column: 14, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: '' }, + { line: 46, column: 18, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: '' }, + { line: 61, column: 18, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: '' }, + { line: 72, column: 18, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: '' }, + { line: 75, column: 18, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: '' }, + { line: 77, column: 14, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: '' }, + { line: 83, column: 14, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: '' } +]; +const flake8MessagesToBeReturned: baseLinter.ILintMessage[] = [ + { line: 5, column: 1, severity: baseLinter.LintMessageSeverity.Error, code: 'E302', message: 'expected 2 blank lines, found 1', provider: '', type: '' }, + { line: 19, column: 15, severity: baseLinter.LintMessageSeverity.Error, code: 'E127', message: 'continuation line over-indented for visual indent', provider: '', type: '' }, + { line: 24, column: 23, severity: baseLinter.LintMessageSeverity.Error, code: 'E261', message: 'at least two spaces before inline comment', provider: '', type: '' }, + { line: 62, column: 30, severity: baseLinter.LintMessageSeverity.Error, code: 'E261', message: 'at least two spaces before inline comment', provider: '', type: '' }, + { line: 70, column: 22, severity: baseLinter.LintMessageSeverity.Error, code: 'E261', message: 'at least two spaces before inline comment', provider: '', type: '' }, + { line: 80, column: 5, severity: baseLinter.LintMessageSeverity.Error, code: 'E303', message: 'too many blank lines (2)', provider: '', type: '' }, + { line: 87, column: 24, severity: baseLinter.LintMessageSeverity.Warning, code: 'W292', message: 'no newline at end of file', provider: '', type: '' } +]; +const pep8MessagesToBeReturned: baseLinter.ILintMessage[] = [ + { line: 5, column: 1, severity: baseLinter.LintMessageSeverity.Error, code: 'E302', message: 'expected 2 blank lines, found 1', provider: '', type: '' }, + { line: 19, column: 15, severity: baseLinter.LintMessageSeverity.Error, code: 'E127', message: 'continuation line over-indented for visual indent', provider: '', type: '' }, + { line: 24, column: 23, severity: baseLinter.LintMessageSeverity.Error, code: 'E261', message: 'at least two spaces before inline comment', provider: '', type: '' }, + { line: 62, column: 30, severity: baseLinter.LintMessageSeverity.Error, code: 'E261', message: 'at least two spaces before inline comment', provider: '', type: '' }, + { line: 70, column: 22, severity: baseLinter.LintMessageSeverity.Error, code: 'E261', message: 'at least two spaces before inline comment', provider: '', type: '' }, + { line: 80, column: 5, severity: baseLinter.LintMessageSeverity.Error, code: 'E303', message: 'too many blank lines (2)', provider: '', type: '' }, + { line: 87, column: 24, severity: baseLinter.LintMessageSeverity.Warning, code: 'W292', message: 'no newline at end of file', provider: '', type: '' } +]; +const pydocstyleMessagseToBeReturned: baseLinter.ILintMessage[] = [ + { code: 'D400', severity: baseLinter.LintMessageSeverity.Information, message: 'First line should end with a period (not \'e\')', column: 0, line: 1, type: '', provider: 'pydocstyle' }, + { code: 'D400', severity: baseLinter.LintMessageSeverity.Information, message: 'First line should end with a period (not \'t\')', column: 0, line: 5, type: '', provider: 'pydocstyle' }, + { code: 'D102', severity: baseLinter.LintMessageSeverity.Information, message: 'Missing docstring in public method', column: 4, line: 8, type: '', provider: 'pydocstyle' }, + { code: 'D401', severity: baseLinter.LintMessageSeverity.Information, message: 'First line should be in imperative mood (\'thi\', not \'this\')', column: 4, line: 11, type: '', provider: 'pydocstyle' }, + { code: 'D403', severity: baseLinter.LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'This\', not \'this\')', column: 4, line: 11, type: '', provider: 'pydocstyle' }, + { code: 'D400', severity: baseLinter.LintMessageSeverity.Information, message: 'First line should end with a period (not \'e\')', column: 4, line: 11, type: '', provider: 'pydocstyle' }, + { code: 'D403', severity: baseLinter.LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'And\', not \'and\')', column: 4, line: 15, type: '', provider: 'pydocstyle' }, + { code: 'D400', severity: baseLinter.LintMessageSeverity.Information, message: 'First line should end with a period (not \'t\')', column: 4, line: 15, type: '', provider: 'pydocstyle' }, + { code: 'D403', severity: baseLinter.LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', column: 4, line: 21, type: '', provider: 'pydocstyle' }, + { code: 'D400', severity: baseLinter.LintMessageSeverity.Information, message: 'First line should end with a period (not \'g\')', column: 4, line: 21, type: '', provider: 'pydocstyle' }, + { code: 'D403', severity: baseLinter.LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', column: 4, line: 28, type: '', provider: 'pydocstyle' }, + { code: 'D400', severity: baseLinter.LintMessageSeverity.Information, message: 'First line should end with a period (not \'g\')', column: 4, line: 28, type: '', provider: 'pydocstyle' }, + { code: 'D403', severity: baseLinter.LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', column: 4, line: 38, type: '', provider: 'pydocstyle' }, + { code: 'D400', severity: baseLinter.LintMessageSeverity.Information, message: 'First line should end with a period (not \'g\')', column: 4, line: 38, type: '', provider: 'pydocstyle' }, + { code: 'D403', severity: baseLinter.LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', column: 4, line: 53, type: '', provider: 'pydocstyle' }, + { code: 'D400', severity: baseLinter.LintMessageSeverity.Information, message: 'First line should end with a period (not \'g\')', column: 4, line: 53, type: '', provider: 'pydocstyle' }, + { code: 'D403', severity: baseLinter.LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', column: 4, line: 68, type: '', provider: 'pydocstyle' }, + { code: 'D400', severity: baseLinter.LintMessageSeverity.Information, message: 'First line should end with a period (not \'g\')', column: 4, line: 68, type: '', provider: 'pydocstyle' }, + { code: 'D403', severity: baseLinter.LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', column: 4, line: 80, type: '', provider: 'pydocstyle' }, + { code: 'D400', severity: baseLinter.LintMessageSeverity.Information, message: 'First line should end with a period (not \'g\')', column: 4, line: 80, type: '', provider: 'pydocstyle' } +]; + +const filteredPylintMessagesToBeReturned: baseLinter.ILintMessage[] = [ + { line: 26, column: 14, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blop\' member', provider: '', type: '' }, + { line: 36, column: 14, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: '' }, + { line: 46, column: 18, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: '' }, + { line: 61, column: 18, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: '' }, + { line: 72, column: 18, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: '' }, + { line: 75, column: 18, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: '' }, + { line: 77, column: 14, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: '' }, + { line: 83, column: 14, severity: baseLinter.LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: '' } +]; +const filteredPylint3MessagesToBeReturned: baseLinter.ILintMessage[] = [ +]; +const filteredFlake8MessagesToBeReturned: baseLinter.ILintMessage[] = [ + { line: 87, column: 24, severity: baseLinter.LintMessageSeverity.Warning, code: 'W292', message: 'no newline at end of file', provider: '', type: '' } +]; +const filteredPep88MessagesToBeReturned: baseLinter.ILintMessage[] = [ + { line: 87, column: 24, severity: baseLinter.LintMessageSeverity.Warning, code: 'W292', message: 'no newline at end of file', provider: '', type: '' } +]; +const fiteredPydocstyleMessagseToBeReturned: baseLinter.ILintMessage[] = [ + { code: 'D102', severity: baseLinter.LintMessageSeverity.Information, message: 'Missing docstring in public method', column: 4, line: 8, type: '', provider: 'pydocstyle' } +]; + +// tslint:disable-next-line:max-func-body-length +suite('Linting', () => { + const isPython3Deferred = createDeferred(); + const isPython3 = isPython3Deferred.promise; + suiteSetup(async () => { + pylintFileToLintLines = fs.readFileSync(fileToLint).toString('utf-8').split(/\r?\n/g); + await initialize(); + const version = await execPythonFile(fileToLint, PythonSettings.getInstance(vscode.Uri.file(fileToLint)).pythonPath, ['--version'], __dirname, true); + isPython3Deferred.resolve(version.indexOf('3.') >= 0); + }); + setup(async () => { + await initializeTest(); + await resetSettings(); + }); + suiteTeardown(closeActiveWindows); + teardown(async () => { + await closeActiveWindows(); + await resetSettings(); + }); + async function resetSettings() { + // Don't run these updates in parallel, as they are updating the same file. + await updateSetting('linting.enabled', true, rootWorkspaceUri, vscode.ConfigurationTarget.Workspace); + if (IS_MULTI_ROOT_TEST) { + await updateSetting('linting.enabled', true, rootWorkspaceUri, vscode.ConfigurationTarget.WorkspaceFolder); + } + await updateSetting('linting.lintOnSave', false, rootWorkspaceUri, vscode.ConfigurationTarget.Workspace); + await updateSetting('linting.lintOnTextChange', false, rootWorkspaceUri, vscode.ConfigurationTarget.Workspace); + await updateSetting('linting.pylintEnabled', false, rootWorkspaceUri, vscode.ConfigurationTarget.Workspace); + await updateSetting('linting.flake8Enabled', false, rootWorkspaceUri, vscode.ConfigurationTarget.Workspace); + await updateSetting('linting.pep8Enabled', false, rootWorkspaceUri, vscode.ConfigurationTarget.Workspace); + await updateSetting('linting.prospectorEnabled', false, rootWorkspaceUri, vscode.ConfigurationTarget.Workspace); + await updateSetting('linting.mypyEnabled', false, rootWorkspaceUri, vscode.ConfigurationTarget.Workspace); + await updateSetting('linting.pydocstyleEnabled', false, rootWorkspaceUri, vscode.ConfigurationTarget.Workspace); + await updateSetting('linting.pylamaEnabled', false, rootWorkspaceUri, vscode.ConfigurationTarget.Workspace); + + if (IS_MULTI_ROOT_TEST) { + await updateSetting('linting.lintOnSave', false, rootWorkspaceUri, vscode.ConfigurationTarget.WorkspaceFolder); + await updateSetting('linting.lintOnTextChange', false, rootWorkspaceUri, vscode.ConfigurationTarget.WorkspaceFolder); + await updateSetting('linting.pylintEnabled', false, rootWorkspaceUri, vscode.ConfigurationTarget.WorkspaceFolder); + await updateSetting('linting.flake8Enabled', false, rootWorkspaceUri, vscode.ConfigurationTarget.WorkspaceFolder); + await updateSetting('linting.pep8Enabled', false, rootWorkspaceUri, vscode.ConfigurationTarget.WorkspaceFolder); + await updateSetting('linting.prospectorEnabled', false, rootWorkspaceUri, vscode.ConfigurationTarget.WorkspaceFolder); + await updateSetting('linting.mypyEnabled', false, rootWorkspaceUri, vscode.ConfigurationTarget.WorkspaceFolder); + await updateSetting('linting.pydocstyleEnabled', false, rootWorkspaceUri, vscode.ConfigurationTarget.WorkspaceFolder); + await updateSetting('linting.pylamaEnabled', false, rootWorkspaceUri, vscode.ConfigurationTarget.WorkspaceFolder); + } + } + async function testEnablingDisablingOfLinter(linter: baseLinter.BaseLinter, setting: PythonSettingKeys, enabled: boolean, output: MockOutputChannel) { + await updateSetting(setting, enabled, rootWorkspaceUri, IS_MULTI_ROOT_TEST ? vscode.ConfigurationTarget.WorkspaceFolder : vscode.ConfigurationTarget.Workspace); + const document = await vscode.workspace.openTextDocument(fileToLint); + const editor = await vscode.window.showTextDocument(document); + const cancelToken = new vscode.CancellationTokenSource(); + const messages = await linter.lint(editor.document, cancelToken.token); + if (enabled) { + assert.notEqual(messages.length, 0, `No linter errors when linter is enabled, Output - ${output.output}`); + } + else { + assert.equal(messages.length, 0, `Errors returned when linter is disabled, Output - ${output.output}`); + } + } + test('Disable Pylint and test linter', async () => { + const ch = new MockOutputChannel('Lint'); + await testEnablingDisablingOfLinter(new pyLint.Linter(ch), 'linting.pylintEnabled', false, ch); + }); + test('Enable Pylint and test linter', async () => { + const ch = new MockOutputChannel('Lint'); + await testEnablingDisablingOfLinter(new pyLint.Linter(ch), 'linting.pylintEnabled', true, ch); + }); + test('Disable Pep8 and test linter', async () => { + const ch = new MockOutputChannel('Lint'); + await testEnablingDisablingOfLinter(new pep8.Linter(ch), 'linting.pep8Enabled', false, ch); + }); + test('Enable Pep8 and test linter', async () => { + const ch = new MockOutputChannel('Lint'); + await testEnablingDisablingOfLinter(new pep8.Linter(ch), 'linting.pep8Enabled', true, ch); + }); + test('Disable Flake8 and test linter', async () => { + const ch = new MockOutputChannel('Lint'); + await testEnablingDisablingOfLinter(new flake8.Linter(ch), 'linting.flake8Enabled', false, ch); + }); + test('Enable Flake8 and test linter', async () => { + const ch = new MockOutputChannel('Lint'); + await testEnablingDisablingOfLinter(new flake8.Linter(ch), 'linting.flake8Enabled', true, ch); + }); + test('Disable Prospector and test linter', async () => { + const ch = new MockOutputChannel('Lint'); + await testEnablingDisablingOfLinter(new prospector.Linter(ch), 'linting.prospectorEnabled', false, ch); + }); + test('Disable Pydocstyle and test linter', async () => { + const ch = new MockOutputChannel('Lint'); + await testEnablingDisablingOfLinter(new pydocstyle.Linter(ch), 'linting.pydocstyleEnabled', false, ch); + }); + test('Enable Pydocstyle and test linter', async () => { + const ch = new MockOutputChannel('Lint'); + await testEnablingDisablingOfLinter(new pydocstyle.Linter(ch), 'linting.pydocstyleEnabled', true, ch); + }); + + // tslint:disable-next-line:no-any + async function testLinterMessages(linter: baseLinter.BaseLinter, outputChannel: MockOutputChannel, pythonFile: string, messagesToBeReceived: baseLinter.ILintMessage[]): Promise { + const cancelToken = new vscode.CancellationTokenSource(); + const settingToEnable = SettingToDisableProduct.get(linter.product); + // tslint:disable-next-line:no-any prefer-type-cast + await updateSetting(settingToEnable as any, true, rootWorkspaceUri, IS_MULTI_ROOT_TEST ? vscode.ConfigurationTarget.WorkspaceFolder : vscode.ConfigurationTarget.Workspace); + const document = await vscode.workspace.openTextDocument(pythonFile); + const editor = await vscode.window.showTextDocument(document); + const messages = await linter.lint(editor.document, cancelToken.token); + if (messagesToBeReceived.length === 0) { + assert.equal(messages.length, 0, `No errors in linter, Output - ${outputChannel.output}`); + } + else { + if (outputChannel.output.indexOf('ENOENT') === -1) { + // Pylint for Python Version 2.7 could return 80 linter messages, where as in 3.5 it might only return 1. + // Looks like pylint stops linting as soon as it comes across any ERRORS. + assert.notEqual(messages.length, 0, `No errors in linter, Output - ${outputChannel.output}`); + } + } + } + test('PyLint', async () => { + const ch = new MockOutputChannel('Lint'); + const linter = new pyLint.Linter(ch); + await testLinterMessages(linter, ch, fileToLint, pylintMessagesToBeReturned); + }); + test('Flake8', async () => { + const ch = new MockOutputChannel('Lint'); + const linter = new flake8.Linter(ch); + await testLinterMessages(linter, ch, fileToLint, flake8MessagesToBeReturned); + }); + test('Pep8', async () => { + const ch = new MockOutputChannel('Lint'); + const linter = new pep8.Linter(ch); + await testLinterMessages(linter, ch, fileToLint, pep8MessagesToBeReturned); + }); + test('Pydocstyle', async () => { + const ch = new MockOutputChannel('Lint'); + const linter = new pydocstyle.Linter(ch); + await testLinterMessages(linter, ch, fileToLint, pydocstyleMessagseToBeReturned); + }); + // tslint:disable-next-line:no-floating-promises + isPython3.then(value => { + const messagesToBeReturned = value ? filteredPylint3MessagesToBeReturned : filteredPylintMessagesToBeReturned; + test('PyLint with config in root', async () => { + const ch = new MockOutputChannel('Lint'); + const linter = new pyLint.Linter(ch); + await testLinterMessages(linter, ch, path.join(pylintConfigPath, 'file.py'), messagesToBeReturned); + }); + }); + test('Flake8 with config in root', async () => { + const ch = new MockOutputChannel('Lint'); + const linter = new flake8.Linter(ch); + await testLinterMessages(linter, ch, path.join(flake8ConfigPath, 'file.py'), filteredFlake8MessagesToBeReturned); + }); + test('Pep8 with config in root', async () => { + const ch = new MockOutputChannel('Lint'); + const linter = new pep8.Linter(ch); + await testLinterMessages(linter, ch, path.join(pep8ConfigPath, 'file.py'), filteredPep88MessagesToBeReturned); + }); + // tslint:disable-next-line:no-floating-promises + isPython3.then(value => { + const messagesToBeReturned = value ? [] : fiteredPydocstyleMessagseToBeReturned; + test('Pydocstyle with config in root', async () => { + const ch = new MockOutputChannel('Lint'); + const linter = new pydocstyle.Linter(ch); + await testLinterMessages(linter, ch, path.join(pydocstyleConfigPath27, 'file.py'), messagesToBeReturned); + }); + }); +}); diff --git a/src/test/mockClasses.ts b/src/test/mockClasses.ts index 6f0addbd2f69..d2fc17c44043 100644 --- a/src/test/mockClasses.ts +++ b/src/test/mockClasses.ts @@ -23,3 +23,21 @@ export class MockOutputChannel implements vscode.OutputChannel { } dispose() { } } + +export class MockStatusBarItem implements vscode.StatusBarItem { + public alignment: vscode.StatusBarAlignment; + public priority: number; + public text: string; + public tooltip: string; + public color: string; + public command: string; + show(): void { + + } + hide(): void { + + } + dispose(): void { + + } +} \ No newline at end of file diff --git a/src/test/multiRootTest.ts b/src/test/multiRootTest.ts new file mode 100644 index 000000000000..57bf5368ccb7 --- /dev/null +++ b/src/test/multiRootTest.ts @@ -0,0 +1,9 @@ +import * as path from 'path'; + +process.env.CODE_TESTS_WORKSPACE = path.join(__dirname, '..', '..', 'src', 'testMultiRootWkspc', 'multi.code-workspace'); + +function start() { + console.log('start Multiroot tests'); + require('../../node_modules/vscode/bin/test'); +} +start(); diff --git a/src/test/multiRootWkspc/disableLinters/.vscode/tags b/src/test/multiRootWkspc/disableLinters/.vscode/tags new file mode 100644 index 000000000000..4739b4629cfb --- /dev/null +++ b/src/test/multiRootWkspc/disableLinters/.vscode/tags @@ -0,0 +1,19 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +Foo ..\\file.py /^class Foo(object):$/;" kind:class line:5 +__init__ ..\\file.py /^ def __init__(self):$/;" kind:member line:8 +__revision__ ..\\file.py /^__revision__ = None$/;" kind:variable line:3 +file.py ..\\file.py 1;" kind:file line:1 +meth1 ..\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth2 ..\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 ..\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 ..\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 ..\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 ..\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 ..\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 ..\\file.py /^ def meth8(self):$/;" kind:member line:80 diff --git a/src/test/multiRootWkspc/disableLinters/file.py b/src/test/multiRootWkspc/disableLinters/file.py new file mode 100644 index 000000000000..27509dd2fcd6 --- /dev/null +++ b/src/test/multiRootWkspc/disableLinters/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print(self) + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print (self\ + + "foo") + + def meth3(self): + """test one line disabling""" + # no error + print (self.bla) # pylint: disable=no-member + # error + print (self.blop) + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) + # pylint: enable=no-member + # error + print (self.blip) + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + if self.blop: + # pylint: enable=no-member + # error + print (self.blip) + else: + # no error + print (self.blip) + # no error + print (self.blip) + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + try: + # pylint: enable=no-member + # error + print (self.blip) + except UndefinedName: # pylint: disable=undefined-variable + # no error + print (self.blip) + # no error + print (self.blip) + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print (self.blip) + else: + # error + print (self.blip) + # error + print (self.blip) + + + def meth8(self): + """test late disabling""" + # error + print (self.blip) + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) diff --git a/src/test/multiRootWkspc/multi.code-workspace b/src/test/multiRootWkspc/multi.code-workspace new file mode 100644 index 000000000000..3c2ef8e8ca44 --- /dev/null +++ b/src/test/multiRootWkspc/multi.code-workspace @@ -0,0 +1,29 @@ +{ + "folders": [ + { + "path": "workspace1" + }, + { + "path": "workspace2" + }, + { + "path": "workspace3" + }, + { + "path": "parent\\child" + }, + { + "path": "disableLinters" + } + ], + "settings": { + "python.linting.flake8Enabled": false, + "python.linting.mypyEnabled": true, + "python.linting.pydocstyleEnabled": true, + "python.linting.pylamaEnabled": true, + "python.linting.pylintEnabled": false, + "python.linting.pep8Enabled": true, + "python.linting.prospectorEnabled": true, + "python.workspaceSymbols.enabled": true + } +} diff --git a/src/test/multiRootWkspc/parent/child/.vscode/settings.json b/src/test/multiRootWkspc/parent/child/.vscode/settings.json new file mode 100644 index 000000000000..b78380782cd9 --- /dev/null +++ b/src/test/multiRootWkspc/parent/child/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.workspaceSymbols.enabled": true +} \ No newline at end of file diff --git a/src/test/multiRootWkspc/parent/child/.vscode/tags b/src/test/multiRootWkspc/parent/child/.vscode/tags new file mode 100644 index 000000000000..e6791c755b0f --- /dev/null +++ b/src/test/multiRootWkspc/parent/child/.vscode/tags @@ -0,0 +1,24 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +Child2Class ..\\childFile.py /^class Child2Class(object):$/;" kind:class line:5 +Foo ..\\file.py /^class Foo(object):$/;" kind:class line:5 +__init__ ..\\childFile.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\file.py /^ def __init__(self):$/;" kind:member line:8 +__revision__ ..\\childFile.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\file.py /^__revision__ = None$/;" kind:variable line:3 +childFile.py ..\\childFile.py 1;" kind:file line:1 +file.py ..\\file.py 1;" kind:file line:1 +meth1 ..\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1OfChild ..\\childFile.py /^ def meth1OfChild(self, arg):$/;" kind:member line:11 +meth2 ..\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 ..\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 ..\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 ..\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 ..\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 ..\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 ..\\file.py /^ def meth8(self):$/;" kind:member line:80 diff --git a/src/test/multiRootWkspc/parent/child/childFile.py b/src/test/multiRootWkspc/parent/child/childFile.py new file mode 100644 index 000000000000..31d6fc7b4a18 --- /dev/null +++ b/src/test/multiRootWkspc/parent/child/childFile.py @@ -0,0 +1,13 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Child2Class(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1OfChild(self, arg): + """this issues a message""" + print (self) diff --git a/src/test/multiRootWkspc/parent/child/file.py b/src/test/multiRootWkspc/parent/child/file.py new file mode 100644 index 000000000000..27509dd2fcd6 --- /dev/null +++ b/src/test/multiRootWkspc/parent/child/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print(self) + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print (self\ + + "foo") + + def meth3(self): + """test one line disabling""" + # no error + print (self.bla) # pylint: disable=no-member + # error + print (self.blop) + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) + # pylint: enable=no-member + # error + print (self.blip) + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + if self.blop: + # pylint: enable=no-member + # error + print (self.blip) + else: + # no error + print (self.blip) + # no error + print (self.blip) + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + try: + # pylint: enable=no-member + # error + print (self.blip) + except UndefinedName: # pylint: disable=undefined-variable + # no error + print (self.blip) + # no error + print (self.blip) + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print (self.blip) + else: + # error + print (self.blip) + # error + print (self.blip) + + + def meth8(self): + """test late disabling""" + # error + print (self.blip) + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) diff --git a/src/test/multiRootWkspc/workspace1/.vscode/settings.json b/src/test/multiRootWkspc/workspace1/.vscode/settings.json new file mode 100644 index 000000000000..a783cfe01962 --- /dev/null +++ b/src/test/multiRootWkspc/workspace1/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "python.linting.pylintEnabled": true, + "python.linting.enabled": false, + "python.linting.flake8Enabled": true +} diff --git a/src/test/multiRootWkspc/workspace1/.vscode/tags b/src/test/multiRootWkspc/workspace1/.vscode/tags new file mode 100644 index 000000000000..4739b4629cfb --- /dev/null +++ b/src/test/multiRootWkspc/workspace1/.vscode/tags @@ -0,0 +1,19 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +Foo ..\\file.py /^class Foo(object):$/;" kind:class line:5 +__init__ ..\\file.py /^ def __init__(self):$/;" kind:member line:8 +__revision__ ..\\file.py /^__revision__ = None$/;" kind:variable line:3 +file.py ..\\file.py 1;" kind:file line:1 +meth1 ..\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth2 ..\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 ..\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 ..\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 ..\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 ..\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 ..\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 ..\\file.py /^ def meth8(self):$/;" kind:member line:80 diff --git a/src/test/multiRootWkspc/workspace1/file.py b/src/test/multiRootWkspc/workspace1/file.py new file mode 100644 index 000000000000..27509dd2fcd6 --- /dev/null +++ b/src/test/multiRootWkspc/workspace1/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print(self) + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print (self\ + + "foo") + + def meth3(self): + """test one line disabling""" + # no error + print (self.bla) # pylint: disable=no-member + # error + print (self.blop) + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) + # pylint: enable=no-member + # error + print (self.blip) + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + if self.blop: + # pylint: enable=no-member + # error + print (self.blip) + else: + # no error + print (self.blip) + # no error + print (self.blip) + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + try: + # pylint: enable=no-member + # error + print (self.blip) + except UndefinedName: # pylint: disable=undefined-variable + # no error + print (self.blip) + # no error + print (self.blip) + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print (self.blip) + else: + # error + print (self.blip) + # error + print (self.blip) + + + def meth8(self): + """test late disabling""" + # error + print (self.blip) + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) diff --git a/src/test/multiRootWkspc/workspace2/.vscode/settings.json b/src/test/multiRootWkspc/workspace2/.vscode/settings.json new file mode 100644 index 000000000000..385728982cfa --- /dev/null +++ b/src/test/multiRootWkspc/workspace2/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python.workspaceSymbols.tagFilePath": "${workspaceRoot}/workspace2.tags.file", + "python.workspaceSymbols.enabled": true +} diff --git a/src/test/multiRootWkspc/workspace2/file.py b/src/test/multiRootWkspc/workspace2/file.py new file mode 100644 index 000000000000..27509dd2fcd6 --- /dev/null +++ b/src/test/multiRootWkspc/workspace2/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print(self) + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print (self\ + + "foo") + + def meth3(self): + """test one line disabling""" + # no error + print (self.bla) # pylint: disable=no-member + # error + print (self.blop) + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) + # pylint: enable=no-member + # error + print (self.blip) + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + if self.blop: + # pylint: enable=no-member + # error + print (self.blip) + else: + # no error + print (self.blip) + # no error + print (self.blip) + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + try: + # pylint: enable=no-member + # error + print (self.blip) + except UndefinedName: # pylint: disable=undefined-variable + # no error + print (self.blip) + # no error + print (self.blip) + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print (self.blip) + else: + # error + print (self.blip) + # error + print (self.blip) + + + def meth8(self): + """test late disabling""" + # error + print (self.blip) + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) diff --git a/src/test/multiRootWkspc/workspace2/workspace2.tags.file b/src/test/multiRootWkspc/workspace2/workspace2.tags.file new file mode 100644 index 000000000000..2d54e7ed7c7b --- /dev/null +++ b/src/test/multiRootWkspc/workspace2/workspace2.tags.file @@ -0,0 +1,24 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +Foo C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^class Foo(object):$/;" kind:class line:5 +Workspace2Class C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\workspace2File.py /^class Workspace2Class(object):$/;" kind:class line:5 +__init__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\workspace2File.py /^ def __init__(self):$/;" kind:member line:8 +__revision__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\workspace2File.py /^__revision__ = None$/;" kind:variable line:3 +file.py C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py 1;" kind:file line:1 +meth1 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1OfWorkspace2 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\workspace2File.py /^ def meth1OfWorkspace2(self, arg):$/;" kind:member line:11 +meth2 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^ def meth8(self):$/;" kind:member line:80 +workspace2File.py C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\workspace2File.py 1;" kind:file line:1 diff --git a/src/test/multiRootWkspc/workspace2/workspace2File.py b/src/test/multiRootWkspc/workspace2/workspace2File.py new file mode 100644 index 000000000000..61aa87c55fed --- /dev/null +++ b/src/test/multiRootWkspc/workspace2/workspace2File.py @@ -0,0 +1,13 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Workspace2Class(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1OfWorkspace2(self, arg): + """this issues a message""" + print (self) diff --git a/src/test/multiRootWkspc/workspace3/.vscode/settings.json b/src/test/multiRootWkspc/workspace3/.vscode/settings.json new file mode 100644 index 000000000000..8779a0c08efe --- /dev/null +++ b/src/test/multiRootWkspc/workspace3/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.workspaceSymbols.tagFilePath": "${workspaceRoot}/workspace3.tags.file" +} diff --git a/src/test/multiRootWkspc/workspace3/file.py b/src/test/multiRootWkspc/workspace3/file.py new file mode 100644 index 000000000000..27509dd2fcd6 --- /dev/null +++ b/src/test/multiRootWkspc/workspace3/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print(self) + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print (self\ + + "foo") + + def meth3(self): + """test one line disabling""" + # no error + print (self.bla) # pylint: disable=no-member + # error + print (self.blop) + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) + # pylint: enable=no-member + # error + print (self.blip) + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + if self.blop: + # pylint: enable=no-member + # error + print (self.blip) + else: + # no error + print (self.blip) + # no error + print (self.blip) + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + try: + # pylint: enable=no-member + # error + print (self.blip) + except UndefinedName: # pylint: disable=undefined-variable + # no error + print (self.blip) + # no error + print (self.blip) + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print (self.blip) + else: + # error + print (self.blip) + # error + print (self.blip) + + + def meth8(self): + """test late disabling""" + # error + print (self.blip) + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) diff --git a/src/test/multiRootWkspc/workspace3/workspace3.tags.file b/src/test/multiRootWkspc/workspace3/workspace3.tags.file new file mode 100644 index 000000000000..9a141392d6ae --- /dev/null +++ b/src/test/multiRootWkspc/workspace3/workspace3.tags.file @@ -0,0 +1,19 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +Foo C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^class Foo(object):$/;" kind:class line:5 +__init__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^ def __init__(self):$/;" kind:member line:8 +__revision__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^__revision__ = None$/;" kind:variable line:3 +file.py C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py 1;" kind:file line:1 +meth1 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth2 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^ def meth8(self):$/;" kind:member line:80 diff --git a/src/test/pipRequirements/example-requirements.txt b/src/test/pipRequirements/example-requirements.txt new file mode 100644 index 000000000000..bc4af132bdb5 --- /dev/null +++ b/src/test/pipRequirements/example-requirements.txt @@ -0,0 +1,37 @@ +# https://www.python.org/dev/peps/pep-0508/#names +This-name_has.everything42 # End-of-line comment. + +# https://www.python.org/dev/peps/pep-0508/#extras +project[extras] + +# https://www.python.org/dev/peps/pep-0508/#versions +# https://www.python.org/dev/peps/pep-0440/ +project > 2.0.0 +project~=1.0.0 +project>1.0.0,<2.0.0 +project==1.0.0.dev1 + +# https://www.python.org/dev/peps/pep-0508/#environment-markers +project;python_version>'3.0' +project~=2.0.0;os_name=="linux" + +# https://pip.readthedocs.io/en/stable/reference/pip_install/#requirements-file-format +# Continuation line. +project \ + >1.0.0 + +# Options +## Stand-alone w/o argument. +--no-links +## Stand-alone w/ argument. +-c constraints.txt +-e git://git.myproject.org/MyProject#egg=MyProject +## Part of requirement. +FooProject >= 1.2 --global-option="--no-user-cfg" + +# File path. +./some/file +some/file + +# URL. +https://some-site.ca/project.whl diff --git a/src/test/providers/shebangCodeLenseProvider.test.ts b/src/test/providers/shebangCodeLenseProvider.test.ts new file mode 100644 index 000000000000..158f6e6db987 --- /dev/null +++ b/src/test/providers/shebangCodeLenseProvider.test.ts @@ -0,0 +1,99 @@ +import * as assert from 'assert'; +import * as child_process from 'child_process'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { IS_WINDOWS, PythonSettings } from '../../client/common/configSettings'; +import { ShebangCodeLensProvider } from '../../client/interpreter/display/shebangCodeLensProvider'; +import { getFirstNonEmptyLineFromMultilineString } from '../../client/interpreter/helpers'; +import { closeActiveWindows, initialize, initializeTest } from '../initialize'; + +const autoCompPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'shebang'); +const fileShebang = path.join(autoCompPath, 'shebang.py'); +const fileShebangEnv = path.join(autoCompPath, 'shebangEnv.py'); +const fileShebangInvalid = path.join(autoCompPath, 'shebangInvalid.py'); +const filePlain = path.join(autoCompPath, 'plain.py'); + +suite('Shebang detection', () => { + suiteSetup(initialize); + suiteTeardown(async () => { + await initialize(); + await closeActiveWindows(); + }); + setup(initializeTest); + + test('A code lens will appear when sheban python and python in settings are different', async () => { + const pythonPath = 'someUnknownInterpreter'; + const editor = await openFile(fileShebang); + PythonSettings.getInstance(editor.document.uri).pythonPath = pythonPath; + const codeLenses = await setupCodeLens(editor); + + assert.equal(codeLenses.length, 1, 'No CodeLens available'); + const codeLens = codeLenses[0]; + assert(codeLens.range.isSingleLine, 'Invalid CodeLens Range'); + assert.equal(codeLens.command.command, 'python.setShebangInterpreter'); + }); + + test('Code lens will not appear when sheban python and python in settings are the same', async () => { + PythonSettings.dispose(); + const pythonPath = await getFullyQualifiedPathToInterpreter('python'); + const editor = await openFile(fileShebang); + PythonSettings.getInstance(editor.document.uri).pythonPath = pythonPath; + const codeLenses = await setupCodeLens(editor); + assert.equal(codeLenses.length, 0, 'CodeLens available although interpreters are equal'); + + }); + + test('Code lens will not appear when sheban python is invalid', async () => { + const editor = await openFile(fileShebangInvalid); + const codeLenses = await setupCodeLens(editor); + assert.equal(codeLenses.length, 0, 'CodeLens available even when shebang is invalid'); + }); + + if (!IS_WINDOWS) { + test('A code lens will appear when shebang python uses env and python settings are different', async () => { + const editor = await openFile(fileShebangEnv); + PythonSettings.getInstance(editor.document.uri).pythonPath = 'p1'; + const codeLenses = await setupCodeLens(editor); + + assert.equal(codeLenses.length, 1, 'No CodeLens available'); + const codeLens = codeLenses[0]; + assert(codeLens.range.isSingleLine, 'Invalid CodeLens Range'); + assert.equal(codeLens.command.command, 'python.setShebangInterpreter'); + + }); + + test('Code lens will not appear even when shebang python uses env and python settings are the same', async () => { + const pythonPath = await getFullyQualifiedPathToInterpreter('python'); + const editor = await openFile(fileShebangEnv); + PythonSettings.getInstance(editor.document.uri).pythonPath = pythonPath; + const codeLenses = await setupCodeLens(editor); + assert.equal(codeLenses.length, 0, 'CodeLens available although interpreters are equal'); + }); + } + + test('Code lens will not appear as there is no shebang', async () => { + const editor = await openFile(filePlain); + const codeLenses = await setupCodeLens(editor); + assert.equal(codeLenses.length, 0, 'CodeLens available although no shebang'); + }); + + async function openFile(fileName: string) { + const document = await vscode.workspace.openTextDocument(fileName); + const editor = await vscode.window.showTextDocument(document); + assert(vscode.window.activeTextEditor, 'No active editor'); + return editor; + } + async function getFullyQualifiedPathToInterpreter(pythonPath: string) { + return new Promise(resolve => { + child_process.execFile(pythonPath, ['-c', 'import sys;print(sys.executable)'], (_, stdout) => { + resolve(getFirstNonEmptyLineFromMultilineString(stdout)); + }); + }).catch(() => undefined); + } + + async function setupCodeLens(editor: vscode.TextEditor) { + const document = editor.document; + const codeLensProvider = new ShebangCodeLensProvider(); + return await codeLensProvider.provideCodeLenses(document, null); + } +}); diff --git a/src/test/pythonFiles/autocomp/deco.py b/src/test/pythonFiles/autocomp/deco.py new file mode 100644 index 000000000000..b843741ef647 --- /dev/null +++ b/src/test/pythonFiles/autocomp/deco.py @@ -0,0 +1,6 @@ + +import abc +class Decorator(metaclass=abc.ABCMeta): + @abc.-# no abstract class + @abc.abstractclassmethod + \ No newline at end of file diff --git a/src/test/pythonFiles/autocomp/doc.py b/src/test/pythonFiles/autocomp/doc.py new file mode 100644 index 000000000000..a0d62874538f --- /dev/null +++ b/src/test/pythonFiles/autocomp/doc.py @@ -0,0 +1,11 @@ +import os + +if os.path.exists(("/etc/hosts")): + with open("/etc/hosts", "a") as f: + for line in f.readlines(): + content = line.upper() + + + +import time +time.slee \ No newline at end of file diff --git a/src/test/pythonFiles/autocomp/imp.py b/src/test/pythonFiles/autocomp/imp.py new file mode 100644 index 000000000000..0d0c98ed1cde --- /dev/null +++ b/src/test/pythonFiles/autocomp/imp.py @@ -0,0 +1,2 @@ +from os import * +fsta \ No newline at end of file diff --git a/src/test/pythonFiles/autocomp/lamb.py b/src/test/pythonFiles/autocomp/lamb.py new file mode 100644 index 000000000000..05b92f5cd581 --- /dev/null +++ b/src/test/pythonFiles/autocomp/lamb.py @@ -0,0 +1,2 @@ +instant_print = lambda x: [print(x), sys.stdout.flush(), sys.stderr.flush()] +instant_print("X"). \ No newline at end of file diff --git a/src/test/pythonFiles/autocomp/misc.py b/src/test/pythonFiles/autocomp/misc.py index 07f87dbe2137..3d4a54cbc145 100644 --- a/src/test/pythonFiles/autocomp/misc.py +++ b/src/test/pythonFiles/autocomp/misc.py @@ -1336,7 +1336,7 @@ class Random(_random.Random): Class Random can also be subclassed if you want to use a different basic generator of your own devising: in that case, override the following - methods: random(), seed(), getstate(), and setstate(). + methods: random(), seed(), getstate(), and setstate(). Optionally, implement a getrandbits() method so that randrange() can cover arbitrarily large ranges. diff --git a/src/test/pythonFiles/autocomp/pep484.py b/src/test/pythonFiles/autocomp/pep484.py new file mode 100644 index 000000000000..79edec69ae1a --- /dev/null +++ b/src/test/pythonFiles/autocomp/pep484.py @@ -0,0 +1,12 @@ + +def greeting(name: str) -> str: + return 'Hello ' + name.upper() + + +def add(num1, num2) -> int: + return num1 + num2 + +add().bit_length() + + + diff --git a/src/test/pythonFiles/autocomp/pep526.py b/src/test/pythonFiles/autocomp/pep526.py new file mode 100644 index 000000000000..d8cd0300ed0d --- /dev/null +++ b/src/test/pythonFiles/autocomp/pep526.py @@ -0,0 +1,22 @@ + + +PEP_526_style: str = "hello world" +captain: str # Note: no initial value! +PEP_484_style = SOMETHING # type: str + + +PEP_484_style.upper() +PEP_526_style.upper() +captain.upper() + +# https://github.com/DonJayamanne/pythonVSCode/issues/918 +class A: + a = 0 + + +class B: + b: int = 0 + + +A().a # -> Autocomplete works +B().b.bit_length() # -> Autocomplete doesn't work \ No newline at end of file diff --git a/src/test/pythonFiles/autoimport/one.py b/src/test/pythonFiles/autoimport/one.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/autoimport/two/__init__.py b/src/test/pythonFiles/autoimport/two/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/autoimport/two/three.py b/src/test/pythonFiles/autoimport/two/three.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/definition/await.test.py b/src/test/pythonFiles/definition/await.test.py new file mode 100644 index 000000000000..7b4acd876c27 --- /dev/null +++ b/src/test/pythonFiles/definition/await.test.py @@ -0,0 +1,19 @@ +# https://github.com/DonJayamanne/pythonVSCode/issues/962 + +class A: + def __init__(self): + self.test_value = 0 + + async def test(self): + pass + + async def test2(self): + await self.test() + +async def testthis(): + """ + Wow + """ + pass + +await testthis() \ No newline at end of file diff --git a/src/test/pythonFiles/definition/decorators.py b/src/test/pythonFiles/definition/decorators.py new file mode 100644 index 000000000000..6d772479b55d --- /dev/null +++ b/src/test/pythonFiles/definition/decorators.py @@ -0,0 +1,28 @@ +def identity(ob): + return ob + +@identity +def myfunc(): + print "my function" + +myfunc() + +# https://github.com/DonJayamanne/pythonVSCode/issues/1046 +from fabric.api import sudo +# currently go to definition of sudo will go to some decorator function +# works, if fabric package is not installed +sudo() + +from numba import jit + +# https://github.com/DonJayamanne/pythonVSCode/issues/478 +@jit() +def calculate_cash_flows(remaining_loan_term, remaining_io_term, + settle_balance, settle_date, payment_day, + ir_fixed, ir_accrual_day_count_basis, + amortizing_debt_service): + print("") + +# currently go to definition of sudo will go to some decorator function +# works, if fabric package is not installed +calculate_cash_flows(1,2,3,4,5,6,7,8) diff --git a/src/test/pythonFiles/definition/five.py b/src/test/pythonFiles/definition/five.py new file mode 100644 index 000000000000..507c5fed967c --- /dev/null +++ b/src/test/pythonFiles/definition/five.py @@ -0,0 +1,2 @@ +import four +four.showMessage() diff --git a/src/test/pythonFiles/definition/four.py b/src/test/pythonFiles/definition/four.py new file mode 100644 index 000000000000..f67f78af0856 --- /dev/null +++ b/src/test/pythonFiles/definition/four.py @@ -0,0 +1,27 @@ +# -*- coding๏ผšutf-8 -*- +# pylint: disable=E0401, W0512 + +import os + + +class Foo(object): + '''่ฏดๆ˜Ž''' + + @staticmethod + def bar(): + """ + ่ฏดๆ˜Ž - keep this line, it works + delete following line, it works + ๅฆ‚ๆžœๅญ˜ๅœจ้œ€่ฆ็ญ‰ๅพ…ๅฎกๆ‰นๆˆ–ๆญฃๅœจๆ‰ง่กŒ็š„ไปปๅŠก๏ผŒๅฐ†ไธๅˆทๆ–ฐ้กต้ข + """ + return os.path.exists('c:/') + +def showMessage(): + """ + ะšัŽะผ ัƒั‚ ะถัะผะฟัั€ ะฟะพัˆะถะธะผ ะปัŒะฐะฑะพั€ัะถ, ะบะพะผะผัŽะฝั‹ ัะฝั‚ัั€ััั‰ัั‚ ะฝะฐะผ ะตะด, ะดะตะบั‚ะฐ ะธะณะฝะพั‚ะฐ ะฝั‹ะผะพั€ั ะถัั‚ ัะธ. + ะจัะฐ ะดะตะบะฐะผ ัะบัˆั‹ั€ะบะธ ัะธ, ัะธ ะทั‹ะด ัั€ั€ัะผ ะดะพะบัะฝะดั‘, ะฒะตะบะถ ั„ะฐะบัั‚ั ะฟัั€ั‡ั‹ะบะฒัŽัั€ั‘ะถ ะบัƒ. + """ + print('1234') + +Foo.bar() +showMessage() \ No newline at end of file diff --git a/src/test/pythonFiles/definition/one.py b/src/test/pythonFiles/definition/one.py new file mode 100644 index 000000000000..f1e3d75ffcbc --- /dev/null +++ b/src/test/pythonFiles/definition/one.py @@ -0,0 +1,46 @@ + +import sys + +print(sys.api_version) + +class Class1(object): + """Some class + And the second line + """ + + description = "Run isort on modules registered in setuptools" + user_options = [] + + def __init__(self, file_path=None, file_contents=None): + self.prop1 = '' + self.prop2 = 1 + + def method1(self): + """ + This is method1 + """ + pass + + def method2(self): + """ + This is method2 + """ + pass + +obj = Class1() +obj.method1() + +def function1(): + print("SOMETHING") + + +def function2(): + print("SOMETHING") + +def function3(): + print("SOMETHING") + +def function4(): + print("SOMETHING") + +function1() \ No newline at end of file diff --git a/src/test/pythonFiles/definition/three.py b/src/test/pythonFiles/definition/three.py new file mode 100644 index 000000000000..35ad7f399172 --- /dev/null +++ b/src/test/pythonFiles/definition/three.py @@ -0,0 +1,2 @@ +import two +two.ct().fun() \ No newline at end of file diff --git a/src/test/pythonFiles/definition/two.py b/src/test/pythonFiles/definition/two.py new file mode 100644 index 000000000000..99a6e3c4bdf1 --- /dev/null +++ b/src/test/pythonFiles/definition/two.py @@ -0,0 +1,6 @@ +class ct: + def fun(): + """ + This is fun + """ + pass \ No newline at end of file diff --git a/src/test/pythonFiles/environments/conda/Scripts/conda.exe b/src/test/pythonFiles/environments/conda/Scripts/conda.exe new file mode 100644 index 000000000000..aaa0af66f262 --- /dev/null +++ b/src/test/pythonFiles/environments/conda/Scripts/conda.exe @@ -0,0 +1 @@ +// Test file \ No newline at end of file diff --git a/src/test/pythonFiles/environments/conda/bin/python b/src/test/pythonFiles/environments/conda/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/environments/conda/envs/numpy/bin/python b/src/test/pythonFiles/environments/conda/envs/numpy/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/environments/conda/envs/numpy/python.exe b/src/test/pythonFiles/environments/conda/envs/numpy/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/environments/conda/envs/scipy/bin/python b/src/test/pythonFiles/environments/conda/envs/scipy/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/environments/conda/envs/scipy/python.exe b/src/test/pythonFiles/environments/conda/envs/scipy/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/environments/environments.txt b/src/test/pythonFiles/environments/environments.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/environments/path1/one b/src/test/pythonFiles/environments/path1/one new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/environments/path1/one.exe b/src/test/pythonFiles/environments/path1/one.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/environments/path1/python.exe b/src/test/pythonFiles/environments/path1/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/environments/path2/one b/src/test/pythonFiles/environments/path2/one new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/environments/path2/one.exe b/src/test/pythonFiles/environments/path2/one.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/environments/path2/python.exe b/src/test/pythonFiles/environments/path2/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/hover/stringFormat.py b/src/test/pythonFiles/hover/stringFormat.py new file mode 100644 index 000000000000..b54311aa83c1 --- /dev/null +++ b/src/test/pythonFiles/hover/stringFormat.py @@ -0,0 +1,7 @@ + +def print_hello(name): + """say hello to name on stdout. + :param name: the name. + """ + print('hello {0}'.format(name).capitalize()) + diff --git a/src/test/pythonFiles/linting/file.py b/src/test/pythonFiles/linting/file.py index 047ba0dc679e..7b625a769243 100644 --- a/src/test/pythonFiles/linting/file.py +++ b/src/test/pythonFiles/linting/file.py @@ -10,78 +10,78 @@ def __init__(self): def meth1(self, arg): """this issues a message""" - print self + print (self) def meth2(self, arg): """and this one not""" # pylint: disable=unused-argument - print self\ - + "foo" + print (self\ + + "foo") def meth3(self): """test one line disabling""" # no error - print self.bla # pylint: disable=no-member + print (self.bla) # pylint: disable=no-member # error - print self.blop + print (self.blop) def meth4(self): """test re-enabling""" # pylint: disable=no-member # no error - print self.bla - print self.blop + print (self.bla) + print (self.blop) # pylint: enable=no-member # error - print self.blip + print (self.blip) def meth5(self): """test IF sub-block re-enabling""" # pylint: disable=no-member # no error - print self.bla + print (self.bla) if self.blop: # pylint: enable=no-member # error - print self.blip + print (self.blip) else: # no error - print self.blip + print (self.blip) # no error - print self.blip + print (self.blip) def meth6(self): """test TRY/EXCEPT sub-block re-enabling""" # pylint: disable=no-member # no error - print self.bla + print (self.bla) try: # pylint: enable=no-member # error - print self.blip + print (self.blip) except UndefinedName: # pylint: disable=undefined-variable # no error - print self.blip + print (self.blip) # no error - print self.blip + print (self.blip) def meth7(self): """test one line block opening disabling""" if self.blop: # pylint: disable=no-member # error - print self.blip + print (self.blip) else: # error - print self.blip + print (self.blip) # error - print self.blip + print (self.blip) def meth8(self): """test late disabling""" # error - print self.blip + print (self.blip) # pylint: disable=no-member # no error - print self.bla - print self.blop \ No newline at end of file + print (self.bla) + print (self.blop) diff --git a/src/test/pythonFiles/linting/pydocstyleconfig/.pydocstyle b/src/test/pythonFiles/linting/pydocstyleconfig27/.pydocstyle similarity index 100% rename from src/test/pythonFiles/linting/pydocstyleconfig/.pydocstyle rename to src/test/pythonFiles/linting/pydocstyleconfig27/.pydocstyle diff --git a/src/test/pythonFiles/linting/pydocstyleconfig/file.py b/src/test/pythonFiles/linting/pydocstyleconfig27/file.py similarity index 100% rename from src/test/pythonFiles/linting/pydocstyleconfig/file.py rename to src/test/pythonFiles/linting/pydocstyleconfig27/file.py diff --git a/src/test/pythonFiles/shebang/plain.py b/src/test/pythonFiles/shebang/plain.py new file mode 100644 index 000000000000..72f63e675db1 --- /dev/null +++ b/src/test/pythonFiles/shebang/plain.py @@ -0,0 +1,2 @@ + +print("dummy") diff --git a/src/test/pythonFiles/shebang/shebang.py b/src/test/pythonFiles/shebang/shebang.py new file mode 100644 index 000000000000..20d13ba825fb --- /dev/null +++ b/src/test/pythonFiles/shebang/shebang.py @@ -0,0 +1,3 @@ +#!python + +print("dummy") diff --git a/src/test/pythonFiles/shebang/shebangEnv.py b/src/test/pythonFiles/shebang/shebangEnv.py new file mode 100644 index 000000000000..c08ab31b509f --- /dev/null +++ b/src/test/pythonFiles/shebang/shebangEnv.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python + +print("dummy") diff --git a/src/test/pythonFiles/shebang/shebangInvalid.py b/src/test/pythonFiles/shebang/shebangInvalid.py new file mode 100644 index 000000000000..8a66a42523fb --- /dev/null +++ b/src/test/pythonFiles/shebang/shebangInvalid.py @@ -0,0 +1,3 @@ +#!/usr/bin/env1234 python + +print("dummy") diff --git a/src/test/pythonFiles/sorting/withconfig/before.py b/src/test/pythonFiles/sorting/withconfig/before.py index 5f180136e025..e1fd315dbf92 100644 --- a/src/test/pythonFiles/sorting/withconfig/before.py +++ b/src/test/pythonFiles/sorting/withconfig/before.py @@ -1,10 +1,3 @@ -from third_party import lib0 -from third_party import lib1 -from third_party import lib2 -from third_party import lib3 -from third_party import lib4 -from third_party import lib5 -from third_party import lib6 -from third_party import lib7 -from third_party import lib8 -from third_party import lib9 +from third_party import (lib1, lib2, lib3, + lib4, lib5, lib6, + lib7, lib8, lib9) \ No newline at end of file diff --git a/src/test/pythonFiles/symbolFiles/childFile.py b/src/test/pythonFiles/symbolFiles/childFile.py new file mode 100644 index 000000000000..31d6fc7b4a18 --- /dev/null +++ b/src/test/pythonFiles/symbolFiles/childFile.py @@ -0,0 +1,13 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Child2Class(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1OfChild(self, arg): + """this issues a message""" + print (self) diff --git a/src/test/pythonFiles/symbolFiles/file.py b/src/test/pythonFiles/symbolFiles/file.py new file mode 100644 index 000000000000..27509dd2fcd6 --- /dev/null +++ b/src/test/pythonFiles/symbolFiles/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print(self) + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print (self\ + + "foo") + + def meth3(self): + """test one line disabling""" + # no error + print (self.bla) # pylint: disable=no-member + # error + print (self.blop) + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) + # pylint: enable=no-member + # error + print (self.blip) + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + if self.blop: + # pylint: enable=no-member + # error + print (self.blip) + else: + # no error + print (self.blip) + # no error + print (self.blip) + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + try: + # pylint: enable=no-member + # error + print (self.blip) + except UndefinedName: # pylint: disable=undefined-variable + # no error + print (self.blip) + # no error + print (self.blip) + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print (self.blip) + else: + # error + print (self.blip) + # error + print (self.blip) + + + def meth8(self): + """test late disabling""" + # error + print (self.blip) + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) diff --git a/src/test/pythonFiles/symbolFiles/workspace2File.py b/src/test/pythonFiles/symbolFiles/workspace2File.py new file mode 100644 index 000000000000..61aa87c55fed --- /dev/null +++ b/src/test/pythonFiles/symbolFiles/workspace2File.py @@ -0,0 +1,13 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Workspace2Class(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1OfWorkspace2(self, arg): + """this issues a message""" + print (self) diff --git a/src/test/pythonFiles/testFiles/cwd/src/tests/test_cwd.py b/src/test/pythonFiles/testFiles/cwd/src/tests/test_cwd.py new file mode 100644 index 000000000000..33fb0fce9ba6 --- /dev/null +++ b/src/test/pythonFiles/testFiles/cwd/src/tests/test_cwd.py @@ -0,0 +1,14 @@ +import sys +import os + +import unittest + +class Test_Current_Working_Directory(unittest.TestCase): + def test_cwd(self): + testDir = os.path.join(os.getcwd(), 'test') + testFileDir = os.path.dirname(os.path.abspath(__file__)) + self.assertEqual(testDir, testFileDir, 'Not equal' + testDir + testFileDir) + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_one.py b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_one.py new file mode 100644 index 000000000000..db18d3885488 --- /dev/null +++ b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_one.py @@ -0,0 +1,8 @@ +import unittest + +class Test_test_one_1(unittest.TestCase): + def test_1_1_1(self): + self.assertEqual(1,1,'Not equal') + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.py b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.py new file mode 100644 index 000000000000..4e1a6151deb1 --- /dev/null +++ b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.py @@ -0,0 +1,8 @@ +import unittest + +class Test_test_two_2(unittest.TestCase): + def test_2_1_1(self): + self.assertEqual(1,1,'Not equal') + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.txt b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.txt new file mode 100644 index 000000000000..4e1a6151deb1 --- /dev/null +++ b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.txt @@ -0,0 +1,8 @@ +import unittest + +class Test_test_two_2(unittest.TestCase): + def test_2_1_1(self): + self.assertEqual(1,1,'Not equal') + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.updated.txt b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.updated.txt new file mode 100644 index 000000000000..b70c80df1619 --- /dev/null +++ b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.updated.txt @@ -0,0 +1,14 @@ +import unittest + +class Test_test_two_2(unittest.TestCase): + def test_2_1_1(self): + self.assertEqual(1,1,'Not equal') + + def test_2_1_2(self): + self.assertEqual(1,1,'Not equal') + + def test_2_1_3(self): + self.assertEqual(1,1,'Not equal') + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py b/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py new file mode 100644 index 000000000000..4825f3a4db3b --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py @@ -0,0 +1,15 @@ +import sys +import os + +import unittest + +class Test_test1(unittest.TestCase): + def tst_A(self): + self.fail("Not implemented") + + def tst_B(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py b/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py new file mode 100644 index 000000000000..c9a76c07f933 --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py @@ -0,0 +1,18 @@ +import unittest + +class Tst_test2(unittest.TestCase): + def tst_A2(self): + self.fail("Not implemented") + + def tst_B2(self): + self.assertEqual(1,1,'Not equal') + + def tst_C2(self): + self.assertEqual(1,2,'Not equal') + + def tst_D2(self): + raise ArithmeticError() + pass + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/test_root.py b/src/test/pythonFiles/testFiles/noseFiles/test_root.py new file mode 100644 index 000000000000..452813e9a079 --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/test_root.py @@ -0,0 +1,19 @@ +import sys +import os + +import unittest + +class Test_Root_test1(unittest.TestCase): + def test_Root_A(self): + self.fail("Not implemented") + + def test_Root_B(self): + self.assertEqual(1, 1, 'Not equal') + + @unittest.skip("demonstrating skipping") + def test_Root_c(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py b/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py new file mode 100644 index 000000000000..734b84cd342e --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py @@ -0,0 +1,13 @@ +import unittest + + +class Test_test3(unittest.TestCase): + def test4A(self): + self.fail("Not implemented") + + def test4B(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py b/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py new file mode 100644 index 000000000000..e869986b6ead --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py @@ -0,0 +1,19 @@ +import sys +import os + +import unittest + +class Test_test1(unittest.TestCase): + def test_A(self): + self.fail("Not implemented") + + def test_B(self): + self.assertEqual(1, 1, 'Not equal') + + @unittest.skip("demonstrating skipping") + def test_c(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py b/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py new file mode 100644 index 000000000000..ad89d873e879 --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py @@ -0,0 +1,32 @@ +import unittest + +class Test_test2(unittest.TestCase): + def test_A2(self): + self.fail("Not implemented") + + def test_B2(self): + self.assertEqual(1,1,'Not equal') + + def test_C2(self): + self.assertEqual(1,2,'Not equal') + + def test_D2(self): + raise ArithmeticError() + pass + +class Test_test2a(unittest.TestCase): + def test_222A2(self): + self.fail("Not implemented") + + def test_222B2(self): + self.assertEqual(1,1,'Not equal') + + class Test_test2a1(unittest.TestCase): + def test_222A2wow(self): + self.fail("Not implemented") + + def test_222B2wow(self): + self.assertEqual(1,1,'Not equal') + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py b/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py new file mode 100644 index 000000000000..507e6af02063 --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py @@ -0,0 +1,13 @@ +import unittest + + +class Test_test3(unittest.TestCase): + def test_A(self): + self.fail("Not implemented") + + def test_B(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/specificTest/tests/test_unittest_one.py b/src/test/pythonFiles/testFiles/specificTest/tests/test_unittest_one.py new file mode 100644 index 000000000000..72db843aa2af --- /dev/null +++ b/src/test/pythonFiles/testFiles/specificTest/tests/test_unittest_one.py @@ -0,0 +1,19 @@ +import unittest + +class Test_test_one_1(unittest.TestCase): + def test_1_1_1(self): + self.assertEqual(1,1,'Not equal') + + def test_1_1_2(self): + self.assertEqual(1,2,'Not equal') + + @unittest.skip("demonstrating skipping") + def test_1_1_3(self): + self.assertEqual(1,2,'Not equal') + +class Test_test_one_2(unittest.TestCase): + def test_1_2_1(self): + self.assertEqual(1,1,'Not equal') + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/specificTest/tests/test_unittest_two.py b/src/test/pythonFiles/testFiles/specificTest/tests/test_unittest_two.py new file mode 100644 index 000000000000..abac1b49023f --- /dev/null +++ b/src/test/pythonFiles/testFiles/specificTest/tests/test_unittest_two.py @@ -0,0 +1,19 @@ +import unittest + +class Test_test_two_1(unittest.TestCase): + def test_1_1_1(self): + self.assertEqual(1,1,'Not equal') + + def test_1_1_2(self): + self.assertEqual(1,2,'Not equal') + + @unittest.skip("demonstrating skipping") + def test_1_1_3(self): + self.assertEqual(1,2,'Not equal') + +class Test_test_two_2(unittest.TestCase): + def test_2_1_1(self): + self.assertEqual(1,1,'Not equal') + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/extension.refactor.extract.method.test.ts b/src/test/refactor/extension.refactor.extract.method.test.ts similarity index 57% rename from src/test/extension.refactor.extract.method.test.ts rename to src/test/refactor/extension.refactor.extract.method.test.ts index eac3f30cb2f2..436c1f8f519f 100644 --- a/src/test/extension.refactor.extract.method.test.ts +++ b/src/test/refactor/extension.refactor.extract.method.test.ts @@ -1,133 +1,69 @@ -// Place this right on top -import { initialize, closeActiveWindows, PYTHON_PATH, IS_TRAVIS } from './initialize'; import * as assert from 'assert'; // You can import and use all API from the \'vscode\' module // as well as import your extension to test it import * as vscode from 'vscode'; -import { TextDocument, TextLine, Position, Range } from 'vscode'; import * as path from 'path'; -import * as settings from '../client/common/configSettings'; import * as fs from 'fs-extra'; -import { extractMethod } from '../client/providers/simpleRefactorProvider'; -import { RefactorProxy } from '../client/refactor/proxy'; -import { getTextEditsFromPatch } from '../client/common/editor'; - -let EXTENSION_DIR = path.join(__dirname, '..', '..'); -let pythonSettings = settings.PythonSettings.getInstance(); - -const refactorSourceFile = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'refactoring', 'standAlone', 'refactor.py'); -const refactorTargetFile = path.join(__dirname, '..', '..', 'out', 'test', 'pythonFiles', 'refactoring', 'standAlone', 'refactor.py'); +import { PythonSettings } from '../../client/common/configSettings'; +import { initialize, closeActiveWindows, IS_TRAVIS, wait, initializeTest } from './../initialize'; +import { Position } from 'vscode'; +import { extractMethod } from '../../client/providers/simpleRefactorProvider'; +import { RefactorProxy } from '../../client/refactor/proxy'; +import { getTextEditsFromPatch } from '../../client/common/editor'; +import { MockOutputChannel } from './../mockClasses'; + +const EXTENSION_DIR = path.join(__dirname, '..', '..', '..'); +const refactorSourceFile = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'refactoring', 'standAlone', 'refactor.py'); +const refactorTargetFile = path.join(__dirname, '..', '..', '..', 'out', 'test', 'pythonFiles', 'refactoring', 'standAlone', 'refactor.py'); interface RenameResponse { results: [{ diff: string }]; } -class MockOutputChannel implements vscode.OutputChannel { - constructor(name: string) { - this.name = name; - this.output = ''; - } - name: string; - output: string; - append(value: string) { - this.output += value; - } - appendLine(value: string) { this.append(value); this.append('\n'); } - clear() { } - show(preservceFocus?: boolean): void; - show(column?: vscode.ViewColumn, preserveFocus?: boolean): void; - show(x?: any, y?: any): void { } - hide() { } - dispose() { } -} -class MockTextDocument implements vscode.TextDocument { - uri: vscode.Uri; - fileName: string; - isUntitled: boolean; - languageId: string; - version: number; - isDirty: boolean; - offsets: [{ position: Position, offset: number }]; - constructor(private sourceFile: string) { - this.lineCount = fs.readFileSync(this.sourceFile, 'utf8').split(/\r?\n/g).length; - this.offsets = [ - { position: new vscode.Position(239, 30), offset: 8376 }, - { position: new vscode.Position(239, 0), offset: 8346 }, - { position: new vscode.Position(241, 35), offset: 8519 } - ]; - } - save(): Thenable { - return Promise.resolve(true); - } - lineCount: number; - lineAt(position: Position | number): TextLine { - let lineNumber: number = position as number; - if ((position as Position).line) { - lineNumber = (position as Position).line; - } - let line = fs.readFileSync(this.sourceFile, 'utf8').split(/\r?\n/g)[lineNumber]; - - return { isEmptyOrWhitespace: line.trim().length > 0 }; - } - offsetAt(position: Position): number { - return this.offsets.filter(item => item.position.isEqual(position))[0].offset; - } - positionAt(offset: number): Position { - return null; - } - getText(range?: Range): string { - return fs.readFileSync(this.sourceFile, 'utf8'); - } - getWordRangeAtPosition(position: Position): Range { - return null; - } - validateRange(range: Range): Range { - return null; - } - validatePosition(position: Position): Position { - return null; - } -} - suite('Method Extraction', () => { // Hack hac hack const oldExecuteCommand = vscode.commands.executeCommand; const options: vscode.TextEditorOptions = { cursorStyle: vscode.TextEditorCursorStyle.Line, insertSpaces: true, lineNumbers: vscode.TextEditorLineNumbersStyle.Off, tabSize: 4 }; - suiteSetup(done => { - fs.copySync(refactorSourceFile, refactorTargetFile, { clobber: true }); - pythonSettings.pythonPath = PYTHON_PATH; - initialize().then(() => done(), () => done()); + suiteSetup(() => { + fs.copySync(refactorSourceFile, refactorTargetFile, { overwrite: true }); + return initialize(); }); - suiteTeardown(done => { + suiteTeardown(() => { vscode.commands.executeCommand = oldExecuteCommand; - closeActiveWindows().then(done, done); + return closeActiveWindows(); }); - setup(done => { + setup(async () => { if (fs.existsSync(refactorTargetFile)) { + await wait(500); fs.unlinkSync(refactorTargetFile); } - fs.copySync(refactorSourceFile, refactorTargetFile, { clobber: true }); - closeActiveWindows().then(() => { - vscode.commands.executeCommand = (cmd) => Promise.resolve(); - done(); - }).catch(done); + fs.copySync(refactorSourceFile, refactorTargetFile, { overwrite: true }); + await initializeTest(); + (vscode).commands.executeCommand = (cmd) => Promise.resolve(); }); - teardown(done => { + teardown(() => { vscode.commands.executeCommand = oldExecuteCommand; - closeActiveWindows().then(done, done); + return closeActiveWindows(); }); - function testingMethodExtraction(shouldError: boolean, pythonSettings: settings.IPythonSettings, startPos: Position, endPos: Position) { + function testingMethodExtraction(shouldError: boolean, startPos: Position, endPos: Position) { + const pythonSettings = PythonSettings.getInstance(vscode.Uri.file(refactorTargetFile)); let rangeOfTextToExtract = new vscode.Range(startPos, endPos); let proxy = new RefactorProxy(EXTENSION_DIR, pythonSettings, path.dirname(refactorTargetFile)); - let mockTextDoc = new MockTextDocument(refactorTargetFile); + let expectedTextEdits: vscode.TextEdit[]; let ignoreErrorHandling = false; - + let mockTextDoc: vscode.TextDocument; const DIFF = `--- a/refactor.py\n+++ b/refactor.py\n@@ -237,9 +237,12 @@\n try:\n self._process_request(self._input.readline())\n except Exception as ex:\n- message = ex.message + ' \\n' + traceback.format_exc()\n- sys.stderr.write(str(len(message)) + ':' + message)\n- sys.stderr.flush()\n+ self.myNewMethod(ex)\n+\n+ def myNewMethod(self, ex):\n+ message = ex.message + ' \\n' + traceback.format_exc()\n+ sys.stderr.write(str(len(message)) + ':' + message)\n+ sys.stderr.flush()\n \n if __name__ == '__main__':\n RopeRefactoring().watch()\n`; - let expectedTextEdits = getTextEditsFromPatch(mockTextDoc.getText(), DIFF); - return proxy.extractMethod(mockTextDoc, 'myNewMethod', refactorTargetFile, rangeOfTextToExtract, options) + return new Promise((resolve, reject) => { + vscode.workspace.openTextDocument(refactorTargetFile).then(textDocument => { + mockTextDoc = textDocument; + expectedTextEdits = getTextEditsFromPatch(textDocument.getText(), DIFF); + resolve(); + }, error => reject(error)); + }) + .then(() => proxy.extractMethod(mockTextDoc, 'myNewMethod', refactorTargetFile, rangeOfTextToExtract, options)) .then(response => { if (shouldError) { ignoreErrorHandling = true; @@ -157,16 +93,16 @@ suite('Method Extraction', () => { test('Extract Method', done => { let startPos = new vscode.Position(239, 0); let endPos = new vscode.Position(241, 35); - testingMethodExtraction(false, pythonSettings, startPos, endPos).then(() => done(), done); + testingMethodExtraction(false, startPos, endPos).then(() => done(), done); }); test('Extract Method will fail if complete statements are not selected', done => { let startPos = new vscode.Position(239, 30); let endPos = new vscode.Position(241, 35); - testingMethodExtraction(true, pythonSettings, startPos, endPos).then(() => done(), done); + testingMethodExtraction(true, startPos, endPos).then(() => done(), done); }); - function testingMethodExtractionEndToEnd(shouldError: boolean, pythonSettings: settings.IPythonSettings, startPos: Position, endPos: Position) { + function testingMethodExtractionEndToEnd(shouldError: boolean, startPos: Position, endPos: Position) { let ch = new MockOutputChannel('Python'); let textDocument: vscode.TextDocument; let textEditor: vscode.TextEditor; @@ -183,7 +119,7 @@ suite('Method Extraction', () => { textEditor = editor; return; }).then(() => { - return extractMethod(EXTENSION_DIR, textEditor, rangeOfTextToExtract, ch, path.dirname(refactorTargetFile), pythonSettings).then(() => { + return extractMethod(EXTENSION_DIR, textEditor, rangeOfTextToExtract, ch).then(() => { if (shouldError) { ignoreErrorHandling = true; assert.fail('No error', 'Error', 'Extraction should fail with an error', ''); @@ -225,13 +161,13 @@ suite('Method Extraction', () => { test('Extract Method (end to end)', done => { let startPos = new vscode.Position(239, 0); let endPos = new vscode.Position(241, 35); - testingMethodExtractionEndToEnd(false, pythonSettings, startPos, endPos).then(() => done(), done); + testingMethodExtractionEndToEnd(false, startPos, endPos).then(() => done(), done); }); } test('Extract Method will fail if complete statements are not selected', done => { let startPos = new vscode.Position(239, 30); let endPos = new vscode.Position(241, 35); - testingMethodExtractionEndToEnd(true, pythonSettings, startPos, endPos).then(() => done(), done); + testingMethodExtractionEndToEnd(true, startPos, endPos).then(() => done(), done); }); -}); \ No newline at end of file +}); diff --git a/src/test/extension.refactor.extract.var.test.ts b/src/test/refactor/extension.refactor.extract.var.test.ts similarity index 56% rename from src/test/extension.refactor.extract.var.test.ts rename to src/test/refactor/extension.refactor.extract.var.test.ts index a2769fb719a6..06144c333e18 100644 --- a/src/test/extension.refactor.extract.var.test.ts +++ b/src/test/refactor/extension.refactor.extract.var.test.ts @@ -1,134 +1,68 @@ -// Place this right on top -import { initialize, closeActiveWindows, PYTHON_PATH, IS_TRAVIS } from './initialize'; -/// import * as assert from 'assert'; // You can import and use all API from the \'vscode\' module // as well as import your extension to test it import * as vscode from 'vscode'; -import { TextDocument, TextLine, Position, Range } from 'vscode'; import * as path from 'path'; -import * as settings from '../client/common/configSettings'; import * as fs from 'fs-extra'; -import { extractVariable } from '../client/providers/simpleRefactorProvider'; -import { RefactorProxy } from '../client/refactor/proxy'; -import { getTextEditsFromPatch } from '../client/common/editor'; - -let EXTENSION_DIR = path.join(__dirname, '..', '..'); -let pythonSettings = settings.PythonSettings.getInstance(); - -const refactorSourceFile = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'refactoring', 'standAlone', 'refactor.py'); -const refactorTargetFile = path.join(__dirname, '..', '..', 'out', 'test', 'pythonFiles', 'refactoring', 'standAlone', 'refactor.py'); +import { PythonSettings } from '../../client/common/configSettings'; +import { closeActiveWindows, initialize, initializeTest, IS_TRAVIS, wait } from './../initialize'; +import { Position } from 'vscode'; +import { extractVariable } from '../../client/providers/simpleRefactorProvider'; +import { RefactorProxy } from '../../client/refactor/proxy'; +import { getTextEditsFromPatch } from '../../client/common/editor'; +import { MockOutputChannel } from './../mockClasses'; + +const EXTENSION_DIR = path.join(__dirname, '..', '..', '..'); +const refactorSourceFile = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'refactoring', 'standAlone', 'refactor.py'); +const refactorTargetFile = path.join(__dirname, '..', '..', '..', 'out', 'test', 'pythonFiles', 'refactoring', 'standAlone', 'refactor.py'); interface RenameResponse { results: [{ diff: string }]; } -class MockOutputChannel implements vscode.OutputChannel { - constructor(name: string) { - this.name = name; - this.output = ''; - } - name: string; - output: string; - append(value: string) { - this.output += value; - } - appendLine(value: string) { this.append(value); this.append('\n'); } - clear() { } - show(preservceFocus?: boolean): void; - show(column?: vscode.ViewColumn, preserveFocus?: boolean): void; - show(x?: any, y?: any): void { } - hide() { } - dispose() { } -} -class MockTextDocument implements vscode.TextDocument { - uri: vscode.Uri; - fileName: string; - isUntitled: boolean; - languageId: string; - version: number; - isDirty: boolean; - offsets: [{ position: Position, offset: number }]; - constructor(private sourceFile: string) { - this.lineCount = fs.readFileSync(this.sourceFile, 'utf8').split(/\r?\n/g).length; - this.offsets = [ - { position: new vscode.Position(234, 20), offset: 8191 }, - { position: new vscode.Position(234, 29), offset: 8200 }, - { position: new vscode.Position(234, 38), offset: 8209 } - ]; - } - save(): Thenable { - return Promise.resolve(true); - } - lineCount: number; - lineAt(position: Position | number): TextLine { - let lineNumber: number = position as number; - if ((position as Position).line) { - lineNumber = (position as Position).line; - } - let line = fs.readFileSync(this.sourceFile, 'utf8').split(/\r?\n/g)[lineNumber]; - - return { isEmptyOrWhitespace: line.trim().length > 0 }; - } - offsetAt(position: Position): number { - return this.offsets.filter(item => item.position.isEqual(position))[0].offset; - } - positionAt(offset: number): Position { - return null; - } - getText(range?: Range): string { - return fs.readFileSync(this.sourceFile, 'utf8'); - } - getWordRangeAtPosition(position: Position): Range { - return null; - } - validateRange(range: Range): Range { - return null; - } - validatePosition(position: Position): Position { - return null; - } -} - suite('Variable Extraction', () => { // Hack hac hack const oldExecuteCommand = vscode.commands.executeCommand; const options: vscode.TextEditorOptions = { cursorStyle: vscode.TextEditorCursorStyle.Line, insertSpaces: true, lineNumbers: vscode.TextEditorLineNumbersStyle.Off, tabSize: 4 }; - suiteSetup(done => { - fs.copySync(refactorSourceFile, refactorTargetFile, { clobber: true }); - pythonSettings.pythonPath = PYTHON_PATH; - initialize().then(() => done(), () => done()); + suiteSetup(async () => { + fs.copySync(refactorSourceFile, refactorTargetFile, { overwrite: true }); + await initialize(); }); - suiteTeardown(done => { + suiteTeardown(() => { vscode.commands.executeCommand = oldExecuteCommand; - closeActiveWindows().then(done, done); + return closeActiveWindows(); }); - setup(done => { + setup(async () => { if (fs.existsSync(refactorTargetFile)) { + await wait(500); fs.unlinkSync(refactorTargetFile); } - fs.copySync(refactorSourceFile, refactorTargetFile, { clobber: true }); - closeActiveWindows().then(() => { - vscode.commands.executeCommand = (cmd) => Promise.resolve(); - done(); - }).catch(done); + fs.copySync(refactorSourceFile, refactorTargetFile, { overwrite: true }); + await initializeTest(); + (vscode).commands.executeCommand = (cmd) => Promise.resolve(); }); - teardown(done => { + teardown(() => { vscode.commands.executeCommand = oldExecuteCommand; - closeActiveWindows().then(done, done); + return closeActiveWindows(); }); - function testingVariableExtraction(shouldError: boolean, pythonSettings: settings.IPythonSettings, startPos: Position, endPos: Position) { + function testingVariableExtraction(shouldError: boolean, startPos: Position, endPos: Position) { + const pythonSettings = PythonSettings.getInstance(vscode.Uri.file(refactorTargetFile)); let rangeOfTextToExtract = new vscode.Range(startPos, endPos); let proxy = new RefactorProxy(EXTENSION_DIR, pythonSettings, path.dirname(refactorTargetFile)); - let mockTextDoc = new MockTextDocument(refactorTargetFile); + let expectedTextEdits: vscode.TextEdit[]; let ignoreErrorHandling = false; - + let mockTextDoc: vscode.TextDocument; const DIFF = '--- a/refactor.py\n+++ b/refactor.py\n@@ -232,7 +232,8 @@\n sys.stdout.flush()\n \n def watch(self):\n- self._write_response("STARTED")\n+ myNewVariable = "STARTED"\n+ self._write_response(myNewVariable)\n while True:\n try:\n self._process_request(self._input.readline())\n'; - let expectedTextEdits = getTextEditsFromPatch(mockTextDoc.getText(), DIFF); - - return proxy.extractVariable(mockTextDoc, 'myNewVariable', refactorTargetFile, rangeOfTextToExtract, options) + return new Promise((resolve, reject) => { + vscode.workspace.openTextDocument(refactorTargetFile).then(textDocument => { + mockTextDoc = textDocument; + expectedTextEdits = getTextEditsFromPatch(textDocument.getText(), DIFF); + resolve(); + }, error => reject(error)) + }) + .then(() => proxy.extractVariable(mockTextDoc, 'myNewVariable', refactorTargetFile, rangeOfTextToExtract, options)) .then(response => { if (shouldError) { ignoreErrorHandling = true; @@ -158,16 +92,16 @@ suite('Variable Extraction', () => { test('Extract Variable', done => { let startPos = new vscode.Position(234, 29); let endPos = new vscode.Position(234, 38); - testingVariableExtraction(false, pythonSettings, startPos, endPos).then(() => done(), done); + testingVariableExtraction(false, startPos, endPos).then(() => done(), done); }); test('Extract Variable fails if whole string not selected', done => { let startPos = new vscode.Position(234, 20); let endPos = new vscode.Position(234, 38); - testingVariableExtraction(true, pythonSettings, startPos, endPos).then(() => done(), done); + testingVariableExtraction(true, startPos, endPos).then(() => done(), done); }); - function testingVariableExtractionEndToEnd(shouldError: boolean, pythonSettings: settings.IPythonSettings, startPos: Position, endPos: Position) { + function testingVariableExtractionEndToEnd(shouldError: boolean, startPos: Position, endPos: Position) { let ch = new MockOutputChannel('Python'); let textDocument: vscode.TextDocument; let textEditor: vscode.TextEditor; @@ -183,13 +117,13 @@ suite('Variable Extraction', () => { textEditor = editor; return; }).then(() => { - return extractVariable(EXTENSION_DIR, textEditor, rangeOfTextToExtract, ch, path.dirname(refactorTargetFile), pythonSettings).then(() => { + return extractVariable(EXTENSION_DIR, textEditor, rangeOfTextToExtract, ch).then(() => { if (shouldError) { ignoreErrorHandling = true; assert.fail('No error', 'Error', 'Extraction should fail with an error', ''); } return textEditor.document.save(); - }).then(()=>{ + }).then(() => { assert.equal(ch.output.length, 0, 'Output channel is not empty'); assert.equal(textDocument.lineAt(234).text.trim().indexOf('newvariable'), 0, 'New Variable not created'); assert.equal(textDocument.lineAt(234).text.trim().endsWith('= "STARTED"'), true, 'Started Text Assigned to variable'); @@ -226,13 +160,13 @@ suite('Variable Extraction', () => { test('Extract Variable (end to end)', done => { let startPos = new vscode.Position(234, 29); let endPos = new vscode.Position(234, 38); - testingVariableExtractionEndToEnd(false, pythonSettings, startPos, endPos).then(() => done(), done); + testingVariableExtractionEndToEnd(false, startPos, endPos).then(() => done(), done); }); } test('Extract Variable fails if whole string not selected (end to end)', done => { let startPos = new vscode.Position(234, 20); let endPos = new vscode.Position(234, 38); - testingVariableExtractionEndToEnd(true, pythonSettings, startPos, endPos).then(() => done(), done); + testingVariableExtractionEndToEnd(true, startPos, endPos).then(() => done(), done); }); }); diff --git a/src/test/standardTest.ts b/src/test/standardTest.ts new file mode 100644 index 000000000000..ee31d40bb142 --- /dev/null +++ b/src/test/standardTest.ts @@ -0,0 +1,8 @@ +import * as path from 'path'; + +process.env.CODE_TESTS_WORKSPACE = path.join(__dirname, '..', '..', 'src', 'test'); + +function start() { + require('../../node_modules/vscode/bin/test'); +} +start(); diff --git a/src/test/textUtils.ts b/src/test/textUtils.ts new file mode 100644 index 000000000000..d83034e73045 --- /dev/null +++ b/src/test/textUtils.ts @@ -0,0 +1,5 @@ +import { MarkedString } from 'vscode'; + +export function normalizeMarkedString(content: MarkedString): string { + return typeof content === 'string' ? content : content.value; +} \ No newline at end of file diff --git a/src/test/unittests/debugger.test.ts b/src/test/unittests/debugger.test.ts new file mode 100644 index 000000000000..e02fd63422e1 --- /dev/null +++ b/src/test/unittests/debugger.test.ts @@ -0,0 +1,202 @@ +import { assert, expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as path from 'path'; +import { ConfigurationTarget } from 'vscode'; +import { createDeferred } from '../../client/common/helpers'; +import { BaseTestManager } from '../../client/unittests/common/baseTestManager'; +import { CANCELLATION_REASON, CommandSource } from '../../client/unittests/common/constants'; +import { TestCollectionStorageService } from '../../client/unittests/common/storageService'; +import { TestResultsService } from '../../client/unittests/common/testResultsService'; +import { TestsHelper } from '../../client/unittests/common/testUtils'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper } from '../../client/unittests/common/types'; +import { TestResultDisplay } from '../../client/unittests/display/main'; +import { TestManager as NosetestManager } from '../../client/unittests/nosetest/main'; +import { TestManager as PytestManager } from '../../client/unittests/pytest/main'; +import { TestManager as UnitTestManager } from '../../client/unittests/unittest/main'; +import { deleteDirectory, rootWorkspaceUri, updateSetting } from '../common'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; +import { MockOutputChannel } from './../mockClasses'; +import { MockDebugLauncher } from './mocks'; + +use(chaiAsPromised); + +const testFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'debuggerTest'); +const defaultUnitTestArgs = [ + '-v', + '-s', + '.', + '-p', + '*test*.py' +]; + +// tslint:disable-next-line:max-func-body-length +suite('Unit Tests Debugging', () => { + let testManager: BaseTestManager; + let testResultDisplay: TestResultDisplay; + let outChannel: MockOutputChannel; + let storageService: ITestCollectionStorageService; + let resultsService: ITestResultsService; + let mockDebugLauncher: MockDebugLauncher; + let testsHelper: ITestsHelper; + const configTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; + suiteSetup(async function () { + // Test disvovery is where the delay is, hence give 10 seconds (as we discover tests at least twice in each test). + // tslint:disable-next-line:no-invalid-this + this.timeout(10000); + await initialize(); + await updateSetting('unitTest.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); + await updateSetting('unitTest.nosetestArgs', [], rootWorkspaceUri, configTarget); + await updateSetting('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); + }); + setup(async () => { + outChannel = new MockOutputChannel('Python Test Log'); + testResultDisplay = new TestResultDisplay(outChannel); + await deleteDirectory(path.join(testFilesPath, '.cache')); + await initializeTest(); + }); + teardown(async () => { + await updateSetting('unitTest.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); + await updateSetting('unitTest.nosetestArgs', [], rootWorkspaceUri, configTarget); + await updateSetting('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); + outChannel.dispose(); + mockDebugLauncher.dispose(); + if (testManager) { + testManager.dispose(); + } + testResultDisplay.dispose(); + }); + + function createTestManagerDepedencies() { + storageService = new TestCollectionStorageService(); + resultsService = new TestResultsService(); + testsHelper = new TestsHelper(); + mockDebugLauncher = new MockDebugLauncher(); + } + + async function testStartingDebugger() { + const tests = await testManager.discoverTests(CommandSource.commandPalette, true, true); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); + + const testFunction = [tests.testFunctions[0].testFunction]; + // tslint:disable-next-line:no-floating-promises + testManager.runTest(CommandSource.commandPalette, { testFunction }, false, true); + const launched = await mockDebugLauncher.launched; + assert.isTrue(launched, 'Debugger not launched'); + } + + test('Debugger should start (unittest)', async () => { + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new UnitTestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await testStartingDebugger(); + }); + + test('Debugger should start (pytest)', async () => { + await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new PytestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await testStartingDebugger(); + }); + + test('Debugger should start (nosetest)', async () => { + await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new NosetestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await testStartingDebugger(); + }); + + async function testStoppingDebugger() { + const tests = await testManager.discoverTests(CommandSource.commandPalette, true, true); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); + + const testFunction = [tests.testFunctions[0].testFunction]; + const runningPromise = testManager.runTest(CommandSource.commandPalette, { testFunction }, false, true); + const launched = await mockDebugLauncher.launched; + assert.isTrue(launched, 'Debugger not launched'); + + // tslint:disable-next-line:no-floating-promises + testManager.discoverTests(CommandSource.commandPalette, true, true, true); + + await expect(runningPromise).to.be.rejectedWith(CANCELLATION_REASON, 'Incorrect reason for ending the debugger'); + } + + test('Debugger should stop when user invokes a test discovery (unittest)', async () => { + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new UnitTestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await testStoppingDebugger(); + }); + + test('Debugger should stop when user invokes a test discovery (pytest)', async () => { + await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new PytestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await testStoppingDebugger(); + }); + + test('Debugger should stop when user invokes a test discovery (nosetest)', async () => { + await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new NosetestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await testStoppingDebugger(); + }); + + async function testDebuggerWhenRediscoveringTests() { + const tests = await testManager.discoverTests(CommandSource.commandPalette, true, true); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); + + const testFunction = [tests.testFunctions[0].testFunction]; + const runningPromise = testManager.runTest(CommandSource.commandPalette, { testFunction }, false, true); + const launched = await mockDebugLauncher.launched; + assert.isTrue(launched, 'Debugger not launched'); + + const discoveryPromise = testManager.discoverTests(CommandSource.commandPalette, false, true); + const deferred = createDeferred(); + + // tslint:disable-next-line:no-floating-promises + discoveryPromise + // tslint:disable-next-line:no-unsafe-any + .then(() => deferred.resolve('')) + // tslint:disable-next-line:no-unsafe-any + .catch(ex => deferred.reject(ex)); + + // This promise should never resolve nor reject. + // tslint:disable-next-line:no-floating-promises + runningPromise + .then(() => 'Debugger stopped when it shouldn\'t have') + .catch(() => 'Debugger crashed when it shouldn\'t have') + .then(error => { + deferred.reject(error); + }); + + // Should complete without any errors + await deferred.promise; + } + + test('Debugger should not stop when test discovery is invoked automatically by extension (unittest)', async () => { + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new UnitTestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await testDebuggerWhenRediscoveringTests(); + }); + + test('Debugger should not stop when test discovery is invoked automatically by extension (pytest)', async () => { + await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new PytestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await testDebuggerWhenRediscoveringTests(); + }); + + test('Debugger should not stop when test discovery is invoked automatically by extension (nosetest)', async () => { + await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new NosetestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await testDebuggerWhenRediscoveringTests(); + }); +}); diff --git a/src/test/unittests/mocks.ts b/src/test/unittests/mocks.ts new file mode 100644 index 000000000000..fd9741543a78 --- /dev/null +++ b/src/test/unittests/mocks.ts @@ -0,0 +1,70 @@ +import { CancellationToken, Disposable, OutputChannel } from 'vscode'; +import { createDeferred, Deferred } from '../../client/common/helpers'; +import { Product } from '../../client/common/installer'; +import { BaseTestManager } from '../../client/unittests/common/baseTestManager'; +import { CANCELLATION_REASON } from '../../client/unittests/common/constants'; +import { ITestCollectionStorageService, ITestDebugLauncher, ITestResultsService, ITestsHelper, Tests, TestsToRun } from '../../client/unittests/common/types'; + +export class MockDebugLauncher implements ITestDebugLauncher, Disposable { + public get launched(): Promise { + return this._launched.promise; + } + public get debuggerPromise(): Deferred { + // tslint:disable-next-line:no-non-null-assertion + return this._promise!; + } + public get cancellationToken(): CancellationToken { + return this._token; + } + // tslint:disable-next-line:variable-name + private _launched: Deferred; + // tslint:disable-next-line:variable-name + private _promise?: Deferred; + // tslint:disable-next-line:variable-name + private _token: CancellationToken; + constructor() { + this._launched = createDeferred(); + } + public async launchDebugger(rootDirectory: string, testArgs: string[], token?: CancellationToken, outChannel?: OutputChannel): Promise { + this._launched.resolve(true); + // tslint:disable-next-line:no-non-null-assertion + this._token = token!; + this._promise = createDeferred(); + // tslint:disable-next-line:no-non-null-assertion + token!.onCancellationRequested(() => { + if (this._promise) { + this._promise.reject('Mock-User Cancelled'); + } + }); + return this._promise.promise; + } + public dispose() { + this._promise = undefined; + } +} + +export class MockTestManagerWithRunningTests extends BaseTestManager { + // tslint:disable-next-line:no-any + public readonly runnerDeferred = createDeferred(); + // tslint:disable-next-line:no-any + public readonly discoveryDeferred = createDeferred(); + constructor(testRunnerId: 'nosetest' | 'pytest' | 'unittest', product: Product, rootDirectory: string, + outputChannel: OutputChannel, storageService: ITestCollectionStorageService, resultsService: ITestResultsService, testsHelper: ITestsHelper) { + super('nosetest', product, rootDirectory, outputChannel, storageService, resultsService, testsHelper); + } + // tslint:disable-next-line:no-any + protected async runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { + // tslint:disable-next-line:no-non-null-assertion + this.testRunnerCancellationToken!.onCancellationRequested(() => { + this.runnerDeferred.reject(CANCELLATION_REASON); + }); + return this.runnerDeferred.promise; + } + protected async discoverTestsImpl(ignoreCache: boolean, debug?: boolean): Promise { + // tslint:disable-next-line:no-non-null-assertion + this.testDiscoveryCancellationToken!.onCancellationRequested(() => { + this.discoveryDeferred.reject(CANCELLATION_REASON); + }); + return this.discoveryDeferred.promise; + } +} diff --git a/src/test/unittests/nosetest.test.ts b/src/test/unittests/nosetest.test.ts new file mode 100644 index 000000000000..c932a1950332 --- /dev/null +++ b/src/test/unittests/nosetest.test.ts @@ -0,0 +1,197 @@ +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { CommandSource } from '../../client/unittests/common/constants'; +import { TestCollectionStorageService } from '../../client/unittests/common/storageService'; +import { TestResultsService } from '../../client/unittests/common/testResultsService'; +import { TestsHelper } from '../../client/unittests/common/testUtils'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, Tests, TestsToRun } from '../../client/unittests/common/types'; +import { TestResultDisplay } from '../../client/unittests/display/main'; +import * as nose from '../../client/unittests/nosetest/main'; +import { rootWorkspaceUri, updateSetting } from '../common'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; +import { MockOutputChannel } from './../mockClasses'; +import { MockDebugLauncher } from './mocks'; + +const UNITTEST_TEST_FILES_PATH = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'noseFiles'); +const UNITTEST_SINGLE_TEST_FILE_PATH = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'single'); +const filesToDelete = [ + path.join(UNITTEST_TEST_FILES_PATH, '.noseids'), + path.join(UNITTEST_SINGLE_TEST_FILE_PATH, '.noseids') +]; + +// tslint:disable-next-line:max-func-body-length +suite('Unit Tests (nosetest)', () => { + const configTarget = IS_MULTI_ROOT_TEST ? vscode.ConfigurationTarget.WorkspaceFolder : vscode.ConfigurationTarget.Workspace; + const rootDirectory = UNITTEST_TEST_FILES_PATH; + let testManager: nose.TestManager; + let testResultDisplay: TestResultDisplay; + let outChannel: vscode.OutputChannel; + let storageService: ITestCollectionStorageService; + let resultsService: ITestResultsService; + let testsHelper: ITestsHelper; + + suiteSetup(async () => { + filesToDelete.forEach(file => { + if (fs.existsSync(file)) { + fs.unlinkSync(file); + } + }); + await updateSetting('unitTest.nosetestArgs', [], rootWorkspaceUri, configTarget); + await initialize(); + }); + suiteTeardown(async () => { + await updateSetting('unitTest.nosetestArgs', [], rootWorkspaceUri, configTarget); + filesToDelete.forEach(file => { + if (fs.existsSync(file)) { + fs.unlinkSync(file); + } + }); + }); + setup(async () => { + outChannel = new MockOutputChannel('Python Test Log'); + testResultDisplay = new TestResultDisplay(outChannel); + await initializeTest(); + }); + teardown(async () => { + outChannel.dispose(); + testManager.dispose(); + testResultDisplay.dispose(); + await updateSetting('unitTest.nosetestArgs', [], rootWorkspaceUri, configTarget); + }); + function createTestManager(rootDir: string = rootDirectory) { + storageService = new TestCollectionStorageService(); + resultsService = new TestResultsService(); + testsHelper = new TestsHelper(); + testManager = new nose.TestManager(rootDir, outChannel, storageService, resultsService, testsHelper, new MockDebugLauncher()); + } + + test('Discover Tests (single test file)', async () => { + createTestManager(UNITTEST_SINGLE_TEST_FILE_PATH); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); + assert.equal(tests.testFiles.some(t => t.name === path.join('tests', 'test_one.py') && t.nameToRun === t.name), true, 'Test File not found'); + }); + + test('Check that nameToRun in testSuites has class name after : (single test file)', async () => { + createTestManager(UNITTEST_SINGLE_TEST_FILE_PATH); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); + assert.equal(tests.testSuites.every(t => t.testSuite.name === t.testSuite.nameToRun.split(':')[1]), true, 'Suite name does not match class name'); + }); + + function lookForTestFile(tests: Tests, testFile: string) { + const found = tests.testFiles.some(t => t.name === testFile && t.nameToRun === t.name); + assert.equal(found, true, `Test File not found '${testFile}'`); + } + test('Discover Tests (-m=test)', async () => { + await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); + createTestManager(); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + assert.equal(tests.testFiles.length, 5, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 16, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 6, 'Incorrect number of test suites'); + lookForTestFile(tests, path.join('tests', 'test_unittest_one.py')); + lookForTestFile(tests, path.join('tests', 'test_unittest_two.py')); + lookForTestFile(tests, path.join('tests', 'unittest_three_test.py')); + lookForTestFile(tests, path.join('tests', 'test4.py')); + lookForTestFile(tests, 'test_root.py'); + }); + + test('Discover Tests (-w=specific -m=tst)', async () => { + await updateSetting('unitTest.nosetestArgs', ['-w', 'specific', '-m', 'tst'], rootWorkspaceUri, configTarget); + createTestManager(); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); + lookForTestFile(tests, path.join('specific', 'tst_unittest_one.py')); + lookForTestFile(tests, path.join('specific', 'tst_unittest_two.py')); + }); + + test('Discover Tests (-m=test_)', async () => { + await updateSetting('unitTest.nosetestArgs', ['-m', 'test_'], rootWorkspaceUri, configTarget); + createTestManager(); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 3, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); + lookForTestFile(tests, 'test_root.py'); + }); + + test('Run Tests', async () => { + await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); + createTestManager(); + const results = await testManager.runTest(CommandSource.ui); + assert.equal(results.summary.errors, 1, 'Errors'); + assert.equal(results.summary.failures, 7, 'Failures'); + assert.equal(results.summary.passed, 6, 'Passed'); + assert.equal(results.summary.skipped, 2, 'skipped'); + }); + + test('Run Failed Tests', async () => { + await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); + createTestManager(); + let results = await testManager.runTest(CommandSource.ui); + assert.equal(results.summary.errors, 1, 'Errors'); + assert.equal(results.summary.failures, 7, 'Failures'); + assert.equal(results.summary.passed, 6, 'Passed'); + assert.equal(results.summary.skipped, 2, 'skipped'); + + results = await testManager.runTest(CommandSource.ui, undefined, true); + assert.equal(results.summary.errors, 1, 'Errors again'); + assert.equal(results.summary.failures, 7, 'Failures again'); + assert.equal(results.summary.passed, 0, 'Passed again'); + assert.equal(results.summary.skipped, 0, 'skipped again'); + }); + + test('Run Specific Test File', async () => { + await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); + createTestManager(); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + const testFileToRun = tests.testFiles.find(t => t.fullPath.endsWith('test_root.py')); + assert.ok(testFileToRun, 'Test file not found'); + // tslint:disable-next-line:no-non-null-assertion + const testFile: TestsToRun = { testFile: [testFileToRun!], testFolder: [], testFunction: [], testSuite: [] }; + const results = await testManager.runTest(CommandSource.ui, testFile); + assert.equal(results.summary.errors, 0, 'Errors'); + assert.equal(results.summary.failures, 1, 'Failures'); + assert.equal(results.summary.passed, 1, 'Passed'); + assert.equal(results.summary.skipped, 1, 'skipped'); + }); + + test('Run Specific Test Suite', async () => { + await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); + createTestManager(); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + const testSuiteToRun = tests.testSuites.find(s => s.xmlClassName === 'test_root.Test_Root_test1'); + assert.ok(testSuiteToRun, 'Test suite not found'); + // tslint:disable-next-line:no-non-null-assertion + const testSuite: TestsToRun = { testFile: [], testFolder: [], testFunction: [], testSuite: [testSuiteToRun!.testSuite] }; + const results = await testManager.runTest(CommandSource.ui, testSuite); + assert.equal(results.summary.errors, 0, 'Errors'); + assert.equal(results.summary.failures, 1, 'Failures'); + assert.equal(results.summary.passed, 1, 'Passed'); + assert.equal(results.summary.skipped, 1, 'skipped'); + }); + + test('Run Specific Test Function', async () => { + await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); + createTestManager(); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + const testFnToRun = tests.testFunctions.find(f => f.xmlClassName === 'test_root.Test_Root_test1'); + assert.ok(testFnToRun, 'Test function not found'); + // tslint:disable-next-line:no-non-null-assertion + const testFn: TestsToRun = { testFile: [], testFolder: [], testFunction: [testFnToRun!.testFunction], testSuite: [] }; + const results = await testManager.runTest(CommandSource.ui, testFn); + assert.equal(results.summary.errors, 0, 'Errors'); + assert.equal(results.summary.failures, 1, 'Failures'); + assert.equal(results.summary.passed, 0, 'Passed'); + assert.equal(results.summary.skipped, 0, 'skipped'); + }); +}); diff --git a/src/test/unittests/pytest.test.ts b/src/test/unittests/pytest.test.ts new file mode 100644 index 000000000000..1b016f4228cc --- /dev/null +++ b/src/test/unittests/pytest.test.ts @@ -0,0 +1,185 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { CommandSource } from '../../client/unittests/common/constants'; +import { TestCollectionStorageService } from '../../client/unittests/common/storageService'; +import { TestResultsService } from '../../client/unittests/common/testResultsService'; +import { TestsHelper } from '../../client/unittests/common/testUtils'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, TestFile, TestsToRun } from '../../client/unittests/common/types'; +import { TestResultDisplay } from '../../client/unittests/display/main'; +import * as pytest from '../../client/unittests/pytest/main'; +import { rootWorkspaceUri, updateSetting } from '../common'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; +import { MockOutputChannel } from './../mockClasses'; +import { MockDebugLauncher } from './mocks'; + +const UNITTEST_TEST_FILES_PATH = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'standard'); +const UNITTEST_SINGLE_TEST_FILE_PATH = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'single'); +const UNITTEST_TEST_FILES_PATH_WITH_CONFIGS = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'unitestsWithConfigs'); +const unitTestTestFilesCwdPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'cwd', 'src'); + +// tslint:disable-next-line:max-func-body-length +suite('Unit Tests (PyTest)', () => { + let rootDirectory = UNITTEST_TEST_FILES_PATH; + let testManager: pytest.TestManager; + let testResultDisplay: TestResultDisplay; + let outChannel: vscode.OutputChannel; + let storageService: ITestCollectionStorageService; + let resultsService: ITestResultsService; + let testsHelper: ITestsHelper; + const configTarget = IS_MULTI_ROOT_TEST ? vscode.ConfigurationTarget.WorkspaceFolder : vscode.ConfigurationTarget.Workspace; + suiteSetup(async () => { + await initialize(); + await updateSetting('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); + }); + setup(async () => { + rootDirectory = UNITTEST_TEST_FILES_PATH; + outChannel = new MockOutputChannel('Python Test Log'); + testResultDisplay = new TestResultDisplay(outChannel); + await initializeTest(); + }); + teardown(async () => { + outChannel.dispose(); + testManager.dispose(); + testResultDisplay.dispose(); + await updateSetting('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); + }); + function createTestManager(rootDir: string = rootDirectory) { + storageService = new TestCollectionStorageService(); + resultsService = new TestResultsService(); + testsHelper = new TestsHelper(); + testManager = new pytest.TestManager(rootDir, outChannel, storageService, resultsService, testsHelper, new MockDebugLauncher()); + } + + test('Discover Tests (single test file)', async () => { + storageService = new TestCollectionStorageService(); + resultsService = new TestResultsService(); + testsHelper = new TestsHelper(); + testManager = new pytest.TestManager(UNITTEST_SINGLE_TEST_FILE_PATH, outChannel, storageService, resultsService, testsHelper, new MockDebugLauncher()); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); + assert.equal(tests.testFiles.some(t => t.name === 'tests/test_one.py' && t.nameToRun === t.name), true, 'Test File not found'); + assert.equal(tests.testFiles.some(t => t.name === 'test_root.py' && t.nameToRun === t.name), true, 'Test File not found'); + }); + + test('Discover Tests (pattern = test_)', async () => { + await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); + createTestManager(); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + assert.equal(tests.testFiles.length, 6, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 29, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 8, 'Incorrect number of test suites'); + assert.equal(tests.testFiles.some(t => t.name === 'tests/test_unittest_one.py' && t.nameToRun === t.name), true, 'Test File not found'); + assert.equal(tests.testFiles.some(t => t.name === 'tests/test_unittest_two.py' && t.nameToRun === t.name), true, 'Test File not found'); + assert.equal(tests.testFiles.some(t => t.name === 'tests/unittest_three_test.py' && t.nameToRun === t.name), true, 'Test File not found'); + assert.equal(tests.testFiles.some(t => t.name === 'tests/test_pytest.py' && t.nameToRun === t.name), true, 'Test File not found'); + assert.equal(tests.testFiles.some(t => t.name === 'tests/test_another_pytest.py' && t.nameToRun === t.name), true, 'Test File not found'); + assert.equal(tests.testFiles.some(t => t.name === 'test_root.py' && t.nameToRun === t.name), true, 'Test File not found'); + }); + + test('Discover Tests (pattern = _test)', async () => { + await updateSetting('unitTest.pyTestArgs', ['-k=_test.py'], rootWorkspaceUri, configTarget); + createTestManager(); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); + assert.equal(tests.testFiles.some(t => t.name === 'tests/unittest_three_test.py' && t.nameToRun === t.name), true, 'Test File not found'); + }); + + test('Discover Tests (with config)', async () => { + await updateSetting('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); + rootDirectory = UNITTEST_TEST_FILES_PATH_WITH_CONFIGS; + createTestManager(); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 14, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 4, 'Incorrect number of test suites'); + assert.equal(tests.testFiles.some(t => t.name === 'other/test_unittest_one.py' && t.nameToRun === t.name), true, 'Test File not found'); + assert.equal(tests.testFiles.some(t => t.name === 'other/test_pytest.py' && t.nameToRun === t.name), true, 'Test File not found'); + }); + + test('Run Tests', async () => { + await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); + createTestManager(); + const results = await testManager.runTest(CommandSource.ui); + assert.equal(results.summary.errors, 0, 'Errors'); + assert.equal(results.summary.failures, 9, 'Failures'); + assert.equal(results.summary.passed, 17, 'Passed'); + assert.equal(results.summary.skipped, 3, 'skipped'); + }); + + test('Run Failed Tests', async () => { + await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); + createTestManager(); + let results = await testManager.runTest(CommandSource.ui); + assert.equal(results.summary.errors, 0, 'Errors'); + assert.equal(results.summary.failures, 9, 'Failures'); + assert.equal(results.summary.passed, 17, 'Passed'); + assert.equal(results.summary.skipped, 3, 'skipped'); + + results = await testManager.runTest(CommandSource.ui, undefined, true); + assert.equal(results.summary.errors, 0, 'Failed Errors'); + assert.equal(results.summary.failures, 9, 'Failed Failures'); + assert.equal(results.summary.passed, 0, 'Failed Passed'); + assert.equal(results.summary.skipped, 0, 'Failed skipped'); + }); + + test('Run Specific Test File', async () => { + await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); + createTestManager(); + await testManager.discoverTests(CommandSource.ui, true, true); + const testFile: TestFile = { + fullPath: path.join(rootDirectory, 'tests', 'test_another_pytest.py'), + name: 'tests/test_another_pytest.py', + nameToRun: 'tests/test_another_pytest.py', + xmlName: 'tests/test_another_pytest.py', + functions: [], + suites: [], + time: 0 + }; + const testFileToRun: TestsToRun = { testFile: [testFile], testFolder: [], testFunction: [], testSuite: [] }; + const results = await testManager.runTest(CommandSource.ui, testFileToRun); + assert.equal(results.summary.errors, 0, 'Errors'); + assert.equal(results.summary.failures, 1, 'Failures'); + assert.equal(results.summary.passed, 3, 'Passed'); + assert.equal(results.summary.skipped, 0, 'skipped'); + }); + + test('Run Specific Test Suite', async () => { + await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); + createTestManager(); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + const testSuite: TestsToRun = { testFile: [], testFolder: [], testFunction: [], testSuite: [tests.testSuites[0].testSuite] }; + const results = await testManager.runTest(CommandSource.ui, testSuite); + assert.equal(results.summary.errors, 0, 'Errors'); + assert.equal(results.summary.failures, 1, 'Failures'); + assert.equal(results.summary.passed, 1, 'Passed'); + assert.equal(results.summary.skipped, 1, 'skipped'); + }); + + test('Run Specific Test Function', async () => { + await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); + createTestManager(); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + const testFn: TestsToRun = { testFile: [], testFolder: [], testFunction: [tests.testFunctions[0].testFunction], testSuite: [] }; + const results = await testManager.runTest(CommandSource.ui, testFn); + assert.equal(results.summary.errors, 0, 'Errors'); + assert.equal(results.summary.failures, 1, 'Failures'); + assert.equal(results.summary.passed, 0, 'Passed'); + assert.equal(results.summary.skipped, 0, 'skipped'); + }); + + test('Setting cwd should return tests', async () => { + await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); + createTestManager(unitTestTestFilesCwdPath); + + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); + assert.equal(tests.testFolders.length, 1, 'Incorrect number of test folders'); + assert.equal(tests.testFunctions.length, 1, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); + }); +}); diff --git a/src/test/unittests/rediscover.test.ts b/src/test/unittests/rediscover.test.ts new file mode 100644 index 000000000000..ade46afcbfd5 --- /dev/null +++ b/src/test/unittests/rediscover.test.ts @@ -0,0 +1,108 @@ +import { assert } from 'chai'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { ConfigurationTarget } from 'vscode'; +import { BaseTestManager } from '../../client/unittests/common/baseTestManager'; +import { CommandSource } from '../../client/unittests/common/constants'; +import { TestCollectionStorageService } from '../../client/unittests/common/storageService'; +import { TestResultsService } from '../../client/unittests/common/testResultsService'; +import { TestsHelper } from '../../client/unittests/common/testUtils'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper } from '../../client/unittests/common/types'; +import { TestResultDisplay } from '../../client/unittests/display/main'; +import { TestManager as NosetestManager } from '../../client/unittests/nosetest/main'; +import { TestManager as PytestManager } from '../../client/unittests/pytest/main'; +import { TestManager as UnitTestManager } from '../../client/unittests/unittest/main'; +import { deleteDirectory, deleteFile, rootWorkspaceUri, updateSetting } from '../common'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; +import { MockOutputChannel } from './../mockClasses'; +import { MockDebugLauncher } from './mocks'; + +const testFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'debuggerTest'); +const testFile = path.join(testFilesPath, 'tests', 'test_debugger_two.py'); +const testFileWithFewTests = path.join(testFilesPath, 'tests', 'test_debugger_two.txt'); +const testFileWithMoreTests = path.join(testFilesPath, 'tests', 'test_debugger_two.updated.txt'); +const defaultUnitTestArgs = [ + '-v', + '-s', + '.', + '-p', + '*test*.py' +]; + +// tslint:disable-next-line:max-func-body-length +suite('Unit Tests re-discovery', () => { + let testManager: BaseTestManager; + let testResultDisplay: TestResultDisplay; + let outChannel: MockOutputChannel; + let storageService: ITestCollectionStorageService; + let resultsService: ITestResultsService; + let mockDebugLauncher: MockDebugLauncher; + let testsHelper: ITestsHelper; + const configTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; + suiteSetup(async () => { + await initialize(); + }); + setup(async () => { + outChannel = new MockOutputChannel('Python Test Log'); + testResultDisplay = new TestResultDisplay(outChannel); + await fs.copy(testFileWithFewTests, testFile, { overwrite: true }); + await deleteDirectory(path.join(testFilesPath, '.cache')); + await resetSettings(); + await initializeTest(); + }); + teardown(async () => { + await resetSettings(); + await fs.copy(testFileWithFewTests, testFile, { overwrite: true }); + await deleteFile(path.join(path.dirname(testFile), `${path.basename(testFile, '.py')}.pyc`)); + outChannel.dispose(); + if (testManager) { + testManager.dispose(); + } + testResultDisplay.dispose(); + }); + + async function resetSettings() { + await updateSetting('unitTest.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); + await updateSetting('unitTest.nosetestArgs', [], rootWorkspaceUri, configTarget); + await updateSetting('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); + } + + function createTestManagerDepedencies() { + storageService = new TestCollectionStorageService(); + resultsService = new TestResultsService(); + testsHelper = new TestsHelper(); + mockDebugLauncher = new MockDebugLauncher(); + } + + async function discoverUnitTests() { + let tests = await testManager.discoverTests(CommandSource.ui, true, true); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); + assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); + await deleteFile(path.join(path.dirname(testFile), `${path.basename(testFile, '.py')}.pyc`)); + await fs.copy(testFileWithMoreTests, testFile, { overwrite: true }); + tests = await testManager.discoverTests(CommandSource.ui, true, true); + assert.equal(tests.testFunctions.length, 4, 'Incorrect number of updated test functions'); + } + + test('Re-discover tests (unittest)', async () => { + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new UnitTestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await discoverUnitTests(); + }); + + test('Re-discover tests (pytest)', async () => { + await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new PytestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await discoverUnitTests(); + }); + + test('Re-discover tests (nosetest)', async () => { + await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new NosetestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await discoverUnitTests(); + }); +}); diff --git a/src/test/unittests/stoppingDiscoverAndTest.test.ts b/src/test/unittests/stoppingDiscoverAndTest.test.ts new file mode 100644 index 000000000000..eba6a358aa8d --- /dev/null +++ b/src/test/unittests/stoppingDiscoverAndTest.test.ts @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as path from 'path'; +import { Product } from '../../client/common/installer'; +import { CANCELLATION_REASON, CommandSource } from '../../client/unittests/common/constants'; +import { TestCollectionStorageService } from '../../client/unittests/common/storageService'; +import { TestResultsService } from '../../client/unittests/common/testResultsService'; +import { TestsHelper } from '../../client/unittests/common/testUtils'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper } from '../../client/unittests/common/types'; +import { TestResultDisplay } from '../../client/unittests/display/main'; +import { initialize, initializeTest } from '../initialize'; +import { MockOutputChannel } from '../mockClasses'; +import { MockTestManagerWithRunningTests } from './mocks'; + +use(chaiAsPromised); + +const testFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'debuggerTest'); +// tslint:disable-next-line:variable-name +const EmptyTests = { + summary: { + passed: 0, + failures: 0, + errors: 0, + skipped: 0 + }, + testFiles: [], + testFunctions: [], + testSuites: [], + testFolders: [], + rootTestFolders: [] +}; + +// tslint:disable-next-line:max-func-body-length +suite('Unit Tests Stopping Discovery and Runner', () => { + let testResultDisplay: TestResultDisplay; + let outChannel: MockOutputChannel; + let storageService: ITestCollectionStorageService; + let resultsService: ITestResultsService; + let testsHelper: ITestsHelper; + suiteSetup(initialize); + setup(async () => { + outChannel = new MockOutputChannel('Python Test Log'); + testResultDisplay = new TestResultDisplay(outChannel); + await initializeTest(); + }); + teardown(() => { + outChannel.dispose(); + testResultDisplay.dispose(); + }); + + function createTestManagerDepedencies() { + storageService = new TestCollectionStorageService(); + resultsService = new TestResultsService(); + testsHelper = new TestsHelper(); + } + + test('Running tests should not stop existing discovery', async () => { + createTestManagerDepedencies(); + const mockTestManager = new MockTestManagerWithRunningTests('unittest', Product.unittest, testFilesPath, outChannel, storageService, resultsService, testsHelper); + const discoveryPromise = mockTestManager.discoverTests(CommandSource.auto); + mockTestManager.discoveryDeferred.resolve(EmptyTests); + // tslint:disable-next-line:no-floating-promises + mockTestManager.runTest(CommandSource.ui); + + await expect(discoveryPromise).to.eventually.equal(EmptyTests); + }); + + test('Discovering tests should stop running tests', async () => { + createTestManagerDepedencies(); + const mockTestManager = new MockTestManagerWithRunningTests('unittest', Product.unittest, testFilesPath, outChannel, storageService, resultsService, testsHelper); + mockTestManager.discoveryDeferred.resolve(EmptyTests); + await mockTestManager.discoverTests(CommandSource.auto); + const runPromise = mockTestManager.runTest(CommandSource.ui); + // tslint:disable-next-line:no-string-based-set-timeout + await new Promise(resolve => setTimeout(resolve, 1000)); + + // User manually discovering tests will kill the existing test runner. + // tslint:disable-next-line:no-floating-promises + mockTestManager.discoverTests(CommandSource.ui, true, false, true); + await expect(runPromise).to.eventually.be.rejectedWith(CANCELLATION_REASON); + }); +}); diff --git a/src/test/unittests/unittest.test.ts b/src/test/unittests/unittest.test.ts new file mode 100644 index 000000000000..d333ed32fa9c --- /dev/null +++ b/src/test/unittests/unittest.test.ts @@ -0,0 +1,180 @@ +import * as assert from 'assert'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { ConfigurationTarget } from 'vscode'; +import { CommandSource } from '../../client/unittests/common/constants'; +import { TestCollectionStorageService } from '../../client/unittests/common/storageService'; +import { TestResultsService } from '../../client/unittests/common/testResultsService'; +import { TestsHelper } from '../../client/unittests/common/testUtils'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, TestsToRun } from '../../client/unittests/common/types'; +import { TestResultDisplay } from '../../client/unittests/display/main'; +import * as unittest from '../../client/unittests/unittest/main'; +import { rootWorkspaceUri, updateSetting } from '../common'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; +import { MockOutputChannel } from './../mockClasses'; +import { MockDebugLauncher } from './mocks'; + +const testFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles'); +const UNITTEST_TEST_FILES_PATH = path.join(testFilesPath, 'standard'); +const UNITTEST_SINGLE_TEST_FILE_PATH = path.join(testFilesPath, 'single'); +const unitTestTestFilesCwdPath = path.join(testFilesPath, 'cwd', 'src'); +const unitTestSpecificTestFilesPath = path.join(testFilesPath, 'specificTest'); +const defaultUnitTestArgs = [ + '-v', + '-s', + '.', + '-p', + '*test*.py' +]; + +// tslint:disable-next-line:max-func-body-length +suite('Unit Tests (unittest)', () => { + let testManager: unittest.TestManager; + let testResultDisplay: TestResultDisplay; + let outChannel: MockOutputChannel; + let storageService: ITestCollectionStorageService; + let resultsService: ITestResultsService; + let testsHelper: ITestsHelper; + const rootDirectory = UNITTEST_TEST_FILES_PATH; + const configTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; + suiteSetup(async () => { + await initialize(); + await updateSetting('unitTest.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); + }); + setup(async () => { + outChannel = new MockOutputChannel('Python Test Log'); + testResultDisplay = new TestResultDisplay(outChannel); + const cachePath = path.join(UNITTEST_TEST_FILES_PATH, '.cache'); + if (await fs.pathExists(cachePath)) { + await fs.remove(cachePath); + } + await initializeTest(); + }); + teardown(async () => { + outChannel.dispose(); + testManager.dispose(); + testResultDisplay.dispose(); + await updateSetting('unitTest.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); + }); + function createTestManager(rootDir: string = rootDirectory) { + storageService = new TestCollectionStorageService(); + resultsService = new TestResultsService(); + testsHelper = new TestsHelper(); + testManager = new unittest.TestManager(rootDir, outChannel, storageService, resultsService, testsHelper, new MockDebugLauncher()); + } + + test('Discover Tests (single test file)', async () => { + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); + storageService = new TestCollectionStorageService(); + resultsService = new TestResultsService(); + testsHelper = new TestsHelper(); + testManager = new unittest.TestManager(UNITTEST_SINGLE_TEST_FILE_PATH, outChannel, storageService, resultsService, testsHelper, new MockDebugLauncher()); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 3, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); + assert.equal(tests.testFiles.some(t => t.name === 'test_one.py' && t.nameToRun === 'Test_test1.test_A'), true, 'Test File not found'); + }); + + test('Discover Tests', async () => { + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); + createTestManager(); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 9, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 3, 'Incorrect number of test suites'); + assert.equal(tests.testFiles.some(t => t.name === 'test_unittest_one.py' && t.nameToRun === 'Test_test1.test_A'), true, 'Test File not found'); + assert.equal(tests.testFiles.some(t => t.name === 'test_unittest_two.py' && t.nameToRun === 'Test_test2.test_A2'), true, 'Test File not found'); + }); + + test('Discover Tests (pattern = *_test_*.py)', async () => { + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=*_test*.py'], rootWorkspaceUri, configTarget); + createTestManager(); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); + assert.equal(tests.testFiles.some(t => t.name === 'unittest_three_test.py' && t.nameToRun === 'Test_test3.test_A'), true, 'Test File not found'); + }); + + test('Run Tests', async () => { + await updateSetting('unitTest.unittestArgs', ['-v', '-s', './tests', '-p', 'test_unittest*.py'], rootWorkspaceUri, configTarget); + createTestManager(); + const results = await testManager.runTest(CommandSource.ui); + assert.equal(results.summary.errors, 1, 'Errors'); + assert.equal(results.summary.failures, 4, 'Failures'); + assert.equal(results.summary.passed, 3, 'Passed'); + assert.equal(results.summary.skipped, 1, 'skipped'); + }); + + test('Run Failed Tests', async () => { + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_unittest*.py'], rootWorkspaceUri, configTarget); + createTestManager(); + let results = await testManager.runTest(CommandSource.ui); + assert.equal(results.summary.errors, 1, 'Errors'); + assert.equal(results.summary.failures, 4, 'Failures'); + assert.equal(results.summary.passed, 3, 'Passed'); + assert.equal(results.summary.skipped, 1, 'skipped'); + + results = await testManager.runTest(CommandSource.ui, undefined, true); + assert.equal(results.summary.errors, 1, 'Failed Errors'); + assert.equal(results.summary.failures, 4, 'Failed Failures'); + assert.equal(results.summary.passed, 0, 'Failed Passed'); + assert.equal(results.summary.skipped, 0, 'Failed skipped'); + }); + + test('Run Specific Test File', async () => { + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_unittest*.py'], rootWorkspaceUri, configTarget); + createTestManager(unitTestSpecificTestFilesPath); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + + // tslint:disable-next-line:no-non-null-assertion + const testFileToTest = tests.testFiles.find(f => f.name === 'test_unittest_one.py')!; + const testFile: TestsToRun = { testFile: [testFileToTest], testFolder: [], testFunction: [], testSuite: [] }; + const results = await testManager.runTest(CommandSource.ui, testFile); + + assert.equal(results.summary.errors, 0, 'Errors'); + assert.equal(results.summary.failures, 1, 'Failures'); + assert.equal(results.summary.passed, 2, 'Passed'); + assert.equal(results.summary.skipped, 1, 'skipped'); + }); + + test('Run Specific Test Suite', async () => { + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_unittest*.py'], rootWorkspaceUri, configTarget); + createTestManager(unitTestSpecificTestFilesPath); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + + // tslint:disable-next-line:no-non-null-assertion + const testSuiteToTest = tests.testSuites.find(s => s.testSuite.name === 'Test_test_one_1')!.testSuite; + const testSuite: TestsToRun = { testFile: [], testFolder: [], testFunction: [], testSuite: [testSuiteToTest] }; + const results = await testManager.runTest(CommandSource.ui, testSuite); + + assert.equal(results.summary.errors, 0, 'Errors'); + assert.equal(results.summary.failures, 1, 'Failures'); + assert.equal(results.summary.passed, 2, 'Passed'); + assert.equal(results.summary.skipped, 1, 'skipped'); + }); + + test('Run Specific Test Function', async () => { + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_unittest*.py'], rootWorkspaceUri, configTarget); + createTestManager(); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + const testFn: TestsToRun = { testFile: [], testFolder: [], testFunction: [tests.testFunctions[0].testFunction], testSuite: [] }; + const results = await testManager.runTest(CommandSource.ui, testFn); + assert.equal(results.summary.errors, 0, 'Errors'); + assert.equal(results.summary.failures, 1, 'Failures'); + assert.equal(results.summary.passed, 0, 'Passed'); + assert.equal(results.summary.skipped, 0, 'skipped'); + }); + + test('Setting cwd should return tests', async () => { + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); + createTestManager(unitTestTestFilesCwdPath); + + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); + assert.equal(tests.testFolders.length, 1, 'Incorrect number of test folders'); + assert.equal(tests.testFunctions.length, 1, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); + }); +}); diff --git a/src/test/workspaceSymbols/common.ts b/src/test/workspaceSymbols/common.ts new file mode 100644 index 000000000000..527b852ab6ad --- /dev/null +++ b/src/test/workspaceSymbols/common.ts @@ -0,0 +1,8 @@ +import { ConfigurationTarget, Uri, workspace } from 'vscode'; +import { PythonSettings } from '../../client/common/configSettings'; + +export async function enableDisableWorkspaceSymbols(resource: Uri, enabled: boolean, configTarget: ConfigurationTarget) { + const settings = workspace.getConfiguration('python', resource); + await settings.update('workspaceSymbols.enabled', enabled, configTarget); + PythonSettings.dispose(); +} diff --git a/src/test/workspaceSymbols/multiroot.test.ts b/src/test/workspaceSymbols/multiroot.test.ts new file mode 100644 index 000000000000..9d4de7940274 --- /dev/null +++ b/src/test/workspaceSymbols/multiroot.test.ts @@ -0,0 +1,65 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import { CancellationTokenSource, ConfigurationTarget, Uri } from 'vscode'; +import { Generator } from '../../client/workspaceSymbols/generator'; +import { WorkspaceSymbolProvider } from '../../client/workspaceSymbols/provider'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; +import { MockOutputChannel } from '../mockClasses'; +import { updateSetting } from './../common'; + +const multirootPath = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRootWkspc'); + +suite('Multiroot Workspace Symbols', () => { + suiteSetup(function () { + if (!IS_MULTI_ROOT_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + return initialize(); + }); + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(async () => { + await closeActiveWindows(); + await updateSetting('workspaceSymbols.enabled', false, Uri.file(path.join(multirootPath, 'parent', 'child')), ConfigurationTarget.WorkspaceFolder); + await updateSetting('workspaceSymbols.enabled', false, Uri.file(path.join(multirootPath, 'workspace2')), ConfigurationTarget.WorkspaceFolder); + }); + + test('symbols should be returned when enabeld and vice versa', async () => { + const childWorkspaceUri = Uri.file(path.join(multirootPath, 'parent', 'child')); + const outputChannel = new MockOutputChannel('Output'); + + await updateSetting('workspaceSymbols.enabled', false, childWorkspaceUri, ConfigurationTarget.WorkspaceFolder); + + let generator = new Generator(childWorkspaceUri, outputChannel); + let provider = new WorkspaceSymbolProvider([generator], outputChannel); + let symbols = await provider.provideWorkspaceSymbols('', new CancellationTokenSource().token); + assert.equal(symbols.length, 0, 'Symbols returned even when workspace symbols are turned off'); + generator.dispose(); + + await updateSetting('workspaceSymbols.enabled', true, childWorkspaceUri, ConfigurationTarget.WorkspaceFolder); + + generator = new Generator(childWorkspaceUri, outputChannel); + provider = new WorkspaceSymbolProvider([generator], outputChannel); + symbols = await provider.provideWorkspaceSymbols('', new CancellationTokenSource().token); + assert.notEqual(symbols.length, 0, 'Symbols should be returned when workspace symbols are turned on'); + }); + test('symbols should be filtered correctly', async () => { + const childWorkspaceUri = Uri.file(path.join(multirootPath, 'parent', 'child')); + const workspace2Uri = Uri.file(path.join(multirootPath, 'workspace2')); + const outputChannel = new MockOutputChannel('Output'); + + await updateSetting('workspaceSymbols.enabled', true, childWorkspaceUri, ConfigurationTarget.WorkspaceFolder); + await updateSetting('workspaceSymbols.enabled', true, workspace2Uri, ConfigurationTarget.WorkspaceFolder); + + const generators = [ + new Generator(childWorkspaceUri, outputChannel), + new Generator(workspace2Uri, outputChannel)]; + const provider = new WorkspaceSymbolProvider(generators, outputChannel); + const symbols = await provider.provideWorkspaceSymbols('meth1Of', new CancellationTokenSource().token); + + assert.equal(symbols.length, 2, 'Incorrect number of symbols returned'); + assert.notEqual(symbols.findIndex(sym => sym.location.uri.fsPath.endsWith('childFile.py')), -1, 'File with symbol not found in child workspace folder'); + assert.notEqual(symbols.findIndex(sym => sym.location.uri.fsPath.endsWith('workspace2File.py')), -1, 'File with symbol not found in child workspace folder'); + }); +}); diff --git a/src/test/workspaceSymbols/standard.test.ts b/src/test/workspaceSymbols/standard.test.ts new file mode 100644 index 000000000000..b2859b0fc428 --- /dev/null +++ b/src/test/workspaceSymbols/standard.test.ts @@ -0,0 +1,74 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import { CancellationTokenSource, ConfigurationTarget, Uri } from 'vscode'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; +import { Generator } from '../../client/workspaceSymbols/generator'; +import { MockOutputChannel } from '../mockClasses'; +import { WorkspaceSymbolProvider } from '../../client/workspaceSymbols/provider'; +import { updateSetting } from './../common'; +import { PythonSettings } from '../../client/common/configSettings'; + +const workspaceUri = Uri.file(path.join(__dirname, '..', '..', '..', 'src', 'test')); +const configUpdateTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; + +suite('Workspace Symbols', () => { + suiteSetup(() => initialize()); + suiteTeardown(() => closeActiveWindows()); + setup(() => initializeTest()); + teardown(async () => { + await closeActiveWindows(); + await updateSetting('workspaceSymbols.enabled', false, workspaceUri, configUpdateTarget); + }); + + test(`symbols should be returned when enabeld and vice versa`, async () => { + const outputChannel = new MockOutputChannel('Output'); + await updateSetting('workspaceSymbols.enabled', false, workspaceUri, configUpdateTarget); + + // The workspace will be in the output test folder + // So lets modify the settings so it sees the source test folder + let settings = PythonSettings.getInstance(workspaceUri); + settings.workspaceSymbols.tagFilePath = path.join(workspaceUri.fsPath, '.vscode', 'tags') + + let generator = new Generator(workspaceUri, outputChannel); + let provider = new WorkspaceSymbolProvider([generator], outputChannel); + let symbols = await provider.provideWorkspaceSymbols('', new CancellationTokenSource().token); + assert.equal(symbols.length, 0, 'Symbols returned even when workspace symbols are turned off'); + generator.dispose(); + + await updateSetting('workspaceSymbols.enabled', true, workspaceUri, configUpdateTarget); + + // The workspace will be in the output test folder + // So lets modify the settings so it sees the source test folder + settings = PythonSettings.getInstance(workspaceUri); + settings.workspaceSymbols.tagFilePath = path.join(workspaceUri.fsPath, '.vscode', 'tags') + + generator = new Generator(workspaceUri, outputChannel); + provider = new WorkspaceSymbolProvider([generator], outputChannel); + symbols = await provider.provideWorkspaceSymbols('', new CancellationTokenSource().token); + assert.notEqual(symbols.length, 0, 'Symbols should be returned when workspace symbols are turned on'); + }); + test(`symbols should be filtered correctly`, async () => { + const outputChannel = new MockOutputChannel('Output'); + + await updateSetting('workspaceSymbols.enabled', true, workspaceUri, configUpdateTarget); + + // The workspace will be in the output test folder + // So lets modify the settings so it sees the source test folder + const settings = PythonSettings.getInstance(workspaceUri); + settings.workspaceSymbols.tagFilePath = path.join(workspaceUri.fsPath, '.vscode', 'tags') + + const generators = [new Generator(workspaceUri, outputChannel)]; + const provider = new WorkspaceSymbolProvider(generators, outputChannel); + const symbols = await provider.provideWorkspaceSymbols('meth1Of', new CancellationTokenSource().token); + + assert.equal(symbols.length >= 2, true, 'Incorrect number of symbols returned'); + assert.notEqual(symbols.findIndex(sym => sym.location.uri.fsPath.endsWith('childFile.py')), -1, 'File with symbol not found in child workspace folder'); + assert.notEqual(symbols.findIndex(sym => sym.location.uri.fsPath.endsWith('workspace2File.py')), -1, 'File with symbol not found in child workspace folder'); + + const symbolsForMeth = await provider.provideWorkspaceSymbols('meth', new CancellationTokenSource().token); + assert.equal(symbolsForMeth.length >= 10, true, 'Incorrect number of symbols returned'); + assert.notEqual(symbolsForMeth.findIndex(sym => sym.location.uri.fsPath.endsWith('childFile.py')), -1, 'Symbols not returned for childFile.py'); + assert.notEqual(symbolsForMeth.findIndex(sym => sym.location.uri.fsPath.endsWith('workspace2File.py')), -1, 'Symbols not returned for workspace2File.py'); + assert.notEqual(symbolsForMeth.findIndex(sym => sym.location.uri.fsPath.endsWith('file.py')), -1, 'Symbols not returned for file.py'); + }); +}); diff --git a/src/testMultiRootWkspc/disableLinters/.vscode/tags b/src/testMultiRootWkspc/disableLinters/.vscode/tags new file mode 100644 index 000000000000..4739b4629cfb --- /dev/null +++ b/src/testMultiRootWkspc/disableLinters/.vscode/tags @@ -0,0 +1,19 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +Foo ..\\file.py /^class Foo(object):$/;" kind:class line:5 +__init__ ..\\file.py /^ def __init__(self):$/;" kind:member line:8 +__revision__ ..\\file.py /^__revision__ = None$/;" kind:variable line:3 +file.py ..\\file.py 1;" kind:file line:1 +meth1 ..\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth2 ..\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 ..\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 ..\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 ..\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 ..\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 ..\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 ..\\file.py /^ def meth8(self):$/;" kind:member line:80 diff --git a/src/testMultiRootWkspc/disableLinters/file.py b/src/testMultiRootWkspc/disableLinters/file.py new file mode 100644 index 000000000000..439f899e9e22 --- /dev/null +++ b/src/testMultiRootWkspc/disableLinters/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print self + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print self\ + + "foo" + + def meth3(self): + """test one line disabling""" + # no error + print self.bla # pylint: disable=no-member + # error + print self.blop + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + print self.blop + # pylint: enable=no-member + # error + print self.blip + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + if self.blop: + # pylint: enable=no-member + # error + print self.blip + else: + # no error + print self.blip + # no error + print self.blip + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + try: + # pylint: enable=no-member + # error + print self.blip + except UndefinedName: # pylint: disable=undefined-variable + # no error + print self.blip + # no error + print self.blip + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print self.blip + else: + # error + print self.blip + # error + print self.blip + + + def meth8(self): + """test late disabling""" + # error + print self.blip + # pylint: disable=no-member + # no error + print self.bla + print self.blop diff --git a/src/testMultiRootWkspc/multi.code-workspace b/src/testMultiRootWkspc/multi.code-workspace new file mode 100644 index 000000000000..6aca26f07b90 --- /dev/null +++ b/src/testMultiRootWkspc/multi.code-workspace @@ -0,0 +1,42 @@ +{ + "folders": [ + { + "path": "workspace1" + }, + { + "path": "workspace2" + }, + { + "path": "workspace3" + }, + { + "path": "parent\\child" + }, + { + "path": "disableLinters" + }, + { + "path": "../test" + } + ], + "settings": { + "python.linting.flake8Enabled": false, + "python.linting.mypyEnabled": false, + "python.linting.pydocstyleEnabled": false, + "python.linting.pylamaEnabled": false, + "python.linting.pylintEnabled": true, + "python.linting.pep8Enabled": false, + "python.linting.prospectorEnabled": false, + "python.workspaceSymbols.enabled": false, + "python.formatting.formatOnSave": false, + "python.formatting.provider": "yapf", + "python.sortImports.args": [ + "-sp", + "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/sorting/withconfig" + ], + "python.linting.lintOnSave": false, + "python.linting.lintOnTextChange": false, + "python.linting.enabled": true, + "python.pythonPath": "python" + } +} diff --git a/src/testMultiRootWkspc/parent/child/.vscode/settings.json b/src/testMultiRootWkspc/parent/child/.vscode/settings.json new file mode 100644 index 000000000000..c404e94945a9 --- /dev/null +++ b/src/testMultiRootWkspc/parent/child/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.workspaceSymbols.enabled": false +} \ No newline at end of file diff --git a/src/testMultiRootWkspc/parent/child/.vscode/tags b/src/testMultiRootWkspc/parent/child/.vscode/tags new file mode 100644 index 000000000000..e6791c755b0f --- /dev/null +++ b/src/testMultiRootWkspc/parent/child/.vscode/tags @@ -0,0 +1,24 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +Child2Class ..\\childFile.py /^class Child2Class(object):$/;" kind:class line:5 +Foo ..\\file.py /^class Foo(object):$/;" kind:class line:5 +__init__ ..\\childFile.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\file.py /^ def __init__(self):$/;" kind:member line:8 +__revision__ ..\\childFile.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\file.py /^__revision__ = None$/;" kind:variable line:3 +childFile.py ..\\childFile.py 1;" kind:file line:1 +file.py ..\\file.py 1;" kind:file line:1 +meth1 ..\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1OfChild ..\\childFile.py /^ def meth1OfChild(self, arg):$/;" kind:member line:11 +meth2 ..\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 ..\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 ..\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 ..\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 ..\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 ..\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 ..\\file.py /^ def meth8(self):$/;" kind:member line:80 diff --git a/src/testMultiRootWkspc/parent/child/childFile.py b/src/testMultiRootWkspc/parent/child/childFile.py new file mode 100644 index 000000000000..31d6fc7b4a18 --- /dev/null +++ b/src/testMultiRootWkspc/parent/child/childFile.py @@ -0,0 +1,13 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Child2Class(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1OfChild(self, arg): + """this issues a message""" + print (self) diff --git a/src/testMultiRootWkspc/parent/child/file.py b/src/testMultiRootWkspc/parent/child/file.py new file mode 100644 index 000000000000..439f899e9e22 --- /dev/null +++ b/src/testMultiRootWkspc/parent/child/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print self + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print self\ + + "foo" + + def meth3(self): + """test one line disabling""" + # no error + print self.bla # pylint: disable=no-member + # error + print self.blop + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + print self.blop + # pylint: enable=no-member + # error + print self.blip + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + if self.blop: + # pylint: enable=no-member + # error + print self.blip + else: + # no error + print self.blip + # no error + print self.blip + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + try: + # pylint: enable=no-member + # error + print self.blip + except UndefinedName: # pylint: disable=undefined-variable + # no error + print self.blip + # no error + print self.blip + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print self.blip + else: + # error + print self.blip + # error + print self.blip + + + def meth8(self): + """test late disabling""" + # error + print self.blip + # pylint: disable=no-member + # no error + print self.bla + print self.blop diff --git a/src/testMultiRootWkspc/workspace1/.vscode/settings.json b/src/testMultiRootWkspc/workspace1/.vscode/settings.json new file mode 100644 index 000000000000..f4d89e3bc0e4 --- /dev/null +++ b/src/testMultiRootWkspc/workspace1/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "python.linting.enabled": false, + "python.linting.flake8Enabled": true, + "python.linting.pylintEnabled": false +} diff --git a/src/testMultiRootWkspc/workspace1/.vscode/tags b/src/testMultiRootWkspc/workspace1/.vscode/tags new file mode 100644 index 000000000000..4739b4629cfb --- /dev/null +++ b/src/testMultiRootWkspc/workspace1/.vscode/tags @@ -0,0 +1,19 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +Foo ..\\file.py /^class Foo(object):$/;" kind:class line:5 +__init__ ..\\file.py /^ def __init__(self):$/;" kind:member line:8 +__revision__ ..\\file.py /^__revision__ = None$/;" kind:variable line:3 +file.py ..\\file.py 1;" kind:file line:1 +meth1 ..\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth2 ..\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 ..\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 ..\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 ..\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 ..\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 ..\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 ..\\file.py /^ def meth8(self):$/;" kind:member line:80 diff --git a/src/testMultiRootWkspc/workspace1/file.py b/src/testMultiRootWkspc/workspace1/file.py new file mode 100644 index 000000000000..439f899e9e22 --- /dev/null +++ b/src/testMultiRootWkspc/workspace1/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print self + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print self\ + + "foo" + + def meth3(self): + """test one line disabling""" + # no error + print self.bla # pylint: disable=no-member + # error + print self.blop + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + print self.blop + # pylint: enable=no-member + # error + print self.blip + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + if self.blop: + # pylint: enable=no-member + # error + print self.blip + else: + # no error + print self.blip + # no error + print self.blip + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + try: + # pylint: enable=no-member + # error + print self.blip + except UndefinedName: # pylint: disable=undefined-variable + # no error + print self.blip + # no error + print self.blip + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print self.blip + else: + # error + print self.blip + # error + print self.blip + + + def meth8(self): + """test late disabling""" + # error + print self.blip + # pylint: disable=no-member + # no error + print self.bla + print self.blop diff --git a/src/testMultiRootWkspc/workspace2/.vscode/settings.json b/src/testMultiRootWkspc/workspace2/.vscode/settings.json new file mode 100644 index 000000000000..750d19764931 --- /dev/null +++ b/src/testMultiRootWkspc/workspace2/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python.workspaceSymbols.tagFilePath": "${workspaceRoot}/workspace2.tags.file", + "python.workspaceSymbols.enabled": false +} diff --git a/src/testMultiRootWkspc/workspace2/file.py b/src/testMultiRootWkspc/workspace2/file.py new file mode 100644 index 000000000000..439f899e9e22 --- /dev/null +++ b/src/testMultiRootWkspc/workspace2/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print self + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print self\ + + "foo" + + def meth3(self): + """test one line disabling""" + # no error + print self.bla # pylint: disable=no-member + # error + print self.blop + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + print self.blop + # pylint: enable=no-member + # error + print self.blip + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + if self.blop: + # pylint: enable=no-member + # error + print self.blip + else: + # no error + print self.blip + # no error + print self.blip + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + try: + # pylint: enable=no-member + # error + print self.blip + except UndefinedName: # pylint: disable=undefined-variable + # no error + print self.blip + # no error + print self.blip + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print self.blip + else: + # error + print self.blip + # error + print self.blip + + + def meth8(self): + """test late disabling""" + # error + print self.blip + # pylint: disable=no-member + # no error + print self.bla + print self.blop diff --git a/src/testMultiRootWkspc/workspace2/workspace2.tags.file b/src/testMultiRootWkspc/workspace2/workspace2.tags.file new file mode 100644 index 000000000000..375785e2a94e --- /dev/null +++ b/src/testMultiRootWkspc/workspace2/workspace2.tags.file @@ -0,0 +1,24 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +Foo C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^class Foo(object):$/;" kind:class line:5 +Workspace2Class C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\workspace2File.py /^class Workspace2Class(object):$/;" kind:class line:5 +__init__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\workspace2File.py /^ def __init__(self):$/;" kind:member line:8 +__revision__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\workspace2File.py /^__revision__ = None$/;" kind:variable line:3 +file.py C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py 1;" kind:file line:1 +meth1 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1OfWorkspace2 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\workspace2File.py /^ def meth1OfWorkspace2(self, arg):$/;" kind:member line:11 +meth2 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^ def meth8(self):$/;" kind:member line:80 +workspace2File.py C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\workspace2File.py 1;" kind:file line:1 diff --git a/src/testMultiRootWkspc/workspace2/workspace2File.py b/src/testMultiRootWkspc/workspace2/workspace2File.py new file mode 100644 index 000000000000..61aa87c55fed --- /dev/null +++ b/src/testMultiRootWkspc/workspace2/workspace2File.py @@ -0,0 +1,13 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Workspace2Class(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1OfWorkspace2(self, arg): + """this issues a message""" + print (self) diff --git a/src/testMultiRootWkspc/workspace3/.vscode/settings.json b/src/testMultiRootWkspc/workspace3/.vscode/settings.json new file mode 100644 index 000000000000..8779a0c08efe --- /dev/null +++ b/src/testMultiRootWkspc/workspace3/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.workspaceSymbols.tagFilePath": "${workspaceRoot}/workspace3.tags.file" +} diff --git a/src/testMultiRootWkspc/workspace3/file.py b/src/testMultiRootWkspc/workspace3/file.py new file mode 100644 index 000000000000..439f899e9e22 --- /dev/null +++ b/src/testMultiRootWkspc/workspace3/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print self + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print self\ + + "foo" + + def meth3(self): + """test one line disabling""" + # no error + print self.bla # pylint: disable=no-member + # error + print self.blop + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + print self.blop + # pylint: enable=no-member + # error + print self.blip + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + if self.blop: + # pylint: enable=no-member + # error + print self.blip + else: + # no error + print self.blip + # no error + print self.blip + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + try: + # pylint: enable=no-member + # error + print self.blip + except UndefinedName: # pylint: disable=undefined-variable + # no error + print self.blip + # no error + print self.blip + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print self.blip + else: + # error + print self.blip + # error + print self.blip + + + def meth8(self): + """test late disabling""" + # error + print self.blip + # pylint: disable=no-member + # no error + print self.bla + print self.blop diff --git a/src/testMultiRootWkspc/workspace3/workspace3.tags.file b/src/testMultiRootWkspc/workspace3/workspace3.tags.file new file mode 100644 index 000000000000..3a65841e2aff --- /dev/null +++ b/src/testMultiRootWkspc/workspace3/workspace3.tags.file @@ -0,0 +1,19 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +Foo C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^class Foo(object):$/;" kind:class line:5 +__init__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^ def __init__(self):$/;" kind:member line:8 +__revision__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^__revision__ = None$/;" kind:variable line:3 +file.py C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py 1;" kind:file line:1 +meth1 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth2 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^ def meth8(self):$/;" kind:member line:80 diff --git a/syntaxes/pip-requirements.tmLanguage.json b/syntaxes/pip-requirements.tmLanguage.json new file mode 100644 index 000000000000..869efbe7834a --- /dev/null +++ b/syntaxes/pip-requirements.tmLanguage.json @@ -0,0 +1,77 @@ +{ + "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", + "name": "pip requirements", + "scopeName": "source.pip-requirements", + "patterns": [ + { + "explanation": "Line continuation", + "match": "\\s*\\\\s*$", + "name": "constant.character.escape" + }, + { + "match": "#.*", + "name": "comment.line.number-sign" + }, + { + "begin": "'", + "end": "'", + "name": "string.quoted.single" + }, + { + "begin": "\"", + "end": "\"", + "name": "string.quoted.double" + }, + { + "match": "/?(\\S+/)+\\S*", + "name": "string.path" + }, + { + "explanation": "project name", + "match": "^\\s*([A-Za-z0-9][A-Za-z0-9._-]*[A-Za-z0-9]|[A-Za-z0-9])", + "captures": { + "1": { + "name": "entity.name.class" + } + } + }, + { + "explanation": "extras", + "match": "\\[([^\\]]+)\\]", + "captures": { + "1": { + "name": "entity.name.tag" + } + } + }, + { + "explanation": "version specification", + "match": "(<|<=|!=|==|>=|>|~=|===)\\s*([\\w.*+!-]+)", + "captures": { + "1": { + "name": "keyword.operator.comparison" + }, + "2": { + "name": "constant.numeric" + } + } + }, + { + "explanation": "environment markers", + "match": ";\\s*(python_version|python_full_version|os_name|sys_platform|platform_release|platform_system|platform_version|platform_machine|platform_python_implementation|implementation_name|implementation_version|extra)\\s*(<|<=|!=|==|>=|>|~=|===)", + "captures":{ + "1": { + "name": "entity.name.selector" + }, + "2": { + "name": "keyword.operator.comparison" + } + } + }, + { + "explanation": "command-line options (e.g. `--no-links` or `-c`)", + "match": "-[^\\s=]+", + "name": "entity.other.attribute-name" + } + ] +} diff --git a/tsconfig.json b/tsconfig.json index 85f92c623300..c6bac46ad4fa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,18 @@ { "compilerOptions": { - "module": "commonjs", + "module": "commonjs", "target": "es6", "outDir": "out", "lib": [ "es6" ], "sourceMap": true, - "rootDir": "src" + "rootDir": "src", + "experimentalDecorators": true + // TODO: enable to ensure all code complies with strict coding standards + // , "noUnusedLocals": true + // , "noUnusedParameters": false + // , "strict": true }, "exclude": [ "node_modules", @@ -15,7 +20,6 @@ "src/server/node_modules", "src/client/node_modules", "src/server/src/typings", - "src/client/src/typings", - "docs" + "src/client/src/typings" ] -} \ No newline at end of file +} diff --git a/tsfmt.json b/tsfmt.json new file mode 100644 index 000000000000..fffcf07c1998 --- /dev/null +++ b/tsfmt.json @@ -0,0 +1,17 @@ +{ + "tabSize": 4, + "indentSize": 4, + "newLineCharacter": "\n", + "convertTabsToSpaces": false, + "insertSpaceAfterCommaDelimiter": true, + "insertSpaceAfterSemicolonInForStatements": true, + "insertSpaceBeforeAndAfterBinaryOperators": true, + "insertSpaceAfterKeywordsInControlFlowStatements": true, + "insertSpaceAfterFunctionKeywordForAnonymousFunctions": true, + "insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": false, + "insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": false, + "insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": false, + "insertSpaceBeforeFunctionParenthesis": false, + "placeOpenBraceOnNewLineForFunctions": false, + "placeOpenBraceOnNewLineForControlBlocks": false +} diff --git a/tslint.json b/tslint.json index aec10615173d..7ff727e68b34 100644 --- a/tslint.json +++ b/tslint.json @@ -1,13 +1,47 @@ { + "extends": [ + "tslint-eslint-rules", + "tslint-microsoft-contrib" + ], "rules": { "no-unused-expression": true, - "no-unreachable": true, "no-duplicate-variable": true, - "no-duplicate-key": true, "no-unused-variable": true, "curly": true, "class-name": true, - "semicolon": true, - "triple-equals": true + "semicolon": [ + true + ], + "triple-equals": true, + "no-relative-imports": false, + "max-line-length": false, + "typedef": false, + "no-string-throw": true, + "missing-jsdoc": false, + "one-line": [ + true, + "check-catch", + "check-finally", + "check-else" + ], + "no-parameter-properties": false, + "no-reserved-keywords": false, + "newline-before-return": false, + "export-name": false, + "align": false, + "linebreak-style": false, + "strict-boolean-expressions": [ + true, + "allow-null-union", + "allow-undefined-union", + "allow-string", + "allow-number" + ], + "await-promise": [ + true, + "Thenable", + "PromiseLike" + ], + "completed-docs": false } -} \ No newline at end of file +} diff --git a/typings.json b/typings.json deleted file mode 100644 index 9f6c5ea9c4a9..000000000000 --- a/typings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "globalDependencies": { - "fs-extra": "registry:dt/fs-extra#0.0.0+20160319124112", - "xml2js": "registry:dt/xml2js#0.0.0+20160317120654" - } -} diff --git a/typings/globals/fs-extra/index.d.ts b/typings/globals/fs-extra/index.d.ts deleted file mode 100644 index 5cccd2da96ab..000000000000 --- a/typings/globals/fs-extra/index.d.ts +++ /dev/null @@ -1,212 +0,0 @@ -// Generated by typings -// Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/92c93bde1b77bc0a3159d161a7483d54b5f9326d/fs-extra/fs-extra.d.ts -declare module "fs-extra" { - import stream = require("stream"); - - export interface Stats { - isFile(): boolean; - isDirectory(): boolean; - isBlockDevice(): boolean; - isCharacterDevice(): boolean; - isSymbolicLink(): boolean; - isFIFO(): boolean; - isSocket(): boolean; - dev: number; - ino: number; - mode: number; - nlink: number; - uid: number; - gid: number; - rdev: number; - size: number; - blksize: number; - blocks: number; - atime: Date; - mtime: Date; - ctime: Date; - } - - export interface FSWatcher { - close(): void; - } - - export class ReadStream extends stream.Readable { } - export class WriteStream extends stream.Writable { } - - //extended methods - export function copy(src: string, dest: string, callback?: (err: Error) => void): void; - export function copy(src: string, dest: string, filter: CopyFilter, callback?: (err: Error) => void): void; - export function copy(src: string, dest: string, options: CopyOptions, callback?: (err: Error) => void): void; - - export function copySync(src: string, dest: string): void; - export function copySync(src: string, dest: string, filter: CopyFilter): void; - export function copySync(src: string, dest: string, options: CopyOptions): void; - - export function createFile(file: string, callback?: (err: Error) => void): void; - export function createFileSync(file: string): void; - - export function mkdirs(dir: string, callback?: (err: Error) => void): void; - export function mkdirp(dir: string, callback?: (err: Error) => void): void; - export function mkdirs(dir: string, options?: MkdirOptions, callback?: (err: Error) => void): void; - export function mkdirp(dir: string, options?: MkdirOptions, callback?: (err: Error) => void): void; - export function mkdirsSync(dir: string, options?: MkdirOptions): void; - export function mkdirpSync(dir: string, options?: MkdirOptions): void; - - export function outputFile(file: string, data: any, callback?: (err: Error) => void): void; - export function outputFileSync(file: string, data: any): void; - - export function outputJson(file: string, data: any, callback?: (err: Error) => void): void; - export function outputJSON(file: string, data: any, callback?: (err: Error) => void): void; - export function outputJsonSync(file: string, data: any): void; - export function outputJSONSync(file: string, data: any): void; - - export function readJson(file: string, callback: (err: Error, jsonObject: any) => void): void; - export function readJson(file: string, options: OpenOptions, callback: (err: Error, jsonObject: any) => void): void; - export function readJSON(file: string, callback: (err: Error, jsonObject: any) => void): void; - export function readJSON(file: string, options: OpenOptions, callback: (err: Error, jsonObject: any) => void): void; - - export function readJsonSync(file: string, options?: OpenOptions): any; - export function readJSONSync(file: string, options?: OpenOptions): any; - - export function remove(dir: string, callback?: (err: Error) => void): void; - export function removeSync(dir: string): void; - // export function delete(dir: string, callback?: (err: Error) => void): void; - // export function deleteSync(dir: string): void; - - export function writeJson(file: string, object: any, callback?: (err: Error) => void): void; - export function writeJson(file: string, object: any, options?: OpenOptions, callback?: (err: Error) => void): void; - export function writeJSON(file: string, object: any, callback?: (err: Error) => void): void; - export function writeJSON(file: string, object: any, options?: OpenOptions, callback?: (err: Error) => void): void; - - export function writeJsonSync(file: string, object: any, options?: OpenOptions): void; - export function writeJSONSync(file: string, object: any, options?: OpenOptions): void; - - export function rename(oldPath: string, newPath: string, callback?: (err: Error) => void): void; - export function renameSync(oldPath: string, newPath: string): void; - export function truncate(fd: number, len: number, callback?: (err: Error) => void): void; - export function truncateSync(fd: number, len: number): void; - export function chown(path: string, uid: number, gid: number, callback?: (err: Error) => void): void; - export function chownSync(path: string, uid: number, gid: number): void; - export function fchown(fd: number, uid: number, gid: number, callback?: (err: Error) => void): void; - export function fchownSync(fd: number, uid: number, gid: number): void; - export function lchown(path: string, uid: number, gid: number, callback?: (err: Error) => void): void; - export function lchownSync(path: string, uid: number, gid: number): void; - export function chmod(path: string, mode: number, callback?: (err: Error) => void): void; - export function chmod(path: string, mode: string, callback?: (err: Error) => void): void; - export function chmodSync(path: string, mode: number): void; - export function chmodSync(path: string, mode: string): void; - export function fchmod(fd: number, mode: number, callback?: (err: Error) => void): void; - export function fchmod(fd: number, mode: string, callback?: (err: Error) => void): void; - export function fchmodSync(fd: number, mode: number): void; - export function fchmodSync(fd: number, mode: string): void; - export function lchmod(path: string, mode: string, callback?: (err: Error) => void): void; - export function lchmod(path: string, mode: number, callback?: (err: Error) => void): void; - export function lchmodSync(path: string, mode: number): void; - export function lchmodSync(path: string, mode: string): void; - export function stat(path: string, callback?: (err: Error, stats: Stats) => void): void; - export function lstat(path: string, callback?: (err: Error, stats: Stats) => void): void; - export function fstat(fd: number, callback?: (err: Error, stats: Stats) => void): void; - export function statSync(path: string): Stats; - export function lstatSync(path: string): Stats; - export function fstatSync(fd: number): Stats; - export function link(srcpath: string, dstpath: string, callback?: (err: Error) => void): void; - export function linkSync(srcpath: string, dstpath: string): void; - export function symlink(srcpath: string, dstpath: string, type?: string, callback?: (err: Error) => void): void; - export function symlinkSync(srcpath: string, dstpath: string, type?: string): void; - export function readlink(path: string, callback?: (err: Error, linkString: string) => void): void; - export function realpath(path: string, callback?: (err: Error, resolvedPath: string) => void): void; - export function realpath(path: string, cache: string, callback: (err: Error, resolvedPath: string) => void): void; - export function realpathSync(path: string, cache?: boolean): string; - export function unlink(path: string, callback?: (err: Error) => void): void; - export function unlinkSync(path: string): void; - export function rmdir(path: string, callback?: (err: Error) => void): void; - export function rmdirSync(path: string): void; - export function mkdir(path: string, mode?: number, callback?: (err: Error) => void): void; - export function mkdir(path: string, mode?: string, callback?: (err: Error) => void): void; - export function mkdirSync(path: string, mode?: number): void; - export function mkdirSync(path: string, mode?: string): void; - export function readdir(path: string, callback?: (err: Error, files: string[]) => void ): void; - export function readdirSync(path: string): string[]; - export function close(fd: number, callback?: (err: Error) => void): void; - export function closeSync(fd: number): void; - export function open(path: string, flags: string, mode?: string, callback?: (err: Error, fs: number) => void): void; - export function openSync(path: string, flags: string, mode?: string): number; - export function utimes(path: string, atime: number, mtime: number, callback?: (err: Error) => void): void; - export function utimesSync(path: string, atime: number, mtime: number): void; - export function futimes(fd: number, atime: number, mtime: number, callback?: (err: Error) => void): void; - export function futimesSync(fd: number, atime: number, mtime: number): void; - export function fsync(fd: number, callback?: (err: Error) => void): void; - export function fsyncSync(fd: number): void; - export function write(fd: number, buffer: NodeBuffer, offset: number, length: number, position: number, callback?: (err: Error, written: number, buffer: NodeBuffer) => void): void; - export function writeSync(fd: number, buffer: NodeBuffer, offset: number, length: number, position: number): number; - export function read(fd: number, buffer: NodeBuffer, offset: number, length: number, position: number, callback?: (err: Error, bytesRead: number, buffer: NodeBuffer) => void ): void; - export function readSync(fd: number, buffer: NodeBuffer, offset: number, length: number, position: number): number; - export function readFile(filename: string, encoding: string, callback: (err: Error, data: string) => void ): void; - export function readFile(filename: string, options: OpenOptions, callback: (err: Error, data: string) => void ): void; - export function readFile(filename: string, callback: (err: Error, data: NodeBuffer) => void ): void; - export function readFileSync(filename: string): NodeBuffer; - export function readFileSync(filename: string, encoding: string): string; - export function readFileSync(filename: string, options: OpenOptions): string; - export function writeFile(filename: string, data: any, encoding?: string, callback?: (err: Error) => void): void; - export function writeFile(filename: string, data: any, options?: OpenOptions, callback?: (err: Error) => void): void; - export function writeFileSync(filename: string, data: any, encoding?: string): void; - export function writeFileSync(filename: string, data: any, option?: OpenOptions): void; - export function appendFile(filename: string, data: any, encoding?: string, callback?: (err: Error) => void): void; - export function appendFile(filename: string, data: any,option?: OpenOptions, callback?: (err: Error) => void): void; - export function appendFileSync(filename: string, data: any, encoding?: string): void; - export function appendFileSync(filename: string, data: any, option?: OpenOptions): void; - export function watchFile(filename: string, listener: { curr: Stats; prev: Stats; }): void; - export function watchFile(filename: string, options: { persistent?: boolean; interval?: number; }, listener: { curr: Stats; prev: Stats; }): void; - export function unwatchFile(filename: string, listener?: Stats): void; - export function watch(filename: string, options?: { persistent?: boolean; }, listener?: (event: string, filename: string) => any): FSWatcher; - export function exists(path: string, callback?: (exists: boolean) => void ): void; - export function existsSync(path: string): boolean; - export function ensureDir(path: string, cb: (err: Error) => void): void; - export function ensureDirSync(path: string): void; - export function ensureFile(path: string, cb: (err: Error) => void): void; - export function ensureFileSync(path: string): void; - export function ensureLink(path: string, cb: (err: Error) => void): void; - export function ensureLinkSync(path: string): void; - export function ensureSymlink(path: string, cb: (err: Error) => void): void; - export function ensureSymlinkSync(path: string): void; - export function emptyDir(path: string, callback?: (err: Error) => void): void; - export function emptyDirSync(path: string): boolean; - - export interface CopyFilterFunction { - (src: string): boolean - } - - export type CopyFilter = CopyFilterFunction | RegExp; - - export interface CopyOptions { - clobber?: boolean - preserveTimestamps?: boolean - filter?: CopyFilter - } - - export interface OpenOptions { - encoding?: string; - flag?: string; - } - - export interface MkdirOptions { - fs?: any; - mode?: number; - } - - export interface ReadStreamOptions { - flags?: string; - encoding?: string; - fd?: number; - mode?: number; - bufferSize?: number; - } - export interface WriteStreamOptions { - flags?: string; - encoding?: string; - string?: string; - } - export function createReadStream(path: string, options?: ReadStreamOptions): ReadStream; - export function createWriteStream(path: string, options?: WriteStreamOptions): WriteStream; - export function createOutputStream(path: string, options?: WriteStreamOptions): WriteStream; -} \ No newline at end of file diff --git a/typings/globals/fs-extra/typings.json b/typings/globals/fs-extra/typings.json deleted file mode 100644 index 0af0431be597..000000000000 --- a/typings/globals/fs-extra/typings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "resolution": "main", - "tree": { - "src": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/92c93bde1b77bc0a3159d161a7483d54b5f9326d/fs-extra/fs-extra.d.ts", - "raw": "registry:dt/fs-extra#0.0.0+20160319124112", - "typings": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/92c93bde1b77bc0a3159d161a7483d54b5f9326d/fs-extra/fs-extra.d.ts" - } -} diff --git a/typings/globals/xml2js/index.d.ts b/typings/globals/xml2js/index.d.ts deleted file mode 100644 index a3feea4d5ab8..000000000000 --- a/typings/globals/xml2js/index.d.ts +++ /dev/null @@ -1,96 +0,0 @@ -// Generated by typings -// Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7de6c3dd94feaeb21f20054b9f30d5dabc5efabd/xml2js/xml2js.d.ts -declare module 'xml2js' { - - export = xml2js; - - namespace xml2js { - function parseString(xml: string, callback: (err: any, result: any) => void): void; - function parseString(xml: string, options: Options, callback: (err: any, result: any) => void): void; - - var defaults: { - '0.1': Options; - '0.2': OptionsV2; - } - - class Builder { - constructor(options?: BuilderOptions); - buildObject(rootObj: any): string; - } - - class Parser { - constructor(options?: Options); - processAsync(): any; - assignOrPush(obj: any, key: string, newValue: any): any; - reset(): any; - parseString(str: string , cb?: Function): void; - } - - interface RenderOptions { - indent?: string; - newline?: string; - pretty?: boolean; - } - - interface XMLDeclarationOptions { - encoding?: string; - standalone?: boolean; - version?: string; - } - - interface BuilderOptions { - doctype?: any; - headless?: boolean; - indent?: string; - newline?: string; - pretty?: boolean; - renderOpts?: RenderOptions; - rootName?: string; - xmldec?: XMLDeclarationOptions; - } - - interface Options { - async?: boolean; - attrkey?: string; - attrNameProcessors?: [(name: string) => string]; - attrValueProcessors?: [(name: string) => string]; - charkey?: string; - charsAsChildren?: boolean; - childkey?: string; - emptyTag?: any; - explicitArray?: boolean; - explicitCharkey?: boolean; - explicitChildren?: boolean; - explicitRoot?: boolean; - ignoreAttrs?: boolean; - mergeAttrs?: boolean; - normalize?: boolean; - normalizeTags?: boolean; - strict?: boolean; - tagNameProcessors?: [(name: string) => string]; - trim?: boolean; - validator?: Function; - valueProcessors?: [(name: string) => string]; - xmlns?: boolean; - } - - interface OptionsV2 extends Options { - preserveChildrenOrder?: boolean; - rootName?: string; - xmldec?: { - version: string; - encoding?: string; - standalone?: boolean; - }; - doctype?: any; - renderOpts?: { - pretty?: boolean; - indent?: string; - newline?: string; - }; - headless?: boolean; - chunkSize?: number; - cdata?: boolean; - } - } -} diff --git a/typings/globals/xml2js/typings.json b/typings/globals/xml2js/typings.json deleted file mode 100644 index 4f70efbc7f27..000000000000 --- a/typings/globals/xml2js/typings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "resolution": "main", - "tree": { - "src": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7de6c3dd94feaeb21f20054b9f30d5dabc5efabd/xml2js/xml2js.d.ts", - "raw": "registry:dt/xml2js#0.0.0+20160317120654", - "typings": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7de6c3dd94feaeb21f20054b9f30d5dabc5efabd/xml2js/xml2js.d.ts" - } -} diff --git a/typings/index.d.ts b/typings/index.d.ts deleted file mode 100644 index acb34d71937e..000000000000 --- a/typings/index.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/// -/// \ No newline at end of file diff --git a/typings/node.d.ts b/typings/node.d.ts deleted file mode 100644 index 457fd135e6f6..000000000000 --- a/typings/node.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// \ No newline at end of file diff --git a/typings/vscode-typings.d.ts b/typings/vscode-typings.d.ts deleted file mode 100644 index e9d47fd5a066..000000000000 --- a/typings/vscode-typings.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// \ No newline at end of file