├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── actions │ ├── build-vsix │ │ └── action.yml │ └── lint │ │ └── action.yml ├── release.yml └── workflows │ ├── codeql-analysis.yml │ ├── issue-labels.yml │ ├── pr-check.yml │ ├── pr-labels.yml │ └── push-check.yml ├── .gitignore ├── .prettierrc.js ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE.txt ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── build ├── azure-pipeline.pre-release.yml ├── azure-pipeline.stable.yml ├── test_update_ext_version.py └── update_ext_version.py ├── bundled └── tool │ ├── __init__.py │ ├── _debug_server.py │ ├── lsp_jsonrpc.py │ ├── lsp_runner.py │ ├── lsp_server.py │ └── lsp_utils.py ├── dev-requirements.in ├── dev-requirements.txt ├── icon.png ├── noxfile.py ├── package-lock.json ├── package.json ├── package.nls.json ├── pyproject.toml ├── requirements.in ├── requirements.txt ├── runtime.txt ├── src ├── common │ ├── constants.ts │ ├── logging.ts │ ├── python.ts │ ├── server.ts │ ├── settings.ts │ ├── setup.ts │ ├── status.ts │ ├── utilities.ts │ └── vscodeapi.ts ├── extension.ts └── test │ ├── python_tests │ ├── __init__.py │ ├── lsp_test_client │ │ ├── __init__.py │ │ ├── constants.py │ │ ├── defaults.py │ │ ├── session.py │ │ └── utils.py │ ├── requirements.in │ ├── requirements.txt │ ├── test_code_actions.py │ ├── test_data │ │ ├── sample1 │ │ │ └── sample.py │ │ └── sample2 │ │ │ └── sample.py │ ├── test_extra_paths.py │ ├── test_linting.py │ └── test_path_specialization.py │ └── ts_tests │ ├── index.ts │ ├── runTest.ts │ └── tests │ └── common │ ├── settings.unit.test.ts │ └── utilities.unit.test.ts ├── tsconfig.json └── webpack.config.js /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/devcontainers/python:3.9 2 | 3 | RUN python -m pip install --upgrade pip 4 | 5 | COPY dev-requirements.txt ./ 6 | RUN python -m pip install -r dev-requirements.txt \ 7 | && rm dev-requirements.txt 8 | 9 | COPY requirements.txt ./ 10 | RUN python -m pip install -r requirements.txt \ 11 | && rm requirements.txt 12 | 13 | COPY src/test/python_tests/requirements.txt . 14 | RUN python -m pip install -r requirements.txt \ 15 | && rm requirements.txt 16 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.222.0/containers/python-3-miniconda 3 | { 4 | "name": "Python Environment", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | "context": ".." 8 | }, 9 | "features": { 10 | "ghcr.io/devcontainers/features/node:1": "none" 11 | }, 12 | "customizations": { 13 | "vscode": { 14 | "extensions": [ 15 | "amodio.tsl-problem-matcher", 16 | "bungcip.better-toml", 17 | "cschleiden.vscode-github-actions", 18 | "dbaeumer.vscode-eslint", 19 | "esbenp.prettier-vscode", 20 | "editorconfig.editorconfig", 21 | "GitHub.copilot", 22 | "github.vscode-pull-request-github", 23 | "ms-azuretools.vscode-docker", 24 | "ms-python.python", 25 | "ms-python.vscode-pylance", 26 | "ms-python.pylint", 27 | "ms-python.isort", 28 | "ms-python.flake8", 29 | "ms-python.black-formatter", 30 | "ms-vsliveshare.vsliveshare", 31 | "ms-vscode-remote.remote-containers" 32 | ], 33 | "settings": { 34 | "python.defaultInterpreterPath": "/usr/local/bin/python" 35 | } 36 | } 37 | }, 38 | "postCreateCommand": "nox --session setup" 39 | } 40 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": ["@typescript-eslint"], 9 | "rules": { 10 | "@typescript-eslint/naming-convention": "warn", 11 | "@typescript-eslint/semi": "warn", 12 | "curly": "warn", 13 | "eqeqeq": "warn", 14 | "no-throw-literal": "warn", 15 | "semi": "off" 16 | }, 17 | "ignorePatterns": ["out", "dist", "**/*.d.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create an issue for a bug in the Pylint tools extension for VS Code. 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 15 | 16 | ## Diagnostic Data 17 | - Python version (& distribution if applicable, e.g., Anaconda): 18 | - Type of virtual environment used (e.g., conda, venv, virtualenv, etc.): 19 | - Operating system (and version): 20 | - Version of tool extension you are using: 21 | 22 | ## Behaviour 23 | ### Expected Behavior 24 | 25 | 26 | ### Actual Behavior 27 | 28 | 29 | ## Reproduction Steps: 30 | 31 | 32 | ## Logs: 33 | 40 |
41 | Click here for detailed logs 42 | 43 |
44 | 45 | ## Outcome When Attempting Debugging Steps: 46 | 52 | Did running it from the command line work? 53 | 54 | ## Extra Details 55 | 62 | -------------------------------------------------------------------------------- /.github/actions/build-vsix/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Build VSIX' 2 | description: "Build the extension's VSIX" 3 | 4 | inputs: 5 | node_version: 6 | description: 'Version of Node to install' 7 | required: true 8 | 9 | runs: 10 | using: 'composite' 11 | steps: 12 | - name: Install Node 13 | uses: actions/setup-node@v4 14 | with: 15 | node-version: ${{ inputs.node_version }} 16 | cache: 'npm' 17 | 18 | # Minimum supported version is Python 3.9 19 | - name: Use Python 3.9 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: '3.9' 23 | 24 | - name: Pip cache 25 | uses: actions/cache@v4 26 | with: 27 | path: ~/.cache/pip 28 | key: ${{ runner.os }}-pip-build-vsix-${{ hashFiles('**/requirements.txt') }} 29 | restore-keys: | 30 | ${{ runner.os }}-pip-build-vsix- 31 | 32 | - name: Upgrade Pip 33 | run: python -m pip install -U pip 34 | shell: bash 35 | 36 | # For faster/better builds of sdists. 37 | - name: Install build pre-requisite 38 | run: python -m pip install wheel 39 | shell: bash 40 | 41 | - name: Install nox 42 | run: python -m pip install nox 43 | shell: bash 44 | 45 | - name: Run npm ci 46 | run: npm ci --prefer-offline 47 | shell: bash 48 | 49 | - name: Install bundled python libraries 50 | run: python -m nox --session install_bundled_libs 51 | shell: bash 52 | 53 | # Use the GITHUB_RUN_ID environment variable to update the build number. 54 | # GITHUB_RUN_ID is a unique number for each run within a repository. 55 | # This number does not change if you re-run the workflow run. 56 | - name: Update extension build number 57 | run: python -m nox --session update_build_number -- $GITHUB_RUN_ID 58 | shell: bash 59 | 60 | - name: Build VSIX 61 | run: npm run vsce-package 62 | shell: bash 63 | 64 | - name: Upload VSIX 65 | uses: actions/upload-artifact@v4 66 | with: 67 | name: linter-package 68 | path: | 69 | **/*.vsix 70 | if-no-files-found: error 71 | retention-days: 7 72 | -------------------------------------------------------------------------------- /.github/actions/lint/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Lint' 2 | description: 'Lint TypeScript and Python code' 3 | 4 | inputs: 5 | node_version: 6 | description: 'Version of Node to install' 7 | required: true 8 | 9 | runs: 10 | using: 'composite' 11 | steps: 12 | - name: Install Node 13 | uses: actions/setup-node@v4 14 | with: 15 | node-version: ${{ inputs.node_version }} 16 | cache: 'npm' 17 | 18 | - name: Install Node dependencies 19 | run: npm ci 20 | shell: bash 21 | 22 | - name: Lint TypeScript code 23 | run: npm run lint 24 | shell: bash 25 | 26 | - name: Check TypeScript format 27 | run: npm run format-check 28 | shell: bash 29 | 30 | - name: Install Python 31 | uses: actions/setup-python@v5 32 | with: 33 | python-version: '3.9' 34 | 35 | - name: Pip cache 36 | uses: actions/cache@v4 37 | with: 38 | path: ~/.cache/pip 39 | key: ${{ runner.os }}-pip-lint-${{ hashFiles('**/requirements.txt') }} 40 | restore-keys: | 41 | ${{ runner.os }}-pip-lint- 42 | 43 | - name: Upgrade Pip 44 | run: python -m pip install -U pip 45 | shell: bash 46 | 47 | - name: Install wheel and nox 48 | run: python -m pip install wheel nox 49 | shell: bash 50 | 51 | # This will install libraries to a target directory. 52 | - name: Install bundled python libraries 53 | run: python -m nox --session install_bundled_libs 54 | shell: bash 55 | 56 | - name: Check linting and formatting 57 | run: python -m nox --session lint 58 | shell: bash 59 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - 'no-changelog' 5 | 6 | categories: 7 | - title: Enhancements 8 | labels: 9 | - 'feature-request' 10 | 11 | - title: Bug Fixes 12 | labels: 13 | - 'bug' 14 | 15 | - title: Code Health 16 | labels: 17 | - 'debt' 18 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: 'CodeQL' 13 | 14 | on: 15 | push: 16 | branches: [main] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [main] 20 | schedule: 21 | - cron: '15 16 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: ['javascript', 'python'] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v3 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v3 71 | -------------------------------------------------------------------------------- /.github/workflows/issue-labels.yml: -------------------------------------------------------------------------------- 1 | name: Issue labels 2 | 3 | on: 4 | issues: 5 | types: [opened, reopened] 6 | 7 | permissions: 8 | issues: write 9 | 10 | jobs: 11 | # From https://github.com/marketplace/actions/github-script#apply-a-label-to-an-issue. 12 | add-triage-label: 13 | name: "Add 'triage-needed'" 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/github-script@v7 17 | with: 18 | github-token: ${{ secrets.GITHUB_TOKEN }} 19 | script: | 20 | const result = await github.rest.issues.listLabelsOnIssue({ 21 | owner: context.repo.owner, 22 | repo: context.repo.repo, 23 | issue_number: context.issue.number, 24 | }) 25 | const labels = result.data.map((label) => label.name) 26 | const hasNeeds = labels.some((label) => label.startsWith('needs')) 27 | 28 | if (!hasNeeds) { 29 | console.log('This issue is not labeled with a "needs __" label, add the "triage-needed" label.') 30 | 31 | github.rest.issues.addLabels({ 32 | owner: context.repo.owner, 33 | repo: context.repo.repo, 34 | issue_number: context.issue.number, 35 | labels: ['triage-needed'] 36 | }) 37 | } else { 38 | console.log('This issue already has a "needs __" label, do not add the "triage-needed" label.') 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/pr-check.yml: -------------------------------------------------------------------------------- 1 | name: PR Validation 2 | 3 | on: 4 | pull_request: 5 | 6 | env: 7 | NODE_VERSION: 20.19.0 8 | TEST_RESULTS_DIRECTORY: . 9 | # Force a path with spaces and unicode chars to test extension works in these scenarios 10 | special-working-directory: './🐍 🐛' 11 | special-working-directory-relative: '🐍 🐛' 12 | 13 | jobs: 14 | build-vsix: 15 | name: Create VSIX 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v3 20 | 21 | - name: Build VSIX 22 | uses: ./.github/actions/build-vsix 23 | with: 24 | node_version: ${{ env.NODE_VERSION}} 25 | 26 | lint: 27 | name: Lint 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v3 32 | 33 | - name: Lint 34 | uses: ./.github/actions/lint 35 | with: 36 | node_version: ${{ env.NODE_VERSION }} 37 | 38 | tests: 39 | name: Tests 40 | runs-on: ${{ matrix.os }} 41 | defaults: 42 | run: 43 | working-directory: ${{ env.special-working-directory }} 44 | strategy: 45 | fail-fast: false 46 | matrix: 47 | os: [ubuntu-latest, windows-latest] 48 | python: ['3.9', '3.10', '3.11', '3.12', '3.13'] 49 | 50 | steps: 51 | - name: Checkout 52 | uses: actions/checkout@v3 53 | with: 54 | path: ${{ env.special-working-directory-relative }} 55 | 56 | # Install bundled libs using 3.9 even though you test it on other versions. 57 | - name: Use Python 3.9 58 | uses: actions/setup-python@v5 59 | with: 60 | python-version: '3.9' 61 | 62 | - name: Update pip, install wheel and nox 63 | run: python -m pip install -U pip wheel nox 64 | shell: bash 65 | 66 | # This will install libraries to a target directory. 67 | - name: Install bundled python libraries 68 | run: python -m nox --session install_bundled_libs 69 | shell: bash 70 | 71 | # Now that the bundle is installed to target using python 3.9 72 | # switch back the python we want to test with 73 | - name: Use Python ${{ matrix.python }} 74 | uses: actions/setup-python@v5 75 | with: 76 | python-version: ${{ matrix.python }} 77 | 78 | # The new python may not have nox so install it again 79 | - name: Update pip, install wheel and nox (again) 80 | run: python -m pip install -U pip wheel nox 81 | shell: bash 82 | 83 | - name: Run tests 84 | run: python -m nox --session tests 85 | shell: bash 86 | 87 | - name: Validate README.md 88 | run: python -m nox --session validate_readme 89 | shell: bash 90 | 91 | ts-tests: 92 | name: TypeScript Tests 93 | runs-on: ${{ matrix.os }} 94 | strategy: 95 | fail-fast: false 96 | matrix: 97 | os: [ubuntu-latest, windows-latest] 98 | 99 | steps: 100 | - name: Checkout 101 | uses: actions/checkout@v3 102 | 103 | - name: Install Node 104 | uses: actions/setup-node@v3 105 | with: 106 | node-version: ${{ env.NODE_VERSION }} 107 | cache: 'npm' 108 | cache-dependency-path: ./package-lock.json 109 | 110 | - name: Install Node dependencies 111 | run: npm ci 112 | shell: bash 113 | 114 | - name: Compile TS tests 115 | run: npm run pretest 116 | shell: bash 117 | 118 | - name: Run TS tests 119 | uses: GabrielBB/xvfb-action@v1.6 120 | with: 121 | run: npm run tests 122 | -------------------------------------------------------------------------------- /.github/workflows/pr-labels.yml: -------------------------------------------------------------------------------- 1 | name: 'PR labels' 2 | on: 3 | pull_request: 4 | types: 5 | - 'opened' 6 | - 'reopened' 7 | - 'labeled' 8 | - 'unlabeled' 9 | - 'synchronize' 10 | 11 | jobs: 12 | add-pr-label: 13 | name: 'Ensure Required Labels' 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: 'PR impact specified' 17 | uses: mheap/github-action-required-labels@v5 18 | with: 19 | mode: exactly 20 | count: 1 21 | labels: 'bug, debt, feature-request, no-changelog' 22 | -------------------------------------------------------------------------------- /.github/workflows/push-check.yml: -------------------------------------------------------------------------------- 1 | name: Push Validation 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | - 'release' 8 | - 'release/*' 9 | - 'release-*' 10 | 11 | env: 12 | NODE_VERSION: 20.19.0 13 | TEST_RESULTS_DIRECTORY: . 14 | # Force a path with spaces and unicode chars to test extension works in these scenarios 15 | special-working-directory: './🐍 🐛' 16 | special-working-directory-relative: '🐍 🐛' 17 | 18 | jobs: 19 | build-vsix: 20 | name: Create VSIX 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v3 25 | 26 | - name: Build VSIX 27 | uses: ./.github/actions/build-vsix 28 | with: 29 | node_version: ${{ env.NODE_VERSION}} 30 | 31 | lint: 32 | name: Lint 33 | runs-on: ubuntu-latest 34 | steps: 35 | - name: Checkout 36 | uses: actions/checkout@v3 37 | 38 | - name: Lint 39 | uses: ./.github/actions/lint 40 | with: 41 | node_version: ${{ env.NODE_VERSION }} 42 | 43 | tests: 44 | name: Tests 45 | runs-on: ${{ matrix.os }} 46 | defaults: 47 | run: 48 | working-directory: ${{ env.special-working-directory }} 49 | strategy: 50 | fail-fast: false 51 | matrix: 52 | os: [ubuntu-latest, windows-latest] 53 | python: ['3.9', '3.10', '3.11', '3.12', '3.13'] 54 | 55 | steps: 56 | - name: Checkout 57 | uses: actions/checkout@v3 58 | with: 59 | path: ${{ env.special-working-directory-relative }} 60 | 61 | # Install bundled libs using 3.9 even though you test it on other versions. 62 | - name: Use Python 3.9 63 | uses: actions/setup-python@v5 64 | with: 65 | python-version: '3.9' 66 | 67 | - name: Update pip, install wheel and nox 68 | run: python -m pip install -U pip wheel nox 69 | shell: bash 70 | 71 | # This will install libraries to a target directory. 72 | - name: Install bundled python libraries 73 | run: python -m nox --session install_bundled_libs 74 | shell: bash 75 | 76 | # Now that the bundle is installed to target using python 3.9 77 | # switch back the python we want to test with 78 | - name: Use Python ${{ matrix.python }} 79 | uses: actions/setup-python@v5 80 | with: 81 | python-version: ${{ matrix.python }} 82 | 83 | # The new python may not have nox so install it again 84 | - name: Update pip, install wheel and nox (again) 85 | run: python -m pip install -U pip wheel nox 86 | shell: bash 87 | 88 | - name: Run tests 89 | run: python -m nox --session tests 90 | shell: bash 91 | 92 | - name: Validate README.md 93 | run: python -m nox --session validate_readme 94 | shell: bash 95 | 96 | ts-tests: 97 | name: TypeScript Tests 98 | runs-on: ${{ matrix.os }} 99 | strategy: 100 | fail-fast: false 101 | matrix: 102 | os: [ubuntu-latest, windows-latest] 103 | 104 | steps: 105 | - name: Checkout 106 | uses: actions/checkout@v3 107 | 108 | - name: Install Node 109 | uses: actions/setup-node@v3 110 | with: 111 | node-version: ${{ env.NODE_VERSION }} 112 | cache: 'npm' 113 | cache-dependency-path: ./package-lock.json 114 | 115 | - name: Install Node dependencies 116 | run: npm ci 117 | shell: bash 118 | 119 | - name: Compile TS tests 120 | run: npm run pretest 121 | shell: bash 122 | 123 | - name: Run TS tests 124 | uses: GabrielBB/xvfb-action@v1.6 125 | with: 126 | run: npm run tests 127 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | .venv/ 7 | .nox/ 8 | bundled/libs/ 9 | **/.mypy_cache 10 | **/__pycache__ 11 | **/.pytest_cache 12 | **/.vs 13 | src/test/python_tests/test_data/sample1/*.py 14 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | printWidth: 120, 4 | tabWidth: 4, 5 | endOfLine: 'auto', 6 | trailingComma: 'all', 7 | overrides: [ 8 | { 9 | files: ['*.yml', '*.yaml'], 10 | options: { 11 | tabWidth: 2 12 | } 13 | } 14 | ] 15 | }; 16 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": ["dbaeumer.vscode-eslint", "amodio.tsl-problem-matcher"] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Debug Extension Only", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 13 | "outFiles": ["${workspaceFolder}/dist/**/*.js"], 14 | "preLaunchTask": "npm: watch", 15 | "presentation": { 16 | "hidden": false, 17 | "group": "", 18 | "order": 2 19 | } 20 | }, 21 | { 22 | "name": "Python Attach", 23 | "type": "python", 24 | "request": "attach", 25 | "processId": "${command:pickProcess}", 26 | "justMyCode": false, 27 | "presentation": { 28 | "hidden": false, 29 | "group": "", 30 | "order": 3 31 | } 32 | }, 33 | { 34 | "name": "TS Unit Tests", 35 | "type": "extensionHost", 36 | "request": "launch", 37 | "args": [ 38 | "--extensionDevelopmentPath=${workspaceFolder}", 39 | "--extensionTestsPath=${workspaceFolder}/out/test/ts_tests/index" 40 | ], 41 | "outFiles": ["${workspaceFolder}/out/**/*.js", "${workspaceFolder}/dist/**/*.js"], 42 | "preLaunchTask": "tasks: watch-tests" 43 | }, 44 | { 45 | "name": "Python Config for test explorer (hidden)", 46 | "type": "python", 47 | "request": "launch", 48 | "console": "integratedTerminal", 49 | "purpose": ["debug-test"], 50 | "justMyCode": true, 51 | "presentation": { 52 | "hidden": true, 53 | "group": "", 54 | "order": 4 55 | } 56 | }, 57 | { 58 | "name": "Debug Extension (hidden)", 59 | "type": "extensionHost", 60 | "request": "launch", 61 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 62 | "outFiles": ["${workspaceFolder}/dist/**/*.js"], 63 | "env": { 64 | "USE_DEBUGPY": "True" 65 | }, 66 | "presentation": { 67 | "hidden": true, 68 | "group": "", 69 | "order": 4 70 | } 71 | }, 72 | { 73 | "name": "Python debug server (hidden)", 74 | "type": "python", 75 | "request": "attach", 76 | "listen": { "host": "localhost", "port": 5678 }, 77 | "justMyCode": true, 78 | "presentation": { 79 | "hidden": true, 80 | "group": "", 81 | "order": 4 82 | } 83 | } 84 | ], 85 | "compounds": [ 86 | { 87 | "name": "Debug Extension and Python", 88 | "configurations": ["Python debug server (hidden)", "Debug Extension (hidden)"], 89 | "stopAll": true, 90 | "preLaunchTask": "npm: watch", 91 | "presentation": { 92 | "hidden": false, 93 | "group": "", 94 | "order": 1 95 | } 96 | } 97 | ] 98 | } 99 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false, // set this to true to hide the "out" folder with the compiled JS files 5 | "dist": false // set this to true to hide the "dist" folder with the compiled JS files 6 | }, 7 | "search.exclude": { 8 | "out": true, // set this to false to include "out" folder in search results 9 | "dist": true // set this to false to include "dist" folder in search results 10 | }, 11 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 12 | "typescript.tsc.autoDetect": "off", 13 | "editor.formatOnSave": true, 14 | "editor.formatOnPaste": true, 15 | "files.trimTrailingWhitespace": true, 16 | "files.autoSave": "onFocusChange", 17 | "git.autofetch": true, 18 | "githubPullRequests.pullBranch": "always", 19 | "[jsonc]": { 20 | "editor.defaultFormatter": "esbenp.prettier-vscode" 21 | }, 22 | "[python]": { 23 | "editor.defaultFormatter": "ms-python.black-formatter" 24 | }, 25 | "python.testing.pytestArgs": ["src/test/python_tests"], 26 | "python.testing.unittestEnabled": false, 27 | "python.testing.pytestEnabled": true, 28 | "python.testing.cwd": "${workspaceFolder}", 29 | "pylint.importStrategy": "fromEnvironment", 30 | "pylint.args": ["--rcfile=pyproject.toml"], 31 | "black-formatter.importStrategy": "fromEnvironment", 32 | "black-formatter.args": ["--config=pyproject.toml"], 33 | "flake8.importStrategy": "fromEnvironment", 34 | "flake8.args": ["--toml-config=pyproject.toml"], 35 | "isort.importStrategy": "fromEnvironment", 36 | "isort.args": ["--settings-path=pyproject.toml"] 37 | } 38 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": ["$ts-webpack-watch", "$tslint-webpack-watch"], 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never", 13 | "group": "watchers" 14 | }, 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | } 19 | }, 20 | { 21 | "type": "npm", 22 | "script": "watch-tests", 23 | "problemMatcher": "$tsc-watch", 24 | "isBackground": true, 25 | "presentation": { 26 | "reveal": "never", 27 | "group": "watchers" 28 | }, 29 | "group": "build" 30 | }, 31 | { 32 | "label": "tasks: watch-tests", 33 | "dependsOn": ["npm: watch", "npm: watch-tests"], 34 | "problemMatcher": [] 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/** 4 | node_modules/** 5 | src/** 6 | .gitignore 7 | .yarnrc 8 | webpack.config.js 9 | vsc-extension-quickstart.md 10 | **/tsconfig.json 11 | **/.eslintrc.json 12 | **/*.ts 13 | .venv/** 14 | .nox/** 15 | .github/ 16 | **/__pycache__/** 17 | **/.pyc 18 | bundled/libs/bin/** 19 | noxfile.py 20 | .pytest_cache/** 21 | .pylintrc 22 | **/requirements.txt 23 | **/requirements.in 24 | **/tool/_debug_server.py 25 | build/** 26 | .mypy_cache/** 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | **Please see https://github.com/microsoft/vscode-pylint/releases for the latest release notes.** 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please see [our wiki](https://github.com/microsoft/vscode-pylint/wiki) on how to contribute to this project. 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pylint extension for Visual Studio Code 2 | 3 | A Visual Studio Code extension with support for the Pylint linter. This extension ships with `pylint=3.3.4`. 4 | 5 | > **Note**: The minimum version of Pylint this extension supports is `3.3.0`. If you are having issues with Pylint, please report it to [this issue tracker](https://github.com/pylint-dev/pylint/issues) as this extension is just a wrapper around Pylint. 6 | 7 | This extension supports all [actively supported versions](https://devguide.python.org/#status-of-python-branches) of the Python language. 8 | 9 | For more information on Pylint, see https://pylint.readthedocs.io/ 10 | 11 | ## Usage and Features 12 | 13 | The Pylint extension provides a series of features to help your productivity while working with Python code in Visual Studio Code. Check out the [Settings section](#settings) below for more details on how to customize the extension. 14 | 15 | - **Integrated Linting**: Once this extension is installed in Visual Studio Code, Pylint is automatically executed when you open a Python file, providing immediate feedback on your code quality. 16 | - **Customizable Pylint Version**: By default, this extension uses the version of Pylint that is shipped with the extension. However, you can configure it to use a different binary installed in your environment through the `pylint.importStrategy` setting, or set it to a custom Pylint executable through the `pylint.path` settings. 17 | - **Immediate Feedback**: By default, Pylint will update the diagnostics in the editor once you save the file. But you can get immediate feedback on your code quality as you type by enabling the `pylint.lintOnChange` setting. 18 | - **Mono repo support**: If you are working with a mono repo, you can configure the extension to lint Python files in subfolders of the workspace root folder by setting the `pylint.cwd` setting to `${fileDirname}`. You can also set it to ignore/skip linting for certain files or folder paths by specifying a glob pattern to the `pylint.ignorePatterns` setting. 19 | - **Customizable Linting Rules**: You can customize the severity of specific Pylint error codes through the `pylint.severity` setting. 20 | 21 | ### Disabling Pylint 22 | 23 | You can skip linting with Pylint for specific files or directories by setting the `pylint.ignorePatterns` setting. 24 | 25 | But if you wish to disable linting with Pylint for your entire workspace or globally, you can [disable this extension](https://code.visualstudio.com/docs/editor/extension-marketplace#_disable-an-extension) in Visual Studio Code. 26 | 27 | ## Settings 28 | 29 | There are several settings you can configure to customize the behavior of this extension. 30 | 31 | | Settings | Default | Description | 32 | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 33 | | pylint.args | `[]` | Arguments passed to Pylint for linting Python files. Each argument should be provided as a separate string in the array.
Examples:
- `"pylint.args": ["--rcfile="]`
- `"pylint.args": ["--disable=C0111", "--max-line-length=120"]` | 34 | | pylint.cwd | `${workspaceFolder}` | Sets the current working directory used to lint Python files with Pylint. By default, it uses the root directory of the workspace `${workspaceFolder}`. You can set it to `${fileDirname}` to use the parent folder of the file being linted as the working directory for Pylint. | 35 | | pylint.enabled | `true` | Enable/disable linting Python files with Pylint. This setting can be applied globally or at the workspace level. If disabled, the linting server itself will continue to be active and monitor read and write events, but it won't perform linting or expose code actions. | 36 | | pylint.severity | `{ "convention": "Information", "error": "Error", "fatal": "Error", "refactor": "Hint", "warning": "Warning", "info": "Information" }` | Mapping of Pylint's message types to VS Code's diagnostic severity levels as displayed in the Problems window. You can also use it to override specific Pylint error codes. E.g. `{ "convention": "Information", "error": "Error", "fatal": "Error", "refactor": "Hint", "warning": "Warning", "W0611": "Error", "undefined-variable": "Warning" }` | 37 | | pylint.path | `[]` | "Path or command to be used by the extension to lint Python files with Pylint. Accepts an array of a single or multiple strings. If passing a command, each argument should be provided as a separate string in the array. If set to `["pylint"]`, it will use the version of Pylint available in the `PATH` environment variable. Note: Using this option may slowdown linting.
Examples:
- `"pylint.path" : ["~/global_env/pylint"]`
- `"pylint.path" : ["conda", "run", "-n", "lint_env", "python", "-m", "pylint"]`
- `"pylint.path" : ["pylint"]`
- `"pylint.path" : ["${interpreter}", "-m", "pylint"]` | 38 | | pylint.interpreter | `[]` | Path to a Python executable or a command that will be used to launch the Pylint server and any subprocess. Accepts an array of a single or multiple strings. When set to `[]`, the extension will use the path to the selected Python interpreter. If passing a command, each argument should be provided as a separate string in the array. | 39 | | pylint.importStrategy | `useBundled` | Defines which Pylint binary to be used to lint Python files. When set to `useBundled`, the extension will use the Pylint binary that is shipped with the extension. When set to `fromEnvironment`, the extension will attempt to use the Pylint binary and all dependencies that are available in the currently selected environment. Note: If the extension can't find a valid Pylint binary in the selected environment, it will fallback to using the Pylint binary that is shipped with the extension. This setting will be overriden if `pylint.path` is set. | 40 | | pylint.showNotification | `off` | Controls when notifications are shown by this extension. Accepted values are `onError`, `onWarning`, `always` and `off`. | 41 | | pylint.lintOnChange | `false` | Enable linting Python files with Pylint as you type. | 42 | | pylint.ignorePatterns | `[]` | Configure [glob patterns](https://docs.python.org/3/library/fnmatch.html) as supported by the fnmatch Python library to exclude files or folders from being linted with Pylint. | 43 | 44 | The following variables are supported for substitution in the `pylint.args`, `pylint.cwd`, `pylint.path`, `pylint.interpreter` and `pylint.ignorePatterns` settings: 45 | 46 | - `${workspaceFolder}` 47 | - `${workspaceFolder:FolderName}` 48 | - `${userHome}` 49 | - `${env:EnvVarName}` 50 | 51 | The `pylint.path` setting also supports the `${interpreter}` variable as one of the entries of the array. This variable is subtituted based on the value of the `pylint.interpreter` setting. 52 | 53 | ## Commands 54 | 55 | | Command | Description | 56 | | ---------------------- | --------------------------------- | 57 | | Pylint: Restart Server | Force re-start the linter server. | 58 | 59 | ## Logging 60 | 61 | From the Command Palette (**View** > **Command Palette ...**), run the **Developer: Set Log Level...** command. Select **Pylint** from the **Extension logs** group. Then select the log level you want to set. 62 | 63 | Alternatively, you can set the `pylint.trace.server` setting to `verbose` to get more detailed logs from the Pylint server. This can be helpful when filing bug reports. 64 | 65 | To open the logs, click on the language status icon (`{}`) on the bottom right of the Status bar, next to the Python language mode. Locate the **Pylint** entry and select **Open logs**. 66 | 67 | ## Troubleshooting 68 | 69 | In this section, you will find some common issues you might encounter and how to resolve them. If you are experiencing any issues that are not covered here, please [file an issue](https://github.com/microsoft/vscode-pylint/issues). 70 | 71 | - If the `pylint.importStrategy` setting is set to `fromEnvironment` but Pylint is not found in the selected environment, this extension will fallback to using the Pylint binary that is shipped with the extension. However, if there are dependencies installed in the environment, those dependencies will be used along with the shipped Pylint binary. This can lead to problems if the dependencies are not compatible with the shipped Pylint binary. 72 | 73 | To resolve this issue, you can: 74 | 75 | - Set the `pylint.importStrategy` setting to `useBundled` and the `pylint.path` setting to point to the custom binary of Pylint you want to use; or 76 | - Install Pylint in the selected environment. 77 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [Microsoft's definition of a security vulnerability]() of a security vulnerability, please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | - Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | - Full paths of source file(s) related to the manifestation of the issue 23 | - The location of the affected source code (tag/branch/commit or direct URL) 24 | - Any special configuration required to reproduce the issue 25 | - Step-by-step instructions to reproduce the issue 26 | - Proof-of-concept or exploit code (if possible) 27 | - Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | ## How to file issues and get help 4 | 5 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 6 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 7 | feature request as a new Issue. 8 | 9 | For help and questions about using this project, please use the GitHub Discussions. 10 | 11 | ## Microsoft Support Policy 12 | 13 | Support for this **PROJECT or PRODUCT** is limited to the resources listed above. 14 | -------------------------------------------------------------------------------- /build/azure-pipeline.pre-release.yml: -------------------------------------------------------------------------------- 1 | # Run on a schedule 2 | trigger: none 3 | pr: none 4 | 5 | schedules: 6 | - cron: '0 10 * * 1-5' # 10AM UTC (2AM PDT) MON-FRI (VS Code Pre-release builds at 9PM PDT) 7 | displayName: Nightly Pre-Release Schedule 8 | always: false # only run if there are source code changes 9 | branches: 10 | include: 11 | - main 12 | 13 | resources: 14 | repositories: 15 | - repository: templates 16 | type: github 17 | name: microsoft/vscode-engineering 18 | ref: main 19 | endpoint: Monaco 20 | 21 | parameters: 22 | - name: publishExtension 23 | displayName: 🚀 Publish Extension 24 | type: boolean 25 | default: false 26 | 27 | extends: 28 | template: azure-pipelines/extension/pre-release.yml@templates 29 | parameters: 30 | l10nSourcePaths: ./src 31 | ghCreateTag: false 32 | buildSteps: 33 | - task: NodeTool@0 34 | inputs: 35 | versionSpec: '20.19.0' 36 | displayName: Select Node version 37 | 38 | - task: UsePythonVersion@0 39 | inputs: 40 | versionSpec: '3.9' 41 | addToPath: true 42 | architecture: 'x64' 43 | displayName: Select Python version 44 | 45 | - script: npm ci 46 | displayName: Install NPM dependencies 47 | 48 | - script: python -m pip install -U pip 49 | displayName: Upgrade pip 50 | 51 | - script: python -m pip install wheel 52 | displayName: Install wheel 53 | 54 | - script: python -m pip install nox 55 | displayName: Install wheel 56 | 57 | - script: python -m nox --session install_bundled_libs 58 | displayName: Install Python dependencies 59 | 60 | - script: python ./build/update_ext_version.py --for-publishing 61 | displayName: Update build number 62 | 63 | - script: npm run package 64 | displayName: Build extension 65 | 66 | tsa: 67 | config: 68 | areaPath: 'Visual Studio Code Python Extensions' 69 | serviceTreeID: '6e6194bc-7baa-4486-86d0-9f5419626d46' 70 | enabled: true 71 | 72 | publishExtension: ${{ parameters.publishExtension }} 73 | -------------------------------------------------------------------------------- /build/azure-pipeline.stable.yml: -------------------------------------------------------------------------------- 1 | trigger: none 2 | # branches: 3 | # include: 4 | # - release* 5 | # tags: 6 | # include: ['*'] 7 | pr: none 8 | 9 | resources: 10 | repositories: 11 | - repository: templates 12 | type: github 13 | name: microsoft/vscode-engineering 14 | ref: main 15 | endpoint: Monaco 16 | 17 | parameters: 18 | - name: publishExtension 19 | displayName: 🚀 Publish Extension 20 | type: boolean 21 | default: false 22 | 23 | extends: 24 | template: azure-pipelines/extension/stable.yml@templates 25 | parameters: 26 | l10nSourcePaths: ./src 27 | publishExtension: ${{ parameters.publishExtension }} 28 | buildSteps: 29 | - task: NodeTool@0 30 | inputs: 31 | versionSpec: '20.19.0' 32 | displayName: Select Node version 33 | 34 | - task: UsePythonVersion@0 35 | inputs: 36 | versionSpec: '3.9' 37 | addToPath: true 38 | architecture: 'x64' 39 | displayName: Select Python version 40 | 41 | - script: npm ci 42 | displayName: Install NPM dependencies 43 | 44 | - script: python -m pip install -U pip 45 | displayName: Upgrade pip 46 | 47 | - script: python -m pip install wheel 48 | displayName: Install wheel 49 | 50 | - script: python -m pip install nox 51 | displayName: Install wheel 52 | 53 | - script: python -m nox --session install_bundled_libs 54 | displayName: Install Python dependencies 55 | 56 | - script: python ./build/update_ext_version.py --release --for-publishing 57 | displayName: Update build number 58 | 59 | - script: npm run package 60 | displayName: Build extension 61 | tsa: 62 | config: 63 | areaPath: 'Visual Studio Code Python Extensions' 64 | serviceTreeID: '6e6194bc-7baa-4486-86d0-9f5419626d46' 65 | enabled: true 66 | -------------------------------------------------------------------------------- /build/test_update_ext_version.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | import json 5 | 6 | import freezegun 7 | import pytest 8 | import update_ext_version 9 | 10 | TEST_DATETIME = "2022-03-14 01:23:45" 11 | 12 | # The build ID is calculated via: 13 | # "1" + datetime.datetime.strptime(TEST_DATETIME,"%Y-%m-%d %H:%M:%S").strftime('%j%H%M') 14 | EXPECTED_BUILD_ID = "10730123" 15 | 16 | 17 | def create_package_json(directory, version): 18 | """Create `package.json` in `directory` with a specified version of `version`.""" 19 | package_json = directory / "package.json" 20 | package_json.write_text(json.dumps({"version": version}), encoding="utf-8") 21 | return package_json 22 | 23 | 24 | def run_test(tmp_path, version, args, expected): 25 | package_json = create_package_json(tmp_path, version) 26 | update_ext_version.main(package_json, args) 27 | package = json.loads(package_json.read_text(encoding="utf-8")) 28 | assert expected == update_ext_version.parse_version(package["version"]) 29 | 30 | 31 | @pytest.mark.parametrize( 32 | "version, args", 33 | [ 34 | ("1.0.0-rc", []), 35 | ("1.1.0-rc", ["--release"]), 36 | ("1.0.0-rc", ["--release", "--build-id", "-1"]), 37 | ("1.0.0-rc", ["--release", "--for-publishing", "--build-id", "-1"]), 38 | ("1.0.0-rc", ["--release", "--for-publishing", "--build-id", "999999999999"]), 39 | ("1.1.0-rc", ["--build-id", "-1"]), 40 | ("1.1.0-rc", ["--for-publishing", "--build-id", "-1"]), 41 | ("1.1.0-rc", ["--for-publishing", "--build-id", "999999999999"]), 42 | ], 43 | ) 44 | def test_invalid_args(tmp_path, version, args): 45 | with pytest.raises(ValueError): 46 | run_test(tmp_path, version, args, None) 47 | 48 | 49 | @pytest.mark.parametrize( 50 | "version, args, expected", 51 | [ 52 | ("1.1.0-rc", ["--build-id", "12345"], ("1", "1", "12345", "rc")), 53 | ("1.0.0-rc", ["--release", "--build-id", "12345"], ("1", "0", "12345", "")), 54 | ( 55 | "1.1.0-rc", 56 | ["--for-publishing", "--build-id", "12345"], 57 | ("1", "1", "12345", ""), 58 | ), 59 | ( 60 | "1.0.0-rc", 61 | ["--release", "--for-publishing", "--build-id", "12345"], 62 | ("1", "0", "12345", ""), 63 | ), 64 | ( 65 | "1.0.0-rc", 66 | ["--release", "--build-id", "999999999999"], 67 | ("1", "0", "999999999999", ""), 68 | ), 69 | ( 70 | "1.1.0-rc", 71 | ["--build-id", "999999999999"], 72 | ("1", "1", "999999999999", "rc"), 73 | ), 74 | ("1.1.0-rc", [], ("1", "1", EXPECTED_BUILD_ID, "rc")), 75 | ( 76 | "1.0.0-rc", 77 | ["--release"], 78 | ("1", "0", "0", ""), 79 | ), 80 | ( 81 | "1.1.0-rc", 82 | ["--for-publishing"], 83 | ("1", "1", EXPECTED_BUILD_ID, ""), 84 | ), 85 | ( 86 | "1.0.0-rc", 87 | ["--release", "--for-publishing"], 88 | ("1", "0", "0", ""), 89 | ), 90 | ( 91 | "1.0.0-rc", 92 | ["--release"], 93 | ("1", "0", "0", ""), 94 | ), 95 | ( 96 | "1.1.0-rc", 97 | [], 98 | ("1", "1", EXPECTED_BUILD_ID, "rc"), 99 | ), 100 | ], 101 | ) 102 | @freezegun.freeze_time("2022-03-14 01:23:45") 103 | def test_update_ext_version(tmp_path, version, args, expected): 104 | run_test(tmp_path, version, args, expected) 105 | -------------------------------------------------------------------------------- /build/update_ext_version.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | import argparse 5 | import datetime 6 | import json 7 | import pathlib 8 | import sys 9 | from typing import Sequence, Tuple, Union 10 | 11 | EXT_ROOT = pathlib.Path(__file__).parent.parent 12 | PACKAGE_JSON_PATH = EXT_ROOT / "package.json" 13 | 14 | 15 | def build_arg_parse() -> argparse.ArgumentParser: 16 | """Builds the arguments parser.""" 17 | parser = argparse.ArgumentParser( 18 | description="This script updates the Python extension micro version based on the release or pre-release channel." 19 | ) 20 | parser.add_argument( 21 | "--release", 22 | action="store_true", 23 | help="Treats the current build as a release build.", 24 | ) 25 | parser.add_argument( 26 | "--build-id", 27 | action="store", 28 | type=int, 29 | default=None, 30 | help="If present, will be used as a micro version.", 31 | required=False, 32 | ) 33 | parser.add_argument( 34 | "--for-publishing", 35 | action="store_true", 36 | help="Removes `-dev` or `-rc` suffix.", 37 | ) 38 | return parser 39 | 40 | 41 | def is_even(v: Union[int, str]) -> bool: 42 | """Returns True if `v` is even.""" 43 | return not int(v) % 2 44 | 45 | 46 | def micro_build_number() -> str: 47 | """Generates the micro build number. 48 | The format is `1`. 49 | """ 50 | return f"1{datetime.datetime.now(tz=datetime.timezone.utc).strftime('%j%H%M')}" 51 | 52 | 53 | def parse_version(version: str) -> Tuple[str, str, str, str]: 54 | """Parse a version string into a tuple of version parts.""" 55 | major, minor, parts = version.split(".", maxsplit=2) 56 | try: 57 | micro, suffix = parts.split("-", maxsplit=1) 58 | except ValueError: 59 | micro = parts 60 | suffix = "" 61 | return major, minor, micro, suffix 62 | 63 | 64 | def main(package_json: pathlib.Path, argv: Sequence[str]) -> None: 65 | parser = build_arg_parse() 66 | args = parser.parse_args(argv) 67 | 68 | package = json.loads(package_json.read_text(encoding="utf-8")) 69 | 70 | major, minor, micro, suffix = parse_version(package["version"]) 71 | 72 | if args.release and not is_even(minor): 73 | raise ValueError( 74 | f"Release version should have EVEN numbered minor version: {package['version']}" 75 | ) 76 | elif not args.release and is_even(minor): 77 | raise ValueError( 78 | f"Pre-Release version should have ODD numbered minor version: {package['version']}" 79 | ) 80 | 81 | print(f"Updating build FROM: {package['version']}") 82 | if args.build_id: 83 | # If build id is provided it should fall within the 0-INT32 max range 84 | # that the max allowed value for publishing to the Marketplace. 85 | if args.build_id < 0 or ( 86 | args.for_publishing and args.build_id > ((2**32) - 1) 87 | ): 88 | raise ValueError(f"Build ID must be within [0, {(2**32) - 1}]") 89 | 90 | package["version"] = ".".join((major, minor, str(args.build_id))) 91 | elif args.release: 92 | package["version"] = ".".join((major, minor, micro)) 93 | else: 94 | # micro version only updated for pre-release. 95 | package["version"] = ".".join((major, minor, micro_build_number())) 96 | 97 | if not args.for_publishing and not args.release and len(suffix): 98 | package["version"] += "-" + suffix 99 | print(f"Updating build TO: {package['version']}") 100 | 101 | # Overwrite package.json with new data add a new-line at the end of the file. 102 | package_json.write_text( 103 | json.dumps(package, indent=4, ensure_ascii=False) + "\n", encoding="utf-8" 104 | ) 105 | 106 | 107 | if __name__ == "__main__": 108 | main(PACKAGE_JSON_PATH, sys.argv[1:]) 109 | -------------------------------------------------------------------------------- /bundled/tool/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | -------------------------------------------------------------------------------- /bundled/tool/_debug_server.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | """Debugging support for LSP.""" 4 | 5 | import os 6 | import pathlib 7 | import runpy 8 | import sys 9 | 10 | 11 | def update_sys_path(path_to_add: str) -> None: 12 | """Add given path to `sys.path`.""" 13 | if path_to_add not in sys.path and os.path.isdir(path_to_add): 14 | sys.path.append(path_to_add) 15 | 16 | 17 | # Ensure debugger is loaded before we load anything else, to debug initialization. 18 | if os.getenv("USE_DEBUGPY", None) in ["True", "TRUE", "1", "T"]: 19 | debugger_path = os.getenv("DEBUGPY_PATH", None) 20 | 21 | if debugger_path: 22 | if debugger_path.endswith("debugpy"): 23 | debugger_path = os.fspath(pathlib.Path(debugger_path).parent) 24 | 25 | update_sys_path(debugger_path) 26 | 27 | # pylint: disable=wrong-import-position,import-error 28 | import debugpy 29 | 30 | # 5678 is the default port, If you need to change it update it here 31 | # and in launch.json. 32 | debugpy.connect(5678) 33 | 34 | # This will ensure that execution is paused as soon as the debugger 35 | # connects to VS Code. If you don't want to pause here comment this 36 | # line and set breakpoints as appropriate. 37 | debugpy.breakpoint() 38 | 39 | SERVER_PATH = os.fspath(pathlib.Path(__file__).parent / "lsp_server.py") 40 | # NOTE: Set breakpoint in `lsp_server.py` before continuing. 41 | runpy.run_path(SERVER_PATH, run_name="__main__") 42 | -------------------------------------------------------------------------------- /bundled/tool/lsp_jsonrpc.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | """Light-weight JSON-RPC over standard IO.""" 4 | 5 | 6 | import atexit 7 | import io 8 | import json 9 | import os 10 | import pathlib 11 | import subprocess 12 | import threading 13 | import uuid 14 | from concurrent.futures import ThreadPoolExecutor 15 | from typing import BinaryIO, Dict, Optional, Sequence, Union 16 | 17 | CONTENT_LENGTH = "Content-Length: " 18 | RUNNER_SCRIPT = str(pathlib.Path(__file__).parent / "lsp_runner.py") 19 | 20 | 21 | def to_str(text) -> str: 22 | """Convert bytes to string as needed.""" 23 | return text.decode("utf-8") if isinstance(text, bytes) else text 24 | 25 | 26 | class StreamClosedException(Exception): 27 | """JSON RPC stream is closed.""" 28 | 29 | pass # pylint: disable=unnecessary-pass 30 | 31 | 32 | class JsonWriter: 33 | """Manages writing JSON-RPC messages to the writer stream.""" 34 | 35 | def __init__(self, writer: io.TextIOWrapper): 36 | self._writer = writer 37 | self._lock = threading.Lock() 38 | 39 | def close(self): 40 | """Closes the underlying writer stream.""" 41 | with self._lock: 42 | if not self._writer.closed: 43 | self._writer.close() 44 | 45 | def write(self, data): 46 | """Writes given data to stream in JSON-RPC format.""" 47 | if self._writer.closed: 48 | raise StreamClosedException() 49 | 50 | with self._lock: 51 | content = json.dumps(data) 52 | length = len(content.encode("utf-8")) 53 | self._writer.write( 54 | f"{CONTENT_LENGTH}{length}\r\n\r\n{content}".encode("utf-8") 55 | ) 56 | self._writer.flush() 57 | 58 | 59 | class JsonReader: 60 | """Manages reading JSON-RPC messages from stream.""" 61 | 62 | def __init__(self, reader: io.TextIOWrapper): 63 | self._reader = reader 64 | 65 | def close(self): 66 | """Closes the underlying reader stream.""" 67 | if not self._reader.closed: 68 | self._reader.close() 69 | 70 | def read(self): 71 | """Reads data from the stream in JSON-RPC format.""" 72 | if self._reader.closed: 73 | raise StreamClosedException 74 | length = None 75 | while not length: 76 | line = to_str(self._readline()) 77 | if line.startswith(CONTENT_LENGTH): 78 | length = int(line[len(CONTENT_LENGTH) :]) 79 | 80 | line = to_str(self._readline()).strip() 81 | while line: 82 | line = to_str(self._readline()).strip() 83 | 84 | content = to_str(self._reader.read(length)) 85 | return json.loads(content) 86 | 87 | def _readline(self): 88 | line = self._reader.readline() 89 | if not line: 90 | raise EOFError 91 | return line 92 | 93 | 94 | class JsonRpc: 95 | """Manages sending and receiving data over JSON-RPC.""" 96 | 97 | def __init__(self, reader: io.TextIOWrapper, writer: io.TextIOWrapper): 98 | self._reader = JsonReader(reader) 99 | self._writer = JsonWriter(writer) 100 | 101 | def close(self): 102 | """Closes the underlying streams.""" 103 | try: 104 | self._reader.close() 105 | except: # pylint: disable=bare-except 106 | pass 107 | try: 108 | self._writer.close() 109 | except: # pylint: disable=bare-except 110 | pass 111 | 112 | def send_data(self, data): 113 | """Send given data in JSON-RPC format.""" 114 | self._writer.write(data) 115 | 116 | def receive_data(self): 117 | """Receive data in JSON-RPC format.""" 118 | return self._reader.read() 119 | 120 | 121 | def create_json_rpc(readable: BinaryIO, writable: BinaryIO) -> JsonRpc: 122 | """Creates JSON-RPC wrapper for the readable and writable streams.""" 123 | return JsonRpc(readable, writable) 124 | 125 | 126 | class ProcessManager: 127 | """Manages sub-processes launched for running tools.""" 128 | 129 | def __init__(self): 130 | self._args: Dict[str, Sequence[str]] = {} 131 | self._processes: Dict[str, subprocess.Popen] = {} 132 | self._rpc: Dict[str, JsonRpc] = {} 133 | self._lock = threading.Lock() 134 | self._thread_pool = ThreadPoolExecutor(10) 135 | 136 | def stop_all_processes(self): 137 | """Send exit command to all processes and shutdown transport.""" 138 | for i in self._rpc.values(): 139 | try: 140 | i.send_data({"id": str(uuid.uuid4()), "method": "exit"}) 141 | except: # pylint: disable=bare-except 142 | pass 143 | self._thread_pool.shutdown(wait=False) 144 | 145 | def start_process( 146 | self, 147 | workspace: str, 148 | args: Sequence[str], 149 | cwd: str, 150 | env: Optional[Dict[str, str]] = None, 151 | ) -> None: 152 | """Starts a process and establishes JSON-RPC communication over stdio.""" 153 | _env = os.environ.copy() 154 | if env is not None: 155 | _env.update(env) 156 | 157 | # pylint: disable=consider-using-with 158 | proc = subprocess.Popen( 159 | args, 160 | cwd=cwd, 161 | stdout=subprocess.PIPE, 162 | stdin=subprocess.PIPE, 163 | env=_env, 164 | ) 165 | self._processes[workspace] = proc 166 | self._rpc[workspace] = create_json_rpc(proc.stdout, proc.stdin) 167 | 168 | def _monitor_process(): 169 | proc.wait() 170 | with self._lock: 171 | try: 172 | del self._processes[workspace] 173 | rpc = self._rpc.pop(workspace) 174 | rpc.close() 175 | except: # pylint: disable=bare-except 176 | pass 177 | 178 | self._thread_pool.submit(_monitor_process) 179 | 180 | def get_json_rpc(self, workspace: str) -> JsonRpc: 181 | """Gets the JSON-RPC wrapper for the a given id.""" 182 | with self._lock: 183 | if workspace in self._rpc: 184 | return self._rpc[workspace] 185 | raise StreamClosedException() 186 | 187 | 188 | _process_manager = ProcessManager() 189 | atexit.register(_process_manager.stop_all_processes) 190 | 191 | 192 | def _get_json_rpc(workspace: str) -> Union[JsonRpc, None]: 193 | try: 194 | return _process_manager.get_json_rpc(workspace) 195 | except StreamClosedException: 196 | return None 197 | except KeyError: 198 | return None 199 | 200 | 201 | def get_or_start_json_rpc( 202 | workspace: str, 203 | interpreter: Sequence[str], 204 | cwd: str, 205 | env: Optional[Dict[str, str]] = None, 206 | ) -> Union[JsonRpc, None]: 207 | """Gets an existing JSON-RPC connection or starts one and return it.""" 208 | res = _get_json_rpc(workspace) 209 | if not res: 210 | args = [*interpreter, RUNNER_SCRIPT] 211 | _process_manager.start_process(workspace, args, cwd, env) 212 | res = _get_json_rpc(workspace) 213 | return res 214 | 215 | 216 | # pylint: disable=too-few-public-methods 217 | class RpcRunResult: 218 | """Object to hold result from running tool over RPC.""" 219 | 220 | def __init__(self, stdout: str, stderr: str, exception: str = None): 221 | self.stdout = stdout 222 | self.stderr = stderr 223 | self.exception = exception 224 | 225 | 226 | # pylint: disable=too-many-arguments 227 | def run_over_json_rpc( 228 | workspace: str, 229 | interpreter: Sequence[str], 230 | module: str, 231 | argv: Sequence[str], 232 | use_stdin: bool, 233 | cwd: str, 234 | source: str = None, 235 | env: Optional[Dict[str, str]] = None, 236 | ) -> RpcRunResult: 237 | """Uses JSON-RPC to execute a command.""" 238 | rpc: Union[JsonRpc, None] = get_or_start_json_rpc(workspace, interpreter, cwd, env) 239 | if not rpc: 240 | raise ConnectionError("Failed to run over JSON-RPC.") 241 | 242 | msg_id = str(uuid.uuid4()) 243 | msg = { 244 | "id": msg_id, 245 | "method": "run", 246 | "module": module, 247 | "argv": argv, 248 | "useStdin": use_stdin, 249 | "cwd": cwd, 250 | } 251 | if source: 252 | msg["source"] = source 253 | 254 | rpc.send_data(msg) 255 | 256 | data = rpc.receive_data() 257 | 258 | if data["id"] != msg_id: 259 | return RpcRunResult( 260 | "", f"Invalid result for request: {json.dumps(msg, indent=4)}" 261 | ) 262 | 263 | if "error" in data: 264 | result = data["result"] if "result" in data else "" 265 | error = data["error"] 266 | 267 | if data.get("exception", False): 268 | return RpcRunResult(result, "", error) 269 | return RpcRunResult(result, error) 270 | 271 | return RpcRunResult(result, "") 272 | 273 | 274 | def shutdown_json_rpc(): 275 | """Shutdown all JSON-RPC processes.""" 276 | _process_manager.stop_all_processes() 277 | -------------------------------------------------------------------------------- /bundled/tool/lsp_runner.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | """ 4 | Runner to use when running under a different interpreter. 5 | """ 6 | 7 | import os 8 | import pathlib 9 | import sys 10 | import traceback 11 | 12 | 13 | # ********************************************************** 14 | # Update sys.path before importing any bundled libraries. 15 | # ********************************************************** 16 | def update_sys_path(path_to_add: str, strategy: str) -> None: 17 | """Add given path to `sys.path`.""" 18 | if path_to_add not in sys.path and os.path.isdir(path_to_add): 19 | if strategy == "useBundled": 20 | sys.path.insert(0, path_to_add) 21 | else: 22 | sys.path.append(path_to_add) 23 | 24 | 25 | # Ensure that we can import LSP libraries, and other bundled libraries. 26 | BUNDLE_DIR = pathlib.Path(__file__).parent.parent 27 | # Always use bundled server files. 28 | update_sys_path(os.fspath(BUNDLE_DIR / "tool"), "useBundled") 29 | update_sys_path( 30 | os.fspath(BUNDLE_DIR / "libs"), 31 | os.getenv("LS_IMPORT_STRATEGY", "useBundled"), 32 | ) 33 | update_sys_path(os.getcwd(), os.getenv("LS_IMPORT_STRATEGY", "useBundled")) 34 | 35 | 36 | # pylint: disable=wrong-import-position,import-error 37 | import lsp_jsonrpc as jsonrpc 38 | import lsp_utils as utils 39 | 40 | RPC = jsonrpc.create_json_rpc(sys.stdin.buffer, sys.stdout.buffer) 41 | 42 | EXIT_NOW = False 43 | while not EXIT_NOW: 44 | msg = RPC.receive_data() 45 | 46 | method = msg["method"] 47 | if method == "exit": 48 | EXIT_NOW = True 49 | continue 50 | 51 | if method == "run": 52 | is_exception = False # pylint: disable=invalid-name 53 | # This is needed to preserve sys.path, pylint modifies 54 | # sys.path and that might not work for this scenario 55 | # next time around. 56 | with utils.substitute_attr(sys, "path", [""] + sys.path[:]): 57 | try: 58 | result = utils.run_module( 59 | module=msg["module"], 60 | argv=msg["argv"], 61 | use_stdin=msg["useStdin"], 62 | cwd=msg["cwd"], 63 | source=msg["source"] if "source" in msg else None, 64 | ) 65 | except Exception: # pylint: disable=broad-except 66 | result = utils.RunResult("", traceback.format_exc(chain=True)) 67 | is_exception = True # pylint: disable=invalid-name 68 | 69 | response = {"id": msg["id"], "error": result.stderr} 70 | if is_exception: 71 | response["exception"] = is_exception 72 | elif result.stdout: 73 | response["result"] = result.stdout 74 | 75 | RPC.send_data(response) 76 | -------------------------------------------------------------------------------- /bundled/tool/lsp_utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | """Utility functions and classes for use with running tools over LSP.""" 4 | from __future__ import annotations 5 | 6 | import contextlib 7 | import fnmatch 8 | import io 9 | import os 10 | import os.path 11 | import pathlib 12 | import runpy 13 | import site 14 | import subprocess 15 | import sys 16 | import sysconfig 17 | import threading 18 | from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union 19 | 20 | # Save the working directory used when loading this module 21 | SERVER_CWD = os.getcwd() 22 | CWD_LOCK = threading.Lock() 23 | CATEGORIES = { 24 | "F": "fatal", 25 | "E": "error", 26 | "W": "warning", 27 | "C": "convention", 28 | "R": "refactor", 29 | "I": "information", 30 | } 31 | 32 | 33 | def get_message_category(code: str) -> Optional[str]: 34 | """Get the full name of the message category.""" 35 | return CATEGORIES.get(code[0].upper()) 36 | 37 | 38 | def as_list(content: Union[Any, List[Any], Tuple[Any]]) -> List[Any]: 39 | """Ensures we always get a list""" 40 | if isinstance(content, (list, tuple)): 41 | return list(content) 42 | return [content] 43 | 44 | 45 | def _get_sys_config_paths() -> List[str]: 46 | """Returns paths from sysconfig.get_paths().""" 47 | return [ 48 | path 49 | for group, path in sysconfig.get_paths().items() 50 | if group not in ["data", "platdata", "scripts"] 51 | ] 52 | 53 | 54 | def _get_extensions_dir() -> List[str]: 55 | """This is the extensions folder under ~/.vscode or ~/.vscode-server.""" 56 | 57 | # The path here is calculated relative to the tool 58 | # this is because users can launch VS Code with custom 59 | # extensions folder using the --extensions-dir argument 60 | path = pathlib.Path(__file__).parent.parent.parent.parent 61 | # ^ bundled ^ extensions 62 | # tool 63 | if path.name == "extensions": 64 | return [os.fspath(path)] 65 | return [] 66 | 67 | 68 | _stdlib_paths = set( 69 | str(pathlib.Path(p).resolve()) 70 | for p in ( 71 | as_list(site.getsitepackages()) 72 | + as_list(site.getusersitepackages()) 73 | + _get_sys_config_paths() 74 | + _get_extensions_dir() 75 | ) 76 | ) 77 | 78 | 79 | def is_same_path(file_path1: str, file_path2: str) -> bool: 80 | """Returns true if two paths are the same.""" 81 | return pathlib.Path(file_path1) == pathlib.Path(file_path2) 82 | 83 | 84 | def normalize_path(file_path: str) -> str: 85 | """Returns normalized path.""" 86 | return str(pathlib.Path(file_path).resolve()) 87 | 88 | 89 | def is_current_interpreter(executable) -> bool: 90 | """Returns true if the executable path is same as the current interpreter.""" 91 | return is_same_path(executable, sys.executable) 92 | 93 | 94 | def is_stdlib_file(file_path: str) -> bool: 95 | """Return True if the file belongs to the standard library.""" 96 | normalized_path = str(pathlib.Path(file_path).resolve()) 97 | return any(normalized_path.startswith(path) for path in _stdlib_paths) 98 | 99 | 100 | def is_match(patterns: List[str], file_path: str) -> bool: 101 | """Returns true if the file matches one of the fnmatch patterns.""" 102 | if not patterns: 103 | return False 104 | return any(fnmatch.fnmatch(file_path, pattern) for pattern in patterns) 105 | 106 | 107 | # pylint: disable-next=too-few-public-methods 108 | class RunResult: 109 | """Object to hold result from running tool.""" 110 | 111 | def __init__(self, stdout, stderr): 112 | self.stdout = stdout 113 | self.stderr = stderr 114 | 115 | 116 | class CustomIO(io.TextIOWrapper): 117 | """Custom stream object to replace stdio.""" 118 | 119 | name = None 120 | 121 | def __init__(self, name, encoding="utf-8", newline=None): 122 | self._buffer = io.BytesIO() 123 | self._buffer.name = name 124 | super().__init__(self._buffer, encoding=encoding, newline=newline) 125 | 126 | def close(self): 127 | """Provide this close method which is used by some tools.""" 128 | # This is intentionally empty. 129 | 130 | def get_value(self) -> str: 131 | """Returns value from the buffer as string.""" 132 | self.seek(0) 133 | return self.read() 134 | 135 | 136 | @contextlib.contextmanager 137 | def substitute_attr(obj: Any, attribute: str, new_value: Any): 138 | """Manage object attributes context when using runpy.run_module().""" 139 | old_value = getattr(obj, attribute) 140 | setattr(obj, attribute, new_value) 141 | yield 142 | setattr(obj, attribute, old_value) 143 | 144 | 145 | @contextlib.contextmanager 146 | def redirect_io(stream: str, new_stream): 147 | """Redirect stdio streams to a custom stream.""" 148 | old_stream = getattr(sys, stream) 149 | setattr(sys, stream, new_stream) 150 | yield 151 | setattr(sys, stream, old_stream) 152 | 153 | 154 | @contextlib.contextmanager 155 | def change_cwd(new_cwd): 156 | """Change working directory before running code.""" 157 | os.chdir(new_cwd) 158 | yield 159 | os.chdir(SERVER_CWD) 160 | 161 | 162 | class LSPServerError(Exception): 163 | """Base class for errors while working with LSP server.""" 164 | 165 | 166 | class QuickFixRegistrationError(LSPServerError): 167 | """Represents error while registering code actions quick fixes.""" 168 | 169 | def __init__(self, diagnostic_code): 170 | super().__init__() 171 | self.diagnostic_code = diagnostic_code 172 | 173 | def __repr__(self): 174 | return f'Quick Fix for "{self.diagnostic_code}" is already registered.' 175 | 176 | 177 | def _run_module( 178 | module: str, argv: Sequence[str], use_stdin: bool, source: str = None 179 | ) -> RunResult: 180 | """Runs as a module.""" 181 | str_output = CustomIO("", encoding="utf-8") 182 | str_error = CustomIO("", encoding="utf-8") 183 | 184 | try: 185 | with substitute_attr(sys, "argv", argv): 186 | with redirect_io("stdout", str_output): 187 | with redirect_io("stderr", str_error): 188 | if use_stdin and source is not None: 189 | str_input = CustomIO("", encoding="utf-8", newline="\n") 190 | with redirect_io("stdin", str_input): 191 | str_input.write(source) 192 | str_input.seek(0) 193 | runpy.run_module(module, run_name="__main__") 194 | else: 195 | runpy.run_module(module, run_name="__main__") 196 | except SystemExit: 197 | pass 198 | 199 | return RunResult(str_output.get_value(), str_error.get_value()) 200 | 201 | 202 | def run_module( 203 | module: str, argv: Sequence[str], use_stdin: bool, cwd: str, source: str = None 204 | ) -> RunResult: 205 | """Runs as a module.""" 206 | with CWD_LOCK: 207 | if is_same_path(os.getcwd(), cwd): 208 | return _run_module(module, argv, use_stdin, source) 209 | with change_cwd(cwd): 210 | return _run_module(module, argv, use_stdin, source) 211 | 212 | 213 | def run_path( 214 | argv: Sequence[str], 215 | use_stdin: bool, 216 | cwd: str, 217 | source: str = None, 218 | env: Optional[Dict[str, str]] = None, 219 | ) -> RunResult: 220 | """Runs as an executable.""" 221 | _env = os.environ.copy() 222 | if env is not None: 223 | _env.update(env) 224 | 225 | if use_stdin: 226 | with subprocess.Popen( 227 | argv, 228 | encoding="utf-8", 229 | stdout=subprocess.PIPE, 230 | stderr=subprocess.PIPE, 231 | stdin=subprocess.PIPE, 232 | cwd=cwd, 233 | env=_env, 234 | ) as process: 235 | return RunResult(*process.communicate(input=source)) 236 | else: 237 | result = subprocess.run( 238 | argv, 239 | encoding="utf-8", 240 | stdout=subprocess.PIPE, 241 | stderr=subprocess.PIPE, 242 | check=False, 243 | cwd=cwd, 244 | env=_env, 245 | ) 246 | return RunResult(result.stdout, result.stderr) 247 | 248 | 249 | def run_api( 250 | callback: Callable[[Sequence[str], CustomIO, CustomIO, CustomIO | None], None], 251 | argv: Sequence[str], 252 | use_stdin: bool, 253 | cwd: str, 254 | source: str = None, 255 | ) -> RunResult: 256 | """Run a API.""" 257 | with CWD_LOCK: 258 | if is_same_path(os.getcwd(), cwd): 259 | return _run_api(callback, argv, use_stdin, source) 260 | with change_cwd(cwd): 261 | return _run_api(callback, argv, use_stdin, source) 262 | 263 | 264 | def _run_api( 265 | callback: Callable[[Sequence[str], CustomIO, CustomIO, CustomIO | None], None], 266 | argv: Sequence[str], 267 | use_stdin: bool, 268 | source: str = None, 269 | ) -> RunResult: 270 | str_output = CustomIO("", encoding="utf-8") 271 | str_error = CustomIO("", encoding="utf-8") 272 | 273 | try: 274 | with substitute_attr(sys, "argv", argv): 275 | with redirect_io("stdout", str_output): 276 | with redirect_io("stderr", str_error): 277 | if use_stdin and source is not None: 278 | str_input = CustomIO("", encoding="utf-8", newline="\n") 279 | with redirect_io("stdin", str_input): 280 | str_input.write(source) 281 | str_input.seek(0) 282 | callback(argv, str_output, str_error, str_input) 283 | else: 284 | callback(argv, str_output, str_error) 285 | except SystemExit: 286 | pass 287 | 288 | return RunResult(str_output.get_value(), str_error.get_value()) 289 | -------------------------------------------------------------------------------- /dev-requirements.in: -------------------------------------------------------------------------------- 1 | # This file is used to generate requirements.txt. 2 | # To update requirements.txt, run the following commands. 3 | # Use `uv` with Python 3.9 when creating the environment. 4 | # 5 | # Run following command: 6 | # uv pip compile --generate-hashes --upgrade -o ./dev-requirements.txt ./dev-requirements.in 7 | 8 | black 9 | flake8 10 | flake8-pyproject 11 | isort 12 | nox 13 | pip-tools 14 | wheel 15 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by uv via the following command: 2 | # uv pip compile --generate-hashes -o ./dev-requirements.txt ./dev-requirements.in 3 | argcomplete==3.5.3 \ 4 | --hash=sha256:2ab2c4a215c59fd6caaff41a869480a23e8f6a5f910b266c1808037f4e375b61 \ 5 | --hash=sha256:c12bf50eded8aebb298c7b7da7a5ff3ee24dffd9f5281867dfe1424b58c55392 6 | # via nox 7 | attrs==25.1.0 \ 8 | --hash=sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e \ 9 | --hash=sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a 10 | # via nox 11 | black==25.1.0 \ 12 | --hash=sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171 \ 13 | --hash=sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7 \ 14 | --hash=sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da \ 15 | --hash=sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2 \ 16 | --hash=sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc \ 17 | --hash=sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666 \ 18 | --hash=sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f \ 19 | --hash=sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b \ 20 | --hash=sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32 \ 21 | --hash=sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f \ 22 | --hash=sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717 \ 23 | --hash=sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299 \ 24 | --hash=sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0 \ 25 | --hash=sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18 \ 26 | --hash=sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0 \ 27 | --hash=sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3 \ 28 | --hash=sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355 \ 29 | --hash=sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096 \ 30 | --hash=sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e \ 31 | --hash=sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9 \ 32 | --hash=sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba \ 33 | --hash=sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f 34 | # via -r ./dev-requirements.in 35 | build==1.2.2.post1 \ 36 | --hash=sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5 \ 37 | --hash=sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7 38 | # via pip-tools 39 | click==8.1.8 \ 40 | --hash=sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2 \ 41 | --hash=sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a 42 | # via 43 | # black 44 | # pip-tools 45 | colorama==0.4.6 \ 46 | --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ 47 | --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 48 | # via 49 | # build 50 | # click 51 | # colorlog 52 | colorlog==6.9.0 \ 53 | --hash=sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff \ 54 | --hash=sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2 55 | # via nox 56 | dependency-groups==1.3.0 \ 57 | --hash=sha256:1abf34d712deda5581e80d507512664d52b35d1c2d7caf16c85e58ca508547e0 \ 58 | --hash=sha256:5b9751d5d98fbd6dfd038a560a69c8382e41afcbf7ffdbcc28a2a3f85498830f 59 | # via nox 60 | distlib==0.3.9 \ 61 | --hash=sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87 \ 62 | --hash=sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403 63 | # via virtualenv 64 | filelock==3.17.0 \ 65 | --hash=sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338 \ 66 | --hash=sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e 67 | # via virtualenv 68 | flake8==7.1.2 \ 69 | --hash=sha256:1cbc62e65536f65e6d754dfe6f1bada7f5cf392d6f5db3c2b85892466c3e7c1a \ 70 | --hash=sha256:c586ffd0b41540951ae41af572e6790dbd49fc12b3aa2541685d253d9bd504bd 71 | # via 72 | # -r ./dev-requirements.in 73 | # flake8-pyproject 74 | flake8-pyproject==1.2.3 \ 75 | --hash=sha256:6249fe53545205af5e76837644dc80b4c10037e73a0e5db87ff562d75fb5bd4a 76 | # via -r ./dev-requirements.in 77 | importlib-metadata==8.6.1 \ 78 | --hash=sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e \ 79 | --hash=sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580 80 | # via build 81 | isort==6.0.0 \ 82 | --hash=sha256:567954102bb47bb12e0fae62606570faacddd441e45683968c8d1734fb1af892 \ 83 | --hash=sha256:75d9d8a1438a9432a7d7b54f2d3b45cad9a4a0fdba43617d9873379704a8bdf1 84 | # via -r ./dev-requirements.in 85 | mccabe==0.7.0 \ 86 | --hash=sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325 \ 87 | --hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e 88 | # via flake8 89 | mypy-extensions==1.0.0 \ 90 | --hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \ 91 | --hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782 92 | # via black 93 | nox==2025.2.9 \ 94 | --hash=sha256:7d1e92d1918c6980d70aee9cf1c1d19d16faa71c4afe338fffd39e8a460e2067 \ 95 | --hash=sha256:d50cd4ca568bd7621c2e6cbbc4845b3b7f7697f25d5fb0190ce8f4600be79768 96 | # via -r ./dev-requirements.in 97 | packaging==24.2 \ 98 | --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \ 99 | --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f 100 | # via 101 | # black 102 | # build 103 | # dependency-groups 104 | # nox 105 | pathspec==0.12.1 \ 106 | --hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \ 107 | --hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712 108 | # via black 109 | pip==25.0.1 \ 110 | --hash=sha256:88f96547ea48b940a3a385494e181e29fb8637898f88d88737c5049780f196ea \ 111 | --hash=sha256:c46efd13b6aa8279f33f2864459c8ce587ea6a1a59ee20de055868d8f7688f7f 112 | # via pip-tools 113 | pip-tools==7.4.1 \ 114 | --hash=sha256:4c690e5fbae2f21e87843e89c26191f0d9454f362d8acdbd695716493ec8b3a9 \ 115 | --hash=sha256:864826f5073864450e24dbeeb85ce3920cdfb09848a3d69ebf537b521f14bcc9 116 | # via -r ./dev-requirements.in 117 | platformdirs==4.3.6 \ 118 | --hash=sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907 \ 119 | --hash=sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb 120 | # via 121 | # black 122 | # virtualenv 123 | pycodestyle==2.12.1 \ 124 | --hash=sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3 \ 125 | --hash=sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521 126 | # via flake8 127 | pyflakes==3.2.0 \ 128 | --hash=sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f \ 129 | --hash=sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a 130 | # via flake8 131 | pyproject-hooks==1.2.0 \ 132 | --hash=sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8 \ 133 | --hash=sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913 134 | # via 135 | # build 136 | # pip-tools 137 | setuptools==75.8.0 \ 138 | --hash=sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6 \ 139 | --hash=sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3 140 | # via pip-tools 141 | tomli==2.2.1 \ 142 | --hash=sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6 \ 143 | --hash=sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd \ 144 | --hash=sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c \ 145 | --hash=sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b \ 146 | --hash=sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8 \ 147 | --hash=sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6 \ 148 | --hash=sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77 \ 149 | --hash=sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff \ 150 | --hash=sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea \ 151 | --hash=sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192 \ 152 | --hash=sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249 \ 153 | --hash=sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee \ 154 | --hash=sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4 \ 155 | --hash=sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98 \ 156 | --hash=sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8 \ 157 | --hash=sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4 \ 158 | --hash=sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281 \ 159 | --hash=sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744 \ 160 | --hash=sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69 \ 161 | --hash=sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13 \ 162 | --hash=sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140 \ 163 | --hash=sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e \ 164 | --hash=sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e \ 165 | --hash=sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc \ 166 | --hash=sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff \ 167 | --hash=sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec \ 168 | --hash=sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2 \ 169 | --hash=sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222 \ 170 | --hash=sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106 \ 171 | --hash=sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272 \ 172 | --hash=sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a \ 173 | --hash=sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7 174 | # via 175 | # black 176 | # build 177 | # dependency-groups 178 | # flake8-pyproject 179 | # nox 180 | # pip-tools 181 | typing-extensions==4.12.2 \ 182 | --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ 183 | --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 184 | # via black 185 | virtualenv==20.29.2 \ 186 | --hash=sha256:fdaabebf6d03b5ba83ae0a02cfe96f48a716f4fae556461d180825866f75b728 \ 187 | --hash=sha256:febddfc3d1ea571bdb1dc0f98d7b45d24def7428214d4fb73cc486c9568cce6a 188 | # via nox 189 | wheel==0.45.1 \ 190 | --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 \ 191 | --hash=sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248 192 | # via 193 | # -r ./dev-requirements.in 194 | # pip-tools 195 | zipp==3.21.0 \ 196 | --hash=sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4 \ 197 | --hash=sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931 198 | # via importlib-metadata 199 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/vscode-pylint/24f1c1d4383e0a6138096cf0dd96ff0082c9a993/icon.png -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | """All the action we need during build""" 4 | import json 5 | import os 6 | import pathlib 7 | import re 8 | import urllib.request as url_lib 9 | from typing import List 10 | 11 | import nox # pylint: disable=import-error 12 | 13 | 14 | def _install_bundle(session: nox.Session) -> None: 15 | session.install( 16 | "-t", 17 | "./bundled/libs", 18 | "--no-cache-dir", 19 | "--implementation", 20 | "py", 21 | "--no-deps", 22 | "--upgrade", 23 | "-r", 24 | "./requirements.txt", 25 | ) 26 | 27 | 28 | def _check_files(names: List[str]) -> None: 29 | root_dir = pathlib.Path(__file__).parent 30 | for name in names: 31 | file_path = root_dir / name 32 | lines: List[str] = file_path.read_text().splitlines() 33 | if any(line for line in lines if line.startswith("# TODO:")): 34 | raise ValueError(f"Please update {os.fspath(file_path)}.") 35 | 36 | 37 | def _update_pip_packages(session: nox.Session) -> None: 38 | session.run( 39 | "pip-compile", 40 | "--generate-hashes", 41 | "--resolver=backtracking", 42 | "--upgrade", 43 | "./requirements.in", 44 | ) 45 | session.run( 46 | "pip-compile", 47 | "--generate-hashes", 48 | "--resolver=backtracking", 49 | "--upgrade", 50 | "./src/test/python_tests/requirements.in", 51 | ) 52 | session.run( 53 | "pip-compile", 54 | "--generate-hashes", 55 | "--resolver=backtracking", 56 | "--upgrade", 57 | "./dev-requirements.in", 58 | ) 59 | 60 | 61 | def _get_package_data(package): 62 | json_uri = f"https://registry.npmjs.org/{package}" 63 | with url_lib.urlopen(json_uri) as response: 64 | return json.loads(response.read()) 65 | 66 | 67 | def _update_npm_packages(session: nox.Session) -> None: 68 | pinned = { 69 | "vscode-languageclient", 70 | "@types/vscode", 71 | "@types/node", 72 | } 73 | package_json_path = pathlib.Path(__file__).parent / "package.json" 74 | package_json = json.loads(package_json_path.read_text(encoding="utf-8")) 75 | 76 | for package in package_json["dependencies"]: 77 | if package not in pinned: 78 | data = _get_package_data(package) 79 | latest = "^" + data["dist-tags"]["latest"] 80 | package_json["dependencies"][package] = latest 81 | 82 | for package in package_json["devDependencies"]: 83 | if package not in pinned: 84 | data = _get_package_data(package) 85 | latest = "^" + data["dist-tags"]["latest"] 86 | package_json["devDependencies"][package] = latest 87 | 88 | # Ensure engine matches the package 89 | if ( 90 | package_json["engines"]["vscode"] 91 | != package_json["devDependencies"]["@types/vscode"] 92 | ): 93 | print( 94 | "Please check VS Code engine version and @types/vscode version in package.json." 95 | ) 96 | 97 | new_package_json = json.dumps(package_json, indent=4) 98 | # JSON dumps uses \n for line ending on all platforms by default 99 | if not new_package_json.endswith("\n"): 100 | new_package_json += "\n" 101 | package_json_path.write_text(new_package_json, encoding="utf-8") 102 | 103 | session.run("npm", "audit", "fix", external=True, success_codes=[0, 1]) 104 | session.run("npm", "install", external=True) 105 | 106 | 107 | def _setup_template_environment(session: nox.Session) -> None: 108 | session.install("wheel", "pip-tools") 109 | _update_pip_packages(session) 110 | _install_bundle(session) 111 | 112 | 113 | @nox.session(python="3.9") 114 | def install_bundled_libs(session): 115 | """Installs the libraries that will be bundled with the extension.""" 116 | session.install("wheel") 117 | _install_bundle(session) 118 | 119 | 120 | @nox.session(python="3.9") 121 | def setup(session: nox.Session) -> None: 122 | """Sets up the extension for development.""" 123 | _setup_template_environment(session) 124 | 125 | 126 | @nox.session() 127 | def tests(session: nox.Session) -> None: 128 | """Runs all the tests for the extension.""" 129 | session.install("-r", "src/test/python_tests/requirements.txt") 130 | session.run("pytest", "src/test/python_tests") 131 | 132 | session.install("freezegun") 133 | session.run("pytest", "build") 134 | 135 | 136 | @nox.session() 137 | def lint(session: nox.Session) -> None: 138 | """Runs linter and formatter checks on python files.""" 139 | session.install("-r", "src/test/python_tests/requirements.txt") 140 | 141 | session.install("pylint") 142 | session.run("pylint", "./bundled/tool") 143 | session.run( 144 | "pylint", 145 | "--ignore=./src/test/python_tests/test_data", 146 | "./src/test/python_tests", 147 | ) 148 | session.run("pylint", "noxfile.py") 149 | # check formatting using black 150 | session.install("black") 151 | session.run("black", "--check", "./bundled/tool") 152 | session.run("black", "--check", "./src/test/python_tests") 153 | session.run("black", "--check", "noxfile.py") 154 | 155 | # check import sorting using isort 156 | session.install("isort") 157 | session.run("isort", "--check", "./bundled/tool") 158 | session.run("isort", "--check", "./src/test/python_tests") 159 | session.run("isort", "--check", "noxfile.py") 160 | 161 | # check typescript code 162 | session.run("npm", "run", "lint", external=True) 163 | 164 | 165 | @nox.session() 166 | def build_package(session: nox.Session) -> None: 167 | """Builds VSIX package for publishing.""" 168 | _check_files(["README.md", "LICENSE", "SECURITY.md", "SUPPORT.md"]) 169 | _setup_template_environment(session) 170 | session.run("npm", "install", external=True) 171 | session.run("npm", "run", "vsce-package", external=True) 172 | 173 | 174 | @nox.session() 175 | def update_build_number(session: nox.Session) -> None: 176 | """Updates build number for the extension.""" 177 | if len(session.posargs) == 0: 178 | session.log("No updates to package version") 179 | return 180 | 181 | package_json_path = pathlib.Path(__file__).parent / "package.json" 182 | session.log(f"Reading package.json at: {package_json_path}") 183 | 184 | package_json = json.loads(package_json_path.read_text(encoding="utf-8")) 185 | 186 | parts = re.split("\\.|-", package_json["version"]) 187 | major, minor = parts[:2] 188 | 189 | version = f"{major}.{minor}.{session.posargs[0]}" 190 | version = version if len(parts) == 3 else f"{version}-{''.join(parts[3:])}" 191 | 192 | session.log(f"Updating version from {package_json['version']} to {version}") 193 | package_json["version"] = version 194 | package_json_path.write_text(json.dumps(package_json, indent=4), encoding="utf-8") 195 | 196 | 197 | def _get_module_name() -> str: 198 | package_json_path = pathlib.Path(__file__).parent / "package.json" 199 | package_json = json.loads(package_json_path.read_text(encoding="utf-8")) 200 | return package_json["serverInfo"]["module"] 201 | 202 | 203 | @nox.session() 204 | def validate_readme(session: nox.Session) -> None: 205 | """Ensures the linter version in 'requirements.txt' matches 'readme.md'.""" 206 | requirements_file = pathlib.Path(__file__).parent / "requirements.txt" 207 | readme_file = pathlib.Path(__file__).parent / "README.md" 208 | 209 | lines = requirements_file.read_text(encoding="utf-8").splitlines(keepends=False) 210 | module = _get_module_name() 211 | linter_ver = list(line for line in lines if line.startswith(module))[0] 212 | name, version = linter_ver.split(" ")[0].split("==") 213 | 214 | session.log(f"Looking for {name}={version} in README.md") 215 | content = readme_file.read_text(encoding="utf-8") 216 | if f"{name}={version}" not in content: 217 | raise ValueError(f"Linter info {name}={version} was not found in README.md.") 218 | session.log(f"FOUND {name}={version} in README.md") 219 | 220 | 221 | def _update_readme() -> None: 222 | requirements_file = pathlib.Path(__file__).parent / "requirements.txt" 223 | lines = requirements_file.read_text(encoding="utf-8").splitlines(keepends=False) 224 | module = _get_module_name() 225 | linter_ver = list(line for line in lines if line.startswith(module))[0] 226 | _, version = linter_ver.split(" ")[0].split("==") 227 | 228 | readme_file = pathlib.Path(__file__).parent / "README.md" 229 | content = readme_file.read_text(encoding="utf-8") 230 | regex = r"\`([a-zA-Z0-9]+)=([0-9]+\.[0-9]+\.[0-9]+)\`" 231 | result = re.sub(regex, f"`{module}={version}`", content, 0, re.MULTILINE) 232 | content = readme_file.write_text(result, encoding="utf-8") 233 | 234 | 235 | @nox.session() 236 | def update_packages(session: nox.Session) -> None: 237 | """Update pip and npm packages.""" 238 | session.install("wheel", "pip-tools") 239 | _update_pip_packages(session) 240 | _update_readme() 241 | if "--all" in session.posargs: 242 | _update_npm_packages(session) 243 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pylint", 3 | "displayName": "Pylint", 4 | "description": "%extension.description%", 5 | "version": "2025.3.0", 6 | "preview": true, 7 | "serverInfo": { 8 | "name": "Pylint", 9 | "module": "pylint" 10 | }, 11 | "publisher": "ms-python", 12 | "license": "MIT", 13 | "homepage": "https://github.com/Microsoft/vscode-pylint", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/microsoft/vscode-pylint.git" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/Microsoft/vscode-pylint/issues" 20 | }, 21 | "icon": "icon.png", 22 | "galleryBanner": { 23 | "color": "#1e415e", 24 | "theme": "dark" 25 | }, 26 | "keywords": [ 27 | "python", 28 | "linting", 29 | "pylint" 30 | ], 31 | "engines": { 32 | "vscode": "^1.74.0" 33 | }, 34 | "categories": [ 35 | "Programming Languages", 36 | "Linters" 37 | ], 38 | "extensionDependencies": [ 39 | "ms-python.python" 40 | ], 41 | "capabilities": { 42 | "virtualWorkspaces": { 43 | "supported": false, 44 | "description": "Virtual Workspaces are not supported with pylint." 45 | } 46 | }, 47 | "activationEvents": [ 48 | "onLanguage:python", 49 | "workspaceContains:.pylintrc", 50 | "workspaceContains:*.py" 51 | ], 52 | "main": "./dist/extension.js", 53 | "l10n": "./l10n", 54 | "scripts": { 55 | "vscode:prepublish": "npm run package", 56 | "compile": "webpack", 57 | "watch": "webpack --watch", 58 | "package": "webpack --mode production --devtool source-map --config ./webpack.config.js", 59 | "compile-tests": "tsc -p . --outDir out", 60 | "watch-tests": "tsc -p . -w --outDir out", 61 | "pretest": "npm run compile-tests && npm run compile", 62 | "lint": "eslint src --ext ts", 63 | "format-check": "prettier --check 'src/**/*.ts' 'build/**/*.yml' '.github/**/*.yml'", 64 | "tests": "node ./out/test/ts_tests/runTest.js", 65 | "vsce-package": "vsce package -o pylint.vsix" 66 | }, 67 | "contributes": { 68 | "languages": [ 69 | { 70 | "filenames": [ 71 | ".pylintrc" 72 | ], 73 | "id": "ini" 74 | } 75 | ], 76 | "configuration": { 77 | "properties": { 78 | "pylint.args": { 79 | "default": [], 80 | "markdownDescription": "%settings.args.description%", 81 | "items": { 82 | "type": "string" 83 | }, 84 | "scope": "resource", 85 | "type": "array" 86 | }, 87 | "pylint.cwd": { 88 | "default": "${workspaceFolder}", 89 | "markdownDescription": "%settings.cwd.description%", 90 | "scope": "resource", 91 | "type": "string", 92 | "examples": [ 93 | "${workspaceFolder}/src", 94 | "${fileDirname}" 95 | ] 96 | }, 97 | "pylint.enabled": { 98 | "default": true, 99 | "markdownDescription": "%settings.enabled.description%", 100 | "scope": "resource", 101 | "type": "boolean" 102 | }, 103 | "pylint.ignorePatterns": { 104 | "default": [], 105 | "markdownDescription": "%settings.ignorePatterns.description%", 106 | "items": { 107 | "type": "string" 108 | }, 109 | "scope": "resource", 110 | "type": "array", 111 | "uniqueItems": true, 112 | "examples": [ 113 | [ 114 | "**/site-packages/**/*.py", 115 | ".vscode/*.py" 116 | ] 117 | ] 118 | }, 119 | "pylint.importStrategy": { 120 | "default": "useBundled", 121 | "markdownDescription": "%settings.importStrategy.description%", 122 | "enum": [ 123 | "useBundled", 124 | "fromEnvironment" 125 | ], 126 | "enumDescriptions": [ 127 | "%settings.importStrategy.useBundled.description%", 128 | "%settings.importStrategy.fromEnvironment.description%" 129 | ], 130 | "scope": "resource", 131 | "type": "string" 132 | }, 133 | "pylint.interpreter": { 134 | "default": [], 135 | "markdownDescription": "%settings.interpreter.description%", 136 | "scope": "resource", 137 | "items": { 138 | "type": "string" 139 | }, 140 | "type": "array" 141 | }, 142 | "pylint.lintOnChange": { 143 | "default": false, 144 | "markdownDescription": "%settings.lintOnChange.description%", 145 | "scope": "machine", 146 | "type": "boolean", 147 | "tags": [ 148 | "experimental" 149 | ] 150 | }, 151 | "pylint.path": { 152 | "default": [], 153 | "markdownDescription": "%settings.path.description%", 154 | "scope": "resource", 155 | "items": { 156 | "type": "string" 157 | }, 158 | "type": "array", 159 | "examples": [ 160 | [ 161 | "~/global_env/pylint" 162 | ], 163 | [ 164 | "~/.env/python", 165 | "-m", 166 | "pylint" 167 | ] 168 | ] 169 | }, 170 | "pylint.severity": { 171 | "default": { 172 | "convention": "Information", 173 | "error": "Error", 174 | "fatal": "Error", 175 | "refactor": "Hint", 176 | "warning": "Warning", 177 | "info": "Information" 178 | }, 179 | "additionalProperties": { 180 | "type": "string", 181 | "enum": [ 182 | "Error", 183 | "Hint", 184 | "Information", 185 | "Warning" 186 | ] 187 | }, 188 | "markdownDescription": "%settings.severity.description%", 189 | "scope": "resource", 190 | "type": "object" 191 | }, 192 | "pylint.showNotifications": { 193 | "default": "off", 194 | "markdownDescription": "%settings.showNotifications.description%", 195 | "enum": [ 196 | "off", 197 | "onError", 198 | "onWarning", 199 | "always" 200 | ], 201 | "enumDescriptions": [ 202 | "%settings.showNotifications.off.description%", 203 | "%settings.showNotifications.onError.description%", 204 | "%settings.showNotifications.onWarning.description%", 205 | "%settings.showNotifications.always.description%" 206 | ], 207 | "scope": "machine", 208 | "type": "string" 209 | } 210 | } 211 | }, 212 | "commands": [ 213 | { 214 | "title": "%command.restartServer%", 215 | "category": "Pylint", 216 | "command": "pylint.restart" 217 | } 218 | ] 219 | }, 220 | "dependencies": { 221 | "@vscode/python-extension": "^1.0.5", 222 | "fs-extra": "^11.2.0", 223 | "vscode-languageclient": "^8.1.0" 224 | }, 225 | "devDependencies": { 226 | "@types/chai": "^4.3.16", 227 | "@types/fs-extra": "^11.0.4", 228 | "@types/glob": "^8.1.0", 229 | "@types/mocha": "^10.0.6", 230 | "@types/node": "16.x", 231 | "@types/sinon": "^17.0.3", 232 | "@types/vscode": "^1.74.0", 233 | "@typescript-eslint/eslint-plugin": "^7.13.0", 234 | "@typescript-eslint/parser": "^7.13.0", 235 | "@vscode/test-electron": "^2.4.0", 236 | "@vscode/vsce": "^2.27.0", 237 | "chai": "^4.3.10", 238 | "eslint": "^8.57.0", 239 | "glob": "^10.4.1", 240 | "mocha": "^10.4.0", 241 | "prettier": "^3.3.2", 242 | "sinon": "^18.0.0", 243 | "ts-loader": "^9.5.1", 244 | "typemoq": "^2.1.0", 245 | "typescript": "^5.4.5", 246 | "webpack": "^5.92.0", 247 | "webpack-cli": "^5.1.4" 248 | } 249 | } -------------------------------------------------------------------------------- /package.nls.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension.description": "Linting support for Python files using Pylint.", 3 | "command.restartServer": "Restart Server", 4 | "settings.args.description": "Arguments passed to Pylint for linting Python files. Each argument should be provided as a separate string in the array. \n Examples: \n- `\"pylint.args\": [\"--rcfile=\"]` \n - `\"pylint.args\": [\"--disable=C0111\", \"--max-line-length=120\"]`", 5 | "settings.cwd.description": "Sets the current working directory used to lint Python files with Pylint. By default, it uses the root directory of the workspace `${workspaceFolder}`. You can set it to `${fileDirname}` to use the parent folder of the file being linted as the working directory for Pylint.", 6 | "settings.enabled.description": "Enable/disable linting Python files with Pylint.", 7 | "settings.severity.description": "Mapping of Pylint's message types to VS Code's diagnostic severity levels as displayed in the Problems window. You can also use it to override specific Pylint error codes. \n Example:
`{\"convention\": \"Information\", \"error\": \"Error\", \"fatal\": \"Error\", \"refactor\": \"Hint\", \"warning\": \"Warning\", \"W0611\": \"Error\", \"undefined-variable\": \"Warning\"}`", 8 | "settings.lintOnChange.description": "Enable linting Python files with Pylint as you type.", 9 | "settings.path.description": "Path or command to be used by the extension to lint Python files with Pylint. Accepts an array of a single or multiple strings. If passing a command, each argument should be provided as a separate string in the array. If set to `[\"pylint\"]`, it will use the version of Pylint available in the `PATH` environment variable. Note: Using this option may slowdown linting. \nExamples: \n- `[\"~/global_env/pylint\"]` \n- `[\"conda\", \"run\", \"-n\", \"lint_env\", \"python\", \"-m\", \"pylint\"]` \n `[\"pylint\"]`", 10 | "settings.ignorePatterns.description": "Configure [glob patterns](https://docs.python.org/3/library/fnmatch.html) as supported by the fnmatch Python library to exclude files or folders from being linted with Pylint.", 11 | "settings.importStrategy.description": "Defines which Pylint binary to be used to lint Python files. When set to `useBundled`, the extension will use the Pylint binary that is shipped with the extension. When set to `fromEnvironment`, the extension will attempt to use the Pylint binary and all dependencies that are available in the currently selected environment. Note: If the extension can't find a valid Pylint binary in the selected environment, it will fallback to using the Pylint binary that is shipped with the extension The `pylint.path` setting may also be ignored when this setting is set to `fromEnvironment`.", 12 | "settings.importStrategy.useBundled.description": "Always use the bundled version of Pylint shipped with the extension.", 13 | "settings.importStrategy.fromEnvironment.description": "Use Pylint from the selected environment. If the extension fails to find a valid Pylint binary, it will fallback to using the bundled version of Pylint.", 14 | "settings.interpreter.description": "Path to a Python executable or a command that will be used to launch the Pylint server and any subprocess. Accepts an array of a single or multiple strings. When set to `[]`, the extension will use the path to the selected Python interpreter. If passing a command, each argument should be provided as a separate string in the array.", 15 | "settings.showNotifications.description": "Controls when notifications are shown by this extension. Accepted values are `onError`, `onWarning`, `always` and `off`.", 16 | "settings.showNotifications.off.description": "Never display a notification. Any errors or warning are still available in the logs.", 17 | "settings.showNotifications.onError.description": "Show notifications for errors.", 18 | "settings.showNotifications.onWarning.description": "Show notifications for errors and warnings.", 19 | "settings.showNotifications.always.description": "Show all notifications." 20 | } 21 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line_length = 88 3 | 4 | [tool.flake8] 5 | max-line-length = 99 6 | select = "F,E,W,B,B901,B902,B903" 7 | exclude = [ 8 | ".eggs", 9 | ".git", 10 | ".tox", 11 | "nssm", 12 | "obj", 13 | "out", 14 | "packages", 15 | "pywin32", 16 | "tests", 17 | "swagger_client" 18 | ] 19 | ignore = [ 20 | "E722", 21 | "B001", 22 | "W503", 23 | "E203", 24 | "E402" 25 | ] 26 | 27 | [tool.isort] 28 | multi_line_output = 3 29 | line_length = 99 30 | include_trailing_comma = true 31 | 32 | [tool.pylint.'FORMAT'] 33 | max-line-length=99 34 | 35 | [tool.pylint.'MESSAGES CONTROL'] 36 | disable= [ 37 | "bare-except", 38 | "broad-except", 39 | "consider-using-with", 40 | "consider-using-generator", 41 | "duplicate-code", 42 | "import-outside-toplevel", 43 | "invalid-name", 44 | "import-error", 45 | "too-few-public-methods", 46 | "too-many-arguments", 47 | "too-many-positional-arguments", 48 | "too-many-branches", 49 | "too-many-instance-attributes", 50 | "unspecified-encoding", 51 | "wrong-import-position", 52 | ] 53 | 54 | [tool.pyright] 55 | reportOptionalMemberAccess = false 56 | reportGeneralTypeIssues = false 57 | reportOptionalSubscript = false 58 | reportUnboundVariable = false 59 | -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | # This file is used to generate requirements.txt. 2 | # To update requirements.txt, run the following commands. 3 | # Use `uv` with Python 3.9 when creating the environment. 4 | # 5 | # Run following command: 6 | # uv pip compile --generate-hashes --upgrade -o ./requirements.txt ./requirements.in 7 | 8 | pygls 9 | packaging 10 | pylint 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by uv via the following command: 2 | # uv pip compile --generate-hashes .\requirements.in 3 | astroid==3.3.8 \ 4 | --hash=sha256:187ccc0c248bfbba564826c26f070494f7bc964fd286b6d9fff4420e55de828c \ 5 | --hash=sha256:a88c7994f914a4ea8572fac479459f4955eeccc877be3f2d959a33273b0cf40b 6 | # via pylint 7 | attrs==25.1.0 \ 8 | --hash=sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e \ 9 | --hash=sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a 10 | # via 11 | # cattrs 12 | # lsprotocol 13 | cattrs==24.1.2 \ 14 | --hash=sha256:67c7495b760168d931a10233f979b28dc04daf853b30752246f4f8471c6d68d0 \ 15 | --hash=sha256:8028cfe1ff5382df59dd36474a86e02d817b06eaf8af84555441bac915d2ef85 16 | # via 17 | # lsprotocol 18 | # pygls 19 | colorama==0.4.6 \ 20 | --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ 21 | --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 22 | # via pylint 23 | dill==0.3.9 \ 24 | --hash=sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a \ 25 | --hash=sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c 26 | # via pylint 27 | exceptiongroup==1.2.2 \ 28 | --hash=sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b \ 29 | --hash=sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc 30 | # via cattrs 31 | isort==6.0.0 \ 32 | --hash=sha256:567954102bb47bb12e0fae62606570faacddd441e45683968c8d1734fb1af892 \ 33 | --hash=sha256:75d9d8a1438a9432a7d7b54f2d3b45cad9a4a0fdba43617d9873379704a8bdf1 34 | # via pylint 35 | lsprotocol==2023.0.1 \ 36 | --hash=sha256:c75223c9e4af2f24272b14c6375787438279369236cd568f596d4951052a60f2 \ 37 | --hash=sha256:cc5c15130d2403c18b734304339e51242d3018a05c4f7d0f198ad6e0cd21861d 38 | # via pygls 39 | mccabe==0.7.0 \ 40 | --hash=sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325 \ 41 | --hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e 42 | # via pylint 43 | packaging==24.2 \ 44 | --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \ 45 | --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f 46 | # via -r ./requirements.in 47 | platformdirs==4.3.6 \ 48 | --hash=sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907 \ 49 | --hash=sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb 50 | # via pylint 51 | pygls==1.3.1 \ 52 | --hash=sha256:140edceefa0da0e9b3c533547c892a42a7d2fd9217ae848c330c53d266a55018 \ 53 | --hash=sha256:6e00f11efc56321bdeb6eac04f6d86131f654c7d49124344a9ebb968da3dd91e 54 | # via -r ./requirements.in 55 | pylint==3.3.4 \ 56 | --hash=sha256:289e6a1eb27b453b08436478391a48cd53bb0efb824873f949e709350f3de018 \ 57 | --hash=sha256:74ae7a38b177e69a9b525d0794bd8183820bfa7eb68cc1bee6e8ed22a42be4ce 58 | # via -r ./requirements.in 59 | tomli==2.2.1 \ 60 | --hash=sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6 \ 61 | --hash=sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd \ 62 | --hash=sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c \ 63 | --hash=sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b \ 64 | --hash=sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8 \ 65 | --hash=sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6 \ 66 | --hash=sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77 \ 67 | --hash=sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff \ 68 | --hash=sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea \ 69 | --hash=sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192 \ 70 | --hash=sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249 \ 71 | --hash=sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee \ 72 | --hash=sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4 \ 73 | --hash=sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98 \ 74 | --hash=sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8 \ 75 | --hash=sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4 \ 76 | --hash=sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281 \ 77 | --hash=sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744 \ 78 | --hash=sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69 \ 79 | --hash=sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13 \ 80 | --hash=sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140 \ 81 | --hash=sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e \ 82 | --hash=sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e \ 83 | --hash=sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc \ 84 | --hash=sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff \ 85 | --hash=sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec \ 86 | --hash=sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2 \ 87 | --hash=sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222 \ 88 | --hash=sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106 \ 89 | --hash=sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272 \ 90 | --hash=sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a \ 91 | --hash=sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7 92 | # via pylint 93 | tomlkit==0.13.2 \ 94 | --hash=sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde \ 95 | --hash=sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79 96 | # via pylint 97 | typing-extensions==4.12.2 \ 98 | --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ 99 | --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 100 | # via 101 | # astroid 102 | # cattrs 103 | # pylint 104 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.9.13 -------------------------------------------------------------------------------- /src/common/constants.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as path from 'path'; 5 | 6 | const folderName = path.basename(__dirname); 7 | export const EXTENSION_ROOT_DIR = 8 | folderName === 'common' ? path.dirname(path.dirname(__dirname)) : path.dirname(__dirname); 9 | export const BUNDLED_PYTHON_SCRIPTS_DIR = path.join(EXTENSION_ROOT_DIR, 'bundled'); 10 | export const SERVER_SCRIPT_PATH = path.join(BUNDLED_PYTHON_SCRIPTS_DIR, 'tool', `lsp_server.py`); 11 | export const DEBUG_SERVER_SCRIPT_PATH = path.join(BUNDLED_PYTHON_SCRIPTS_DIR, 'tool', `_debug_server.py`); 12 | export const PYTHON_MAJOR = 3; 13 | export const PYTHON_MINOR = 9; 14 | export const PYTHON_VERSION = `${PYTHON_MAJOR}.${PYTHON_MINOR}`; 15 | export const LS_SERVER_RESTART_DELAY = 1000; 16 | -------------------------------------------------------------------------------- /src/common/logging.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as util from 'util'; 5 | import { Disposable, LogOutputChannel } from 'vscode'; 6 | 7 | type Arguments = unknown[]; 8 | class OutputChannelLogger { 9 | constructor(private readonly channel: LogOutputChannel) {} 10 | 11 | public traceLog(...data: Arguments): void { 12 | this.channel.appendLine(util.format(...data)); 13 | } 14 | 15 | public traceError(...data: Arguments): void { 16 | this.channel.error(util.format(...data)); 17 | } 18 | 19 | public traceWarn(...data: Arguments): void { 20 | this.channel.warn(util.format(...data)); 21 | } 22 | 23 | public traceInfo(...data: Arguments): void { 24 | this.channel.info(util.format(...data)); 25 | } 26 | 27 | public traceVerbose(...data: Arguments): void { 28 | this.channel.debug(util.format(...data)); 29 | } 30 | } 31 | 32 | let channel: OutputChannelLogger | undefined; 33 | export function registerLogger(logChannel: LogOutputChannel): Disposable { 34 | channel = new OutputChannelLogger(logChannel); 35 | return { 36 | dispose: () => { 37 | channel = undefined; 38 | }, 39 | }; 40 | } 41 | 42 | export function traceLog(...args: Arguments): void { 43 | channel?.traceLog(...args); 44 | } 45 | 46 | export function traceError(...args: Arguments): void { 47 | channel?.traceError(...args); 48 | } 49 | 50 | export function traceWarn(...args: Arguments): void { 51 | channel?.traceWarn(...args); 52 | } 53 | 54 | export function traceInfo(...args: Arguments): void { 55 | channel?.traceInfo(...args); 56 | } 57 | 58 | export function traceVerbose(...args: Arguments): void { 59 | channel?.traceVerbose(...args); 60 | } 61 | -------------------------------------------------------------------------------- /src/common/python.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | /* eslint-disable @typescript-eslint/naming-convention */ 5 | import { commands, Disposable, Event, EventEmitter, Uri } from 'vscode'; 6 | import { traceError, traceLog } from './logging'; 7 | import { PythonExtension, ResolvedEnvironment } from '@vscode/python-extension'; 8 | import { PYTHON_MAJOR, PYTHON_MINOR, PYTHON_VERSION } from './constants'; 9 | import { getProjectRoot } from './utilities'; 10 | 11 | export interface IInterpreterDetails { 12 | path?: string[]; 13 | resource?: Uri; 14 | } 15 | 16 | const onDidChangePythonInterpreterEvent = new EventEmitter(); 17 | export const onDidChangePythonInterpreter: Event = onDidChangePythonInterpreterEvent.event; 18 | 19 | let _api: PythonExtension | undefined; 20 | async function getPythonExtensionAPI(): Promise { 21 | if (_api) { 22 | return _api; 23 | } 24 | _api = await PythonExtension.api(); 25 | return _api; 26 | } 27 | 28 | function sameInterpreter(a: string[], b: string[]): boolean { 29 | if (a.length !== b.length) { 30 | return false; 31 | } 32 | for (let i = 0; i < a.length; i++) { 33 | if (a[i] !== b[i]) { 34 | return false; 35 | } 36 | } 37 | return true; 38 | } 39 | 40 | let serverPython: string[] | undefined; 41 | function checkAndFireEvent(interpreter: string[] | undefined): void { 42 | if (interpreter === undefined) { 43 | if (serverPython) { 44 | // Python was reset for this uri 45 | serverPython = undefined; 46 | onDidChangePythonInterpreterEvent.fire(); 47 | return; 48 | } else { 49 | return; // No change in interpreter 50 | } 51 | } 52 | 53 | if (!serverPython || !sameInterpreter(serverPython, interpreter)) { 54 | serverPython = interpreter; 55 | onDidChangePythonInterpreterEvent.fire(); 56 | } 57 | } 58 | 59 | async function refreshServerPython(): Promise { 60 | const projectRoot = await getProjectRoot(); 61 | const interpreter = await getInterpreterDetails(projectRoot?.uri); 62 | checkAndFireEvent(interpreter.path); 63 | } 64 | 65 | export async function initializePython(disposables: Disposable[]): Promise { 66 | try { 67 | const api = await getPythonExtensionAPI(); 68 | 69 | if (api) { 70 | disposables.push( 71 | api.environments.onDidChangeActiveEnvironmentPath(async () => { 72 | await refreshServerPython(); 73 | }), 74 | ); 75 | 76 | traceLog('Waiting for interpreter from Python extension.'); 77 | await refreshServerPython(); 78 | } 79 | } catch (error) { 80 | traceError('Error initializing Python: ', error); 81 | } 82 | } 83 | 84 | export async function resolveInterpreter(interpreter: string[]): Promise { 85 | const api = await getPythonExtensionAPI(); 86 | return api?.environments.resolveEnvironment(interpreter[0]); 87 | } 88 | 89 | export async function getInterpreterDetails(resource?: Uri): Promise { 90 | const api = await getPythonExtensionAPI(); 91 | const environment = await api?.environments.resolveEnvironment( 92 | api?.environments.getActiveEnvironmentPath(resource), 93 | ); 94 | if (environment?.executable.uri && checkVersion(environment)) { 95 | return { path: [environment?.executable.uri.fsPath], resource }; 96 | } 97 | return { path: undefined, resource }; 98 | } 99 | 100 | export async function getDebuggerPath(): Promise { 101 | const api = await getPythonExtensionAPI(); 102 | return api?.debug.getDebuggerPackagePath(); 103 | } 104 | 105 | export async function runPythonExtensionCommand(command: string, ...rest: any[]) { 106 | await getPythonExtensionAPI(); 107 | return await commands.executeCommand(command, ...rest); 108 | } 109 | 110 | export function checkVersion(resolved: ResolvedEnvironment | undefined): boolean { 111 | const version = resolved?.version; 112 | if (version?.major === PYTHON_MAJOR && version?.minor >= PYTHON_MINOR) { 113 | return true; 114 | } 115 | traceError(`Python version ${version?.major}.${version?.minor} is not supported.`); 116 | traceError(`Selected python path: ${resolved?.executable.uri?.fsPath}`); 117 | traceError(`Supported versions are ${PYTHON_VERSION} and above.`); 118 | return false; 119 | } 120 | -------------------------------------------------------------------------------- /src/common/server.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as fsapi from 'fs-extra'; 5 | import { Disposable, env, l10n, LanguageStatusSeverity, LogOutputChannel, Uri } from 'vscode'; 6 | import { State } from 'vscode-languageclient'; 7 | import { 8 | LanguageClient, 9 | LanguageClientOptions, 10 | RevealOutputChannelOn, 11 | ServerOptions, 12 | } from 'vscode-languageclient/node'; 13 | import { DEBUG_SERVER_SCRIPT_PATH, SERVER_SCRIPT_PATH } from './constants'; 14 | import { traceError, traceInfo, traceVerbose } from './logging'; 15 | import { getDebuggerPath } from './python'; 16 | import { getExtensionSettings, getGlobalSettings, ISettings, isLintOnChangeEnabled } from './settings'; 17 | import { getLSClientTraceLevel, getDocumentSelector } from './utilities'; 18 | import { updateStatus } from './status'; 19 | 20 | export type IInitOptions = { settings: ISettings[]; globalSettings: ISettings }; 21 | 22 | async function createServer( 23 | settings: ISettings, 24 | serverId: string, 25 | serverName: string, 26 | outputChannel: LogOutputChannel, 27 | initializationOptions: IInitOptions, 28 | ): Promise { 29 | const command = settings.interpreter[0]; 30 | const cwd = settings.cwd === '${fileDirname}' ? Uri.parse(settings.workspace).fsPath : settings.cwd; 31 | 32 | // Set debugger path needed for debugging Python code. 33 | const newEnv = { ...process.env }; 34 | const debuggerPath = await getDebuggerPath(); 35 | const isDebugScript = await fsapi.pathExists(DEBUG_SERVER_SCRIPT_PATH); 36 | if (newEnv.USE_DEBUGPY && debuggerPath) { 37 | newEnv.DEBUGPY_PATH = debuggerPath; 38 | } else { 39 | newEnv.USE_DEBUGPY = 'False'; 40 | } 41 | 42 | // Set import strategy 43 | newEnv.LS_IMPORT_STRATEGY = settings.importStrategy; 44 | 45 | // Set notification type 46 | newEnv.LS_SHOW_NOTIFICATION = settings.showNotifications; 47 | 48 | newEnv.PYTHONUTF8 = '1'; 49 | 50 | if (isLintOnChangeEnabled(serverId)) { 51 | newEnv.VSCODE_PYLINT_LINT_ON_CHANGE = '1'; 52 | } 53 | 54 | const args = 55 | newEnv.USE_DEBUGPY === 'False' || !isDebugScript 56 | ? settings.interpreter.slice(1).concat([SERVER_SCRIPT_PATH]) 57 | : settings.interpreter.slice(1).concat([DEBUG_SERVER_SCRIPT_PATH]); 58 | traceInfo(`Server run command: ${[command, ...args].join(' ')}`); 59 | 60 | const serverOptions: ServerOptions = { 61 | command, 62 | args, 63 | options: { cwd, env: newEnv }, 64 | }; 65 | 66 | // Options to control the language client 67 | const clientOptions: LanguageClientOptions = { 68 | // Register the server for Python documents 69 | documentSelector: getDocumentSelector(), 70 | outputChannel: outputChannel, 71 | traceOutputChannel: outputChannel, 72 | revealOutputChannelOn: RevealOutputChannelOn.Never, 73 | initializationOptions, 74 | }; 75 | 76 | return new LanguageClient(serverId, serverName, serverOptions, clientOptions); 77 | } 78 | 79 | let _disposables: Disposable[] = []; 80 | export async function restartServer( 81 | workspaceSetting: ISettings, 82 | serverId: string, 83 | serverName: string, 84 | outputChannel: LogOutputChannel, 85 | oldLsClient?: LanguageClient, 86 | ): Promise { 87 | if (oldLsClient) { 88 | traceInfo(`Server: Stop requested`); 89 | try { 90 | await oldLsClient.stop(); 91 | } catch (ex) { 92 | traceError(`Server: Stop failed: ${ex}`); 93 | } 94 | _disposables.forEach((d) => d.dispose()); 95 | _disposables = []; 96 | } 97 | updateStatus(undefined, LanguageStatusSeverity.Information, true); 98 | 99 | const newLSClient = await createServer(workspaceSetting, serverId, serverName, outputChannel, { 100 | settings: await getExtensionSettings(serverId, true), 101 | globalSettings: await getGlobalSettings(serverId, false), 102 | }); 103 | 104 | traceInfo(`Server: Start requested.`); 105 | _disposables.push( 106 | newLSClient.onDidChangeState((e) => { 107 | switch (e.newState) { 108 | case State.Stopped: 109 | traceVerbose(`Server State: Stopped`); 110 | break; 111 | case State.Starting: 112 | traceVerbose(`Server State: Starting`); 113 | break; 114 | case State.Running: 115 | traceVerbose(`Server State: Running`); 116 | updateStatus(undefined, LanguageStatusSeverity.Information, false); 117 | break; 118 | } 119 | }), 120 | ); 121 | try { 122 | await newLSClient.start(); 123 | await newLSClient.setTrace(getLSClientTraceLevel(outputChannel.logLevel, env.logLevel)); 124 | } catch (ex) { 125 | updateStatus(l10n.t('Server failed to start.'), LanguageStatusSeverity.Error); 126 | traceError(`Server: Start failed: ${ex}`); 127 | } 128 | 129 | return newLSClient; 130 | } 131 | -------------------------------------------------------------------------------- /src/common/settings.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as path from 'path'; 5 | import { ConfigurationChangeEvent, ConfigurationScope, WorkspaceConfiguration, WorkspaceFolder } from 'vscode'; 6 | import { traceLog, traceWarn } from './logging'; 7 | import { getInterpreterDetails } from './python'; 8 | import { getConfiguration, getWorkspaceFolders } from './vscodeapi'; 9 | import { getInterpreterFromSetting } from './utilities'; 10 | 11 | const DEFAULT_SEVERITY: Record = { 12 | convention: 'Information', 13 | error: 'Error', 14 | fatal: 'Error', 15 | refactor: 'Hint', 16 | warning: 'Warning', 17 | info: 'Information', 18 | }; 19 | export interface ISettings { 20 | cwd: string; 21 | enabled: boolean; 22 | workspace: string; 23 | args: string[]; 24 | severity: Record; 25 | path: string[]; 26 | ignorePatterns: string[]; 27 | interpreter: string[]; 28 | importStrategy: string; 29 | showNotifications: string; 30 | extraPaths: string[]; 31 | } 32 | 33 | export function getExtensionSettings(namespace: string, includeInterpreter?: boolean): Promise { 34 | return Promise.all(getWorkspaceFolders().map((w) => getWorkspaceSettings(namespace, w, includeInterpreter))); 35 | } 36 | 37 | function resolveVariables( 38 | value: string[], 39 | workspace?: WorkspaceFolder, 40 | interpreter?: string[], 41 | env?: NodeJS.ProcessEnv, 42 | ): string[] { 43 | const substitutions = new Map(); 44 | const home = process.env.HOME || process.env.USERPROFILE; 45 | if (home) { 46 | substitutions.set('${userHome}', home); 47 | substitutions.set(`~/`, `${home}/`); 48 | substitutions.set(`~\\`, `${home}\\`); // Adding both path seps '/' and '\\' explicitly handle and preserve the path separators. 49 | } 50 | if (workspace) { 51 | substitutions.set('${workspaceFolder}', workspace.uri.fsPath); 52 | } 53 | 54 | substitutions.set('${cwd}', process.cwd()); 55 | getWorkspaceFolders().forEach((w) => { 56 | substitutions.set('${workspaceFolder:' + w.name + '}', w.uri.fsPath); 57 | }); 58 | 59 | env = env || process.env; 60 | if (env) { 61 | for (const [key, value] of Object.entries(env)) { 62 | if (value) { 63 | substitutions.set('${env:' + key + '}', value); 64 | } 65 | } 66 | } 67 | 68 | const modifiedValue = []; 69 | for (const v of value) { 70 | if (interpreter && v === '${interpreter}') { 71 | modifiedValue.push(...interpreter); 72 | } else { 73 | modifiedValue.push(v); 74 | } 75 | } 76 | 77 | return modifiedValue.map((s) => { 78 | for (const [key, value] of substitutions) { 79 | s = s.replace(key, value); 80 | } 81 | return s; 82 | }); 83 | } 84 | 85 | function getCwd(config: WorkspaceConfiguration, workspace: WorkspaceFolder): string { 86 | const cwd = config.get('cwd', workspace.uri.fsPath); 87 | return resolveVariables([cwd], workspace)[0]; 88 | } 89 | 90 | function getExtraPaths(_namespace: string, workspace: WorkspaceFolder): string[] { 91 | const legacyConfig = getConfiguration('python', workspace.uri); 92 | const legacyExtraPaths = legacyConfig.get('analysis.extraPaths', []); 93 | 94 | if (legacyExtraPaths.length > 0) { 95 | traceLog('Using cwd from `python.analysis.extraPaths`.'); 96 | } 97 | return legacyExtraPaths; 98 | } 99 | 100 | export async function getWorkspaceSettings( 101 | namespace: string, 102 | workspace: WorkspaceFolder, 103 | includeInterpreter?: boolean, 104 | ): Promise { 105 | const config = getConfiguration(namespace, workspace); 106 | 107 | let interpreter: string[] = []; 108 | if (includeInterpreter) { 109 | interpreter = getInterpreterFromSetting(namespace, workspace) ?? []; 110 | if (interpreter.length === 0) { 111 | traceLog(`No interpreter found from setting ${namespace}.interpreter`); 112 | traceLog(`Getting interpreter from ms-python.python extension for workspace ${workspace.uri.fsPath}`); 113 | interpreter = (await getInterpreterDetails(workspace.uri)).path ?? []; 114 | if (interpreter.length > 0) { 115 | traceLog( 116 | `Interpreter from ms-python.python extension for ${workspace.uri.fsPath}:`, 117 | `${interpreter.join(' ')}`, 118 | ); 119 | } 120 | } else { 121 | traceLog(`Interpreter from setting ${namespace}.interpreter: ${interpreter.join(' ')}`); 122 | } 123 | 124 | if (interpreter.length === 0) { 125 | traceLog(`No interpreter found for ${workspace.uri.fsPath} in settings or from ms-python.python extension`); 126 | } 127 | } 128 | 129 | const extraPaths = getExtraPaths(namespace, workspace); 130 | const workspaceSetting = { 131 | enabled: config.get('enabled', true), 132 | cwd: getCwd(config, workspace), 133 | workspace: workspace.uri.toString(), 134 | args: resolveVariables(config.get('args', []), workspace), 135 | severity: config.get>('severity', DEFAULT_SEVERITY), 136 | path: resolveVariables(config.get('path', []), workspace, interpreter), 137 | ignorePatterns: resolveVariables(config.get('ignorePatterns', []), workspace), 138 | interpreter: resolveVariables(interpreter, workspace), 139 | importStrategy: config.get('importStrategy', 'useBundled'), 140 | showNotifications: config.get('showNotifications', 'off'), 141 | extraPaths: resolveVariables(extraPaths, workspace), 142 | }; 143 | return workspaceSetting; 144 | } 145 | 146 | function getGlobalValue(config: WorkspaceConfiguration, key: string, defaultValue: T): T { 147 | const inspect = config.inspect(key); 148 | return inspect?.globalValue ?? inspect?.defaultValue ?? defaultValue; 149 | } 150 | 151 | export async function getGlobalSettings(namespace: string, includeInterpreter?: boolean): Promise { 152 | const config = getConfiguration(namespace); 153 | 154 | let interpreter: string[] = []; 155 | if (includeInterpreter) { 156 | interpreter = getGlobalValue(config, 'interpreter', []); 157 | if (interpreter === undefined || interpreter.length === 0) { 158 | interpreter = (await getInterpreterDetails()).path ?? []; 159 | } 160 | } 161 | 162 | const setting = { 163 | cwd: getGlobalValue(config, 'cwd', process.cwd()), 164 | enabled: getGlobalValue(config, 'enabled', true), 165 | workspace: process.cwd(), 166 | args: getGlobalValue(config, 'args', []), 167 | severity: getGlobalValue>(config, 'severity', DEFAULT_SEVERITY), 168 | path: getGlobalValue(config, 'path', []), 169 | ignorePatterns: getGlobalValue(config, 'ignorePatterns', []), 170 | interpreter: interpreter ?? [], 171 | importStrategy: getGlobalValue(config, 'importStrategy', 'fromEnvironment'), 172 | showNotifications: getGlobalValue(config, 'showNotifications', 'off'), 173 | extraPaths: getGlobalValue(config, 'extraPaths', []), 174 | }; 175 | return setting; 176 | } 177 | 178 | export function isLintOnChangeEnabled(namespace: string): boolean { 179 | const config = getConfiguration(namespace); 180 | return config.get('lintOnChange', false); 181 | } 182 | 183 | export function checkIfConfigurationChanged(e: ConfigurationChangeEvent, namespace: string): boolean { 184 | const settings = [ 185 | `${namespace}.args`, 186 | `${namespace}.cwd`, 187 | `${namespace}.enabled`, 188 | `${namespace}.severity`, 189 | `${namespace}.path`, 190 | `${namespace}.interpreter`, 191 | `${namespace}.importStrategy`, 192 | `${namespace}.showNotifications`, 193 | `${namespace}.ignorePatterns`, 194 | `${namespace}.lintOnChange`, 195 | 'python.analysis.extraPaths', 196 | ]; 197 | const changed = settings.map((s) => e.affectsConfiguration(s)); 198 | return changed.includes(true); 199 | } 200 | 201 | export function logLegacySettings(): void { 202 | getWorkspaceFolders().forEach((workspace) => { 203 | try { 204 | const legacyConfig = getConfiguration('python', workspace.uri); 205 | 206 | const legacyPylintEnabled = legacyConfig.get('linting.pylintEnabled', false); 207 | if (legacyPylintEnabled) { 208 | traceWarn(`"python.linting.pylintEnabled" is deprecated. You can remove that setting.`); 209 | traceWarn( 210 | 'The pylint extension is always enabled. However, you can disable it per workspace using the extensions view.', 211 | ); 212 | traceWarn('You can exclude files and folders using the `python.linting.ignorePatterns` setting.'); 213 | traceWarn( 214 | `"python.linting.pylintEnabled" value for workspace ${workspace.uri.fsPath}: ${legacyPylintEnabled}`, 215 | ); 216 | } 217 | 218 | const legacyCwd = legacyConfig.get('linting.cwd'); 219 | if (legacyCwd) { 220 | traceWarn(`"python.linting.cwd" is deprecated. Use "pylint.cwd" instead.`); 221 | traceWarn(`"python.linting.cwd" value for workspace ${workspace.uri.fsPath}: ${legacyCwd}`); 222 | } 223 | 224 | const legacyArgs = legacyConfig.get('linting.pylintArgs', []); 225 | if (legacyArgs.length > 0) { 226 | traceWarn(`"python.linting.pylintArgs" is deprecated. Use "pylint.args" instead.`); 227 | traceWarn(`"python.linting.pylintArgs" value for workspace ${workspace.uri.fsPath}:`); 228 | traceWarn(`\n${JSON.stringify(legacyArgs, null, 4)}`); 229 | } 230 | 231 | const legacyPath = legacyConfig.get('linting.pylintPath', ''); 232 | if (legacyPath.length > 0 && legacyPath !== 'pylint') { 233 | traceWarn(`"python.linting.pylintPath" is deprecated. Use "pylint.path" instead.`); 234 | traceWarn(`"python.linting.pylintPath" value for workspace ${workspace.uri.fsPath}:`); 235 | traceWarn(`\n${JSON.stringify(legacyPath, null, 4)}`); 236 | } 237 | } catch (err) { 238 | traceWarn(`Error while logging legacy settings: ${err}`); 239 | } 240 | }); 241 | } 242 | -------------------------------------------------------------------------------- /src/common/setup.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as path from 'path'; 5 | import * as fs from 'fs-extra'; 6 | import { EXTENSION_ROOT_DIR } from './constants'; 7 | 8 | export interface IServerInfo { 9 | name: string; 10 | module: string; 11 | } 12 | 13 | export function loadServerDefaults(): IServerInfo { 14 | const packageJson = path.join(EXTENSION_ROOT_DIR, 'package.json'); 15 | const content = fs.readFileSync(packageJson).toString(); 16 | const config = JSON.parse(content); 17 | return config.serverInfo as IServerInfo; 18 | } 19 | -------------------------------------------------------------------------------- /src/common/status.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import { LanguageStatusItem, Disposable, l10n, LanguageStatusSeverity } from 'vscode'; 5 | import { createLanguageStatusItem } from './vscodeapi'; 6 | import { Command } from 'vscode-languageclient'; 7 | import { getDocumentSelector } from './utilities'; 8 | 9 | let _status: LanguageStatusItem | undefined; 10 | export function registerLanguageStatusItem(id: string, name: string, command: string): Disposable { 11 | _status = createLanguageStatusItem(id, getDocumentSelector()); 12 | _status.name = name; 13 | _status.text = name; 14 | _status.command = Command.create(l10n.t('Open logs'), command); 15 | 16 | return { 17 | dispose: () => { 18 | _status?.dispose(); 19 | _status = undefined; 20 | }, 21 | }; 22 | } 23 | 24 | export function updateStatus( 25 | status: string | undefined, 26 | severity: LanguageStatusSeverity, 27 | busy?: boolean, 28 | detail?: string, 29 | ): void { 30 | if (_status) { 31 | _status.text = status && status.length > 0 ? `${_status.name}: ${status}` : `${_status.name}`; 32 | _status.severity = severity; 33 | _status.busy = busy ?? false; 34 | _status.detail = detail; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/common/utilities.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as fs from 'fs-extra'; 5 | import * as path from 'path'; 6 | import { ConfigurationScope, LogLevel, Uri, WorkspaceFolder } from 'vscode'; 7 | import { Trace } from 'vscode-jsonrpc/node'; 8 | import { getConfiguration, getWorkspaceFolders, isVirtualWorkspace } from './vscodeapi'; 9 | import { DocumentSelector } from 'vscode-languageclient'; 10 | 11 | function logLevelToTrace(logLevel: LogLevel): Trace { 12 | switch (logLevel) { 13 | case LogLevel.Error: 14 | case LogLevel.Warning: 15 | case LogLevel.Info: 16 | return Trace.Messages; 17 | 18 | case LogLevel.Debug: 19 | case LogLevel.Trace: 20 | return Trace.Verbose; 21 | 22 | case LogLevel.Off: 23 | default: 24 | return Trace.Off; 25 | } 26 | } 27 | 28 | export function getLSClientTraceLevel(channelLogLevel: LogLevel, globalLogLevel: LogLevel): Trace { 29 | if (channelLogLevel === LogLevel.Off) { 30 | return logLevelToTrace(globalLogLevel); 31 | } 32 | if (globalLogLevel === LogLevel.Off) { 33 | return logLevelToTrace(channelLogLevel); 34 | } 35 | const level = logLevelToTrace(channelLogLevel <= globalLogLevel ? channelLogLevel : globalLogLevel); 36 | return level; 37 | } 38 | 39 | export async function getProjectRoot(): Promise { 40 | const workspaces: readonly WorkspaceFolder[] = getWorkspaceFolders(); 41 | if (workspaces.length === 0) { 42 | return { 43 | uri: Uri.file(process.cwd()), 44 | name: path.basename(process.cwd()), 45 | index: 0, 46 | }; 47 | } else if (workspaces.length === 1) { 48 | return workspaces[0]; 49 | } else { 50 | let rootWorkspace = workspaces[0]; 51 | let root = undefined; 52 | for (const w of workspaces) { 53 | if (await fs.pathExists(w.uri.fsPath)) { 54 | root = w.uri.fsPath; 55 | rootWorkspace = w; 56 | break; 57 | } 58 | } 59 | 60 | for (const w of workspaces) { 61 | if (root && root.length > w.uri.fsPath.length && (await fs.pathExists(w.uri.fsPath))) { 62 | root = w.uri.fsPath; 63 | rootWorkspace = w; 64 | } 65 | } 66 | return rootWorkspace; 67 | } 68 | } 69 | 70 | export function getDocumentSelector(): DocumentSelector { 71 | // virtual workspaces are not supported yet 72 | return isVirtualWorkspace() 73 | ? [{ language: 'python' }] 74 | : [ 75 | { scheme: 'file', language: 'python' }, 76 | { scheme: 'untitled', language: 'python' }, 77 | { scheme: 'vscode-notebook', language: 'python' }, 78 | { scheme: 'vscode-notebook-cell', language: 'python' }, 79 | ]; 80 | } 81 | 82 | export function getInterpreterFromSetting(namespace: string, scope?: ConfigurationScope) { 83 | const config = getConfiguration(namespace, scope); 84 | return config.get('interpreter'); 85 | } 86 | -------------------------------------------------------------------------------- /src/common/vscodeapi.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ 5 | /* eslint-disable @typescript-eslint/no-explicit-any */ 6 | import { 7 | commands, 8 | ConfigurationScope, 9 | Disposable, 10 | languages, 11 | LanguageStatusItem, 12 | LogOutputChannel, 13 | Uri, 14 | window, 15 | workspace, 16 | WorkspaceConfiguration, 17 | WorkspaceFolder, 18 | } from 'vscode'; 19 | import { DocumentSelector } from 'vscode-languageclient'; 20 | 21 | export function createOutputChannel(name: string): LogOutputChannel { 22 | return window.createOutputChannel(name, { log: true }); 23 | } 24 | 25 | export function getConfiguration(config: string, scope?: ConfigurationScope): WorkspaceConfiguration { 26 | return workspace.getConfiguration(config, scope); 27 | } 28 | 29 | export function registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): Disposable { 30 | return commands.registerCommand(command, callback, thisArg); 31 | } 32 | 33 | export const { onDidChangeConfiguration } = workspace; 34 | 35 | export function isVirtualWorkspace(): boolean { 36 | const isVirtual = workspace.workspaceFolders && workspace.workspaceFolders.every((f) => f.uri.scheme !== 'file'); 37 | return !!isVirtual; 38 | } 39 | 40 | export function getWorkspaceFolders(): readonly WorkspaceFolder[] { 41 | return workspace.workspaceFolders ?? []; 42 | } 43 | 44 | export function getWorkspaceFolder(uri: Uri): WorkspaceFolder | undefined { 45 | return workspace.getWorkspaceFolder(uri); 46 | } 47 | 48 | export function createLanguageStatusItem(id: string, selector: DocumentSelector): LanguageStatusItem { 49 | return languages.createLanguageStatusItem(id, selector); 50 | } 51 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as vscode from 'vscode'; 5 | import { LanguageClient } from 'vscode-languageclient/node'; 6 | import { registerLogger, traceError, traceLog, traceVerbose } from './common/logging'; 7 | import { initializePython, onDidChangePythonInterpreter } from './common/python'; 8 | import { restartServer } from './common/server'; 9 | import { checkIfConfigurationChanged, getWorkspaceSettings, logLegacySettings } from './common/settings'; 10 | import { loadServerDefaults } from './common/setup'; 11 | import { getInterpreterFromSetting, getLSClientTraceLevel, getProjectRoot } from './common/utilities'; 12 | import { createOutputChannel, onDidChangeConfiguration, registerCommand } from './common/vscodeapi'; 13 | import { registerLanguageStatusItem, updateStatus } from './common/status'; 14 | import { LS_SERVER_RESTART_DELAY, PYTHON_VERSION } from './common/constants'; 15 | 16 | let lsClient: LanguageClient | undefined; 17 | export async function activate(context: vscode.ExtensionContext): Promise { 18 | // This is required to get server name and module. This should be 19 | // the first thing that we do in this extension. 20 | const serverInfo = loadServerDefaults(); 21 | const serverName = serverInfo.name; 22 | const serverId = serverInfo.module; 23 | 24 | // Setup logging 25 | const outputChannel = createOutputChannel(serverName); 26 | context.subscriptions.push(outputChannel, registerLogger(outputChannel)); 27 | 28 | const changeLogLevel = async (c: vscode.LogLevel, g: vscode.LogLevel) => { 29 | const level = getLSClientTraceLevel(c, g); 30 | await lsClient?.setTrace(level); 31 | }; 32 | 33 | context.subscriptions.push( 34 | outputChannel.onDidChangeLogLevel(async (e) => { 35 | await changeLogLevel(e, vscode.env.logLevel); 36 | }), 37 | vscode.env.onDidChangeLogLevel(async (e) => { 38 | await changeLogLevel(outputChannel.logLevel, e); 39 | }), 40 | ); 41 | 42 | traceLog(`Name: ${serverName}`); 43 | traceLog(`Module: ${serverInfo.module}`); 44 | traceVerbose(`Configuration: ${JSON.stringify(serverInfo)}`); 45 | 46 | let isRestarting = false; 47 | let restartTimer: NodeJS.Timeout | undefined; 48 | const runServer = async () => { 49 | if (isRestarting) { 50 | if (restartTimer) { 51 | clearTimeout(restartTimer); 52 | } 53 | restartTimer = setTimeout(runServer, LS_SERVER_RESTART_DELAY); 54 | return; 55 | } 56 | isRestarting = true; 57 | try { 58 | const projectRoot = await getProjectRoot(); 59 | const workspaceSetting = await getWorkspaceSettings(serverId, projectRoot, true); 60 | if (workspaceSetting.interpreter.length === 0) { 61 | updateStatus(vscode.l10n.t('Please select a Python interpreter.'), vscode.LanguageStatusSeverity.Error); 62 | traceError( 63 | 'Python interpreter missing:\r\n' + 64 | '[Option 1] Select python interpreter using the ms-python.python (select interpreter command).\r\n' + 65 | `[Option 2] Set an interpreter using "${serverId}.interpreter" setting.\r\n`, 66 | `Please use Python ${PYTHON_VERSION} or greater.`, 67 | ); 68 | } else { 69 | lsClient = await restartServer(workspaceSetting, serverId, serverName, outputChannel, lsClient); 70 | } 71 | } finally { 72 | isRestarting = false; 73 | } 74 | }; 75 | 76 | context.subscriptions.push( 77 | onDidChangePythonInterpreter(async () => { 78 | await runServer(); 79 | }), 80 | registerCommand(`${serverId}.showLogs`, async () => { 81 | outputChannel.show(); 82 | }), 83 | registerCommand(`${serverId}.restart`, async () => { 84 | await runServer(); 85 | }), 86 | onDidChangeConfiguration(async (e: vscode.ConfigurationChangeEvent) => { 87 | if (checkIfConfigurationChanged(e, serverId)) { 88 | await runServer(); 89 | } 90 | }), 91 | registerLanguageStatusItem(serverId, serverName, `${serverId}.showLogs`), 92 | ); 93 | 94 | // This is needed to inform users that they might have some legacy settings that 95 | // are no longer supported. Instructions are printed in the output channel on how 96 | // to update them. 97 | logLegacySettings(); 98 | 99 | setImmediate(async () => { 100 | const interpreter = getInterpreterFromSetting(serverId); 101 | if (interpreter === undefined || interpreter.length === 0) { 102 | traceLog(`Python extension loading`); 103 | await initializePython(context.subscriptions); 104 | traceLog(`Python extension loaded`); 105 | } else { 106 | await runServer(); 107 | } 108 | }); 109 | } 110 | 111 | export async function deactivate(): Promise { 112 | if (lsClient) { 113 | await lsClient.stop(); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/test/python_tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | -------------------------------------------------------------------------------- /src/test/python_tests/lsp_test_client/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | -------------------------------------------------------------------------------- /src/test/python_tests/lsp_test_client/constants.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | """ 4 | Constants for use with tests. 5 | """ 6 | import pathlib 7 | 8 | TEST_ROOT = pathlib.Path(__file__).parent.parent 9 | PROJECT_ROOT = TEST_ROOT.parent.parent.parent 10 | TEST_DATA = TEST_ROOT / "test_data" 11 | -------------------------------------------------------------------------------- /src/test/python_tests/lsp_test_client/defaults.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | """ 4 | Default initialize request params. 5 | """ 6 | 7 | import os 8 | from typing import Any, Dict 9 | 10 | from .constants import PROJECT_ROOT 11 | from .utils import as_uri, get_initialization_options 12 | 13 | 14 | def vscode_initialize_defaults() -> Dict[str, Any]: 15 | """Default initialize request params as generated by VS Code.""" 16 | return dict( 17 | { 18 | "processId": os.getpid(), 19 | "clientInfo": {"name": "vscode", "version": "1.45.0"}, 20 | "rootPath": str(PROJECT_ROOT), 21 | "rootUri": as_uri(str(PROJECT_ROOT)), 22 | "capabilities": { 23 | "workspace": { 24 | "applyEdit": True, 25 | "workspaceEdit": { 26 | "documentChanges": True, 27 | "resourceOperations": ["create", "rename", "delete"], 28 | "failureHandling": "textOnlyTransactional", 29 | }, 30 | "didChangeConfiguration": {"dynamicRegistration": True}, 31 | "didChangeWatchedFiles": {"dynamicRegistration": True}, 32 | "symbol": { 33 | "dynamicRegistration": True, 34 | "symbolKind": { 35 | "valueSet": [ 36 | 1, 37 | 2, 38 | 3, 39 | 4, 40 | 5, 41 | 6, 42 | 7, 43 | 8, 44 | 9, 45 | 10, 46 | 11, 47 | 12, 48 | 13, 49 | 14, 50 | 15, 51 | 16, 52 | 17, 53 | 18, 54 | 19, 55 | 20, 56 | 21, 57 | 22, 58 | 23, 59 | 24, 60 | 25, 61 | 26, 62 | ] 63 | }, 64 | "tagSupport": {"valueSet": [1]}, 65 | }, 66 | "executeCommand": {"dynamicRegistration": True}, 67 | "configuration": True, 68 | "workspaceFolders": True, 69 | }, 70 | "textDocument": { 71 | "publishDiagnostics": { 72 | "relatedInformation": True, 73 | "versionSupport": False, 74 | "tagSupport": {"valueSet": [1, 2]}, 75 | "complexDiagnosticCodeSupport": True, 76 | }, 77 | "synchronization": { 78 | "dynamicRegistration": True, 79 | "willSave": True, 80 | "willSaveWaitUntil": True, 81 | "didSave": True, 82 | }, 83 | "completion": { 84 | "dynamicRegistration": True, 85 | "contextSupport": True, 86 | "completionItem": { 87 | "snippetSupport": True, 88 | "commitCharactersSupport": True, 89 | "documentationFormat": ["markdown", "plaintext"], 90 | "deprecatedSupport": True, 91 | "preselectSupport": True, 92 | "tagSupport": {"valueSet": [1]}, 93 | "insertReplaceSupport": True, 94 | }, 95 | "completionItemKind": { 96 | "valueSet": [ 97 | 1, 98 | 2, 99 | 3, 100 | 4, 101 | 5, 102 | 6, 103 | 7, 104 | 8, 105 | 9, 106 | 10, 107 | 11, 108 | 12, 109 | 13, 110 | 14, 111 | 15, 112 | 16, 113 | 17, 114 | 18, 115 | 19, 116 | 20, 117 | 21, 118 | 22, 119 | 23, 120 | 24, 121 | 25, 122 | ] 123 | }, 124 | }, 125 | "hover": { 126 | "dynamicRegistration": True, 127 | "contentFormat": ["markdown", "plaintext"], 128 | }, 129 | "signatureHelp": { 130 | "dynamicRegistration": True, 131 | "signatureInformation": { 132 | "documentationFormat": ["markdown", "plaintext"], 133 | "parameterInformation": {"labelOffsetSupport": True}, 134 | }, 135 | "contextSupport": True, 136 | }, 137 | "definition": {"dynamicRegistration": True, "linkSupport": True}, 138 | "references": {"dynamicRegistration": True}, 139 | "documentHighlight": {"dynamicRegistration": True}, 140 | "documentSymbol": { 141 | "dynamicRegistration": True, 142 | "symbolKind": { 143 | "valueSet": [ 144 | 1, 145 | 2, 146 | 3, 147 | 4, 148 | 5, 149 | 6, 150 | 7, 151 | 8, 152 | 9, 153 | 10, 154 | 11, 155 | 12, 156 | 13, 157 | 14, 158 | 15, 159 | 16, 160 | 17, 161 | 18, 162 | 19, 163 | 20, 164 | 21, 165 | 22, 166 | 23, 167 | 24, 168 | 25, 169 | 26, 170 | ] 171 | }, 172 | "hierarchicalDocumentSymbolSupport": True, 173 | "tagSupport": {"valueSet": [1]}, 174 | }, 175 | "codeAction": { 176 | "dynamicRegistration": True, 177 | "isPreferredSupport": True, 178 | "codeActionLiteralSupport": { 179 | "codeActionKind": { 180 | "valueSet": [ 181 | "", 182 | "quickfix", 183 | "refactor", 184 | "refactor.extract", 185 | "refactor.inline", 186 | "refactor.rewrite", 187 | "source", 188 | "source.organizeImports", 189 | ] 190 | } 191 | }, 192 | }, 193 | "codeLens": {"dynamicRegistration": True}, 194 | "formatting": {"dynamicRegistration": True}, 195 | "rangeFormatting": {"dynamicRegistration": True}, 196 | "onTypeFormatting": {"dynamicRegistration": True}, 197 | "rename": {"dynamicRegistration": True, "prepareSupport": True}, 198 | "documentLink": { 199 | "dynamicRegistration": True, 200 | "tooltipSupport": True, 201 | }, 202 | "typeDefinition": { 203 | "dynamicRegistration": True, 204 | "linkSupport": True, 205 | }, 206 | "implementation": { 207 | "dynamicRegistration": True, 208 | "linkSupport": True, 209 | }, 210 | "colorProvider": {"dynamicRegistration": True}, 211 | "foldingRange": { 212 | "dynamicRegistration": True, 213 | "rangeLimit": 5000, 214 | "lineFoldingOnly": True, 215 | }, 216 | "declaration": {"dynamicRegistration": True, "linkSupport": True}, 217 | "selectionRange": {"dynamicRegistration": True}, 218 | }, 219 | "window": {"workDoneProgress": True}, 220 | }, 221 | "trace": "verbose", 222 | "workspaceFolders": [ 223 | {"uri": as_uri(str(PROJECT_ROOT)), "name": "my_project"} 224 | ], 225 | "initializationOptions": get_initialization_options(), 226 | } 227 | ) 228 | -------------------------------------------------------------------------------- /src/test/python_tests/lsp_test_client/session.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | """ 4 | LSP session client for testing. 5 | """ 6 | 7 | import os 8 | import subprocess 9 | import sys 10 | from concurrent.futures import Future, ThreadPoolExecutor 11 | from threading import Event 12 | 13 | from pyls_jsonrpc.dispatchers import MethodDispatcher 14 | from pyls_jsonrpc.endpoint import Endpoint 15 | from pyls_jsonrpc.streams import JsonRpcStreamReader, JsonRpcStreamWriter 16 | 17 | from . import defaults 18 | from .constants import PROJECT_ROOT 19 | 20 | LSP_EXIT_TIMEOUT = 5000 21 | 22 | 23 | PUBLISH_DIAGNOSTICS = "textDocument/publishDiagnostics" 24 | WINDOW_LOG_MESSAGE = "window/logMessage" 25 | WINDOW_SHOW_MESSAGE = "window/showMessage" 26 | 27 | 28 | # pylint: disable=too-many-instance-attributes 29 | class LspSession(MethodDispatcher): 30 | """Send and Receive messages over LSP as a test LS Client.""" 31 | 32 | def __init__(self, cwd=None, script=None): 33 | self.cwd = cwd if cwd else os.getcwd() 34 | # pylint: disable=consider-using-with 35 | self._thread_pool = ThreadPoolExecutor() 36 | self._sub = None 37 | self._writer = None 38 | self._reader = None 39 | self._endpoint = None 40 | self._notification_callbacks = {} 41 | self.script = ( 42 | script if script else (PROJECT_ROOT / "bundled" / "tool" / "lsp_server.py") 43 | ) 44 | 45 | def __enter__(self): 46 | """Context manager entrypoint. 47 | 48 | shell=True needed for pytest-cov to work in subprocess. 49 | """ 50 | # pylint: disable=consider-using-with 51 | self._sub = subprocess.Popen( 52 | [sys.executable, str(self.script)], 53 | stdout=subprocess.PIPE, 54 | stdin=subprocess.PIPE, 55 | bufsize=0, 56 | cwd=self.cwd, 57 | env=os.environ, 58 | shell="WITH_COVERAGE" in os.environ, 59 | ) 60 | 61 | self._writer = JsonRpcStreamWriter(os.fdopen(self._sub.stdin.fileno(), "wb")) 62 | self._reader = JsonRpcStreamReader(os.fdopen(self._sub.stdout.fileno(), "rb")) 63 | 64 | dispatcher = { 65 | PUBLISH_DIAGNOSTICS: self._publish_diagnostics, 66 | WINDOW_SHOW_MESSAGE: self._window_show_message, 67 | WINDOW_LOG_MESSAGE: self._window_log_message, 68 | } 69 | self._endpoint = Endpoint(dispatcher, self._writer.write) 70 | self._thread_pool.submit(self._reader.listen, self._endpoint.consume) 71 | return self 72 | 73 | def __exit__(self, typ, value, _tb): 74 | self.shutdown(True) 75 | try: 76 | self._sub.terminate() 77 | except Exception: # pylint:disable=broad-except 78 | pass 79 | self._endpoint.shutdown() 80 | self._thread_pool.shutdown() 81 | 82 | def initialize( 83 | self, 84 | initialize_params=None, 85 | process_server_capabilities=None, 86 | ): 87 | """Sends the initialize request to LSP server.""" 88 | if initialize_params is None: 89 | initialize_params = defaults.vscode_initialize_defaults() 90 | server_initialized = Event() 91 | 92 | def _after_initialize(fut): 93 | if process_server_capabilities: 94 | process_server_capabilities(fut.result()) 95 | self.initialized() 96 | server_initialized.set() 97 | 98 | self._send_request( 99 | "initialize", 100 | params=( 101 | initialize_params 102 | if initialize_params is not None 103 | else defaults.vscode_initialize_defaults() 104 | ), 105 | handle_response=_after_initialize, 106 | ) 107 | 108 | server_initialized.wait() 109 | 110 | def initialized(self, initialized_params=None): 111 | """Sends the initialized notification to LSP server.""" 112 | self._endpoint.notify("initialized", initialized_params or {}) 113 | 114 | def shutdown(self, should_exit, exit_timeout=LSP_EXIT_TIMEOUT): 115 | """Sends the shutdown request to LSP server.""" 116 | 117 | def _after_shutdown(_): 118 | if should_exit: 119 | self.exit_lsp(exit_timeout) 120 | 121 | fut = self._send_request("shutdown", handle_response=_after_shutdown) 122 | return fut.result() 123 | 124 | def exit_lsp(self, exit_timeout=LSP_EXIT_TIMEOUT): 125 | """Handles LSP server process exit.""" 126 | self._endpoint.notify("exit") 127 | assert self._sub.wait(exit_timeout) == 0 128 | 129 | def notify_did_change(self, did_change_params): 130 | """Sends did change notification to LSP Server.""" 131 | self._send_notification("textDocument/didChange", params=did_change_params) 132 | 133 | def notify_did_save(self, did_save_params): 134 | """Sends did save notification to LSP Server.""" 135 | self._send_notification("textDocument/didSave", params=did_save_params) 136 | 137 | def notify_did_open(self, did_open_params): 138 | """Sends did open notification to LSP Server.""" 139 | self._send_notification("textDocument/didOpen", params=did_open_params) 140 | 141 | def notify_did_close(self, did_close_params): 142 | """Sends did close notification to LSP Server.""" 143 | self._send_notification("textDocument/didClose", params=did_close_params) 144 | 145 | def text_document_formatting(self, formatting_params): 146 | """Sends text document format request to LSP server.""" 147 | fut = self._send_request("textDocument/formatting", params=formatting_params) 148 | return fut.result() 149 | 150 | def text_document_code_action(self, code_action_params): 151 | """Sends text document code actions request to LSP server.""" 152 | fut = self._send_request("textDocument/codeAction", params=code_action_params) 153 | return fut.result() 154 | 155 | def code_action_resolve(self, code_action_resolve_params): 156 | """Sends text document code actions resolve request to LSP server.""" 157 | fut = self._send_request( 158 | "codeAction/resolve", params=code_action_resolve_params 159 | ) 160 | return fut.result() 161 | 162 | def set_notification_callback(self, notification_name, callback): 163 | """Set custom LS notification handler.""" 164 | self._notification_callbacks[notification_name] = callback 165 | 166 | def get_notification_callback(self, notification_name): 167 | """Gets callback if set or default callback for a given LS 168 | notification.""" 169 | try: 170 | return self._notification_callbacks[notification_name] 171 | except KeyError: 172 | 173 | def _default_handler(_params): 174 | """Default notification handler.""" 175 | 176 | return _default_handler 177 | 178 | def _publish_diagnostics(self, publish_diagnostics_params): 179 | """Internal handler for text document publish diagnostics.""" 180 | return self._handle_notification( 181 | PUBLISH_DIAGNOSTICS, publish_diagnostics_params 182 | ) 183 | 184 | def _window_log_message(self, window_log_message_params): 185 | """Internal handler for window log message.""" 186 | return self._handle_notification(WINDOW_LOG_MESSAGE, window_log_message_params) 187 | 188 | def _window_show_message(self, window_show_message_params): 189 | """Internal handler for window show message.""" 190 | return self._handle_notification( 191 | WINDOW_SHOW_MESSAGE, window_show_message_params 192 | ) 193 | 194 | def _handle_notification(self, notification_name, params): 195 | """Internal handler for notifications.""" 196 | fut = Future() 197 | 198 | def _handler(): 199 | callback = self.get_notification_callback(notification_name) 200 | callback(params) 201 | fut.set_result(None) 202 | 203 | self._thread_pool.submit(_handler) 204 | return fut 205 | 206 | def _send_request(self, name, params=None, handle_response=lambda f: f.done()): 207 | """Sends {name} request to the LSP server.""" 208 | fut = self._endpoint.request(name, params) 209 | fut.add_done_callback(handle_response) 210 | return fut 211 | 212 | def _send_notification(self, name, params=None): 213 | """Sends {name} notification to the LSP server.""" 214 | self._endpoint.notify(name, params) 215 | -------------------------------------------------------------------------------- /src/test/python_tests/lsp_test_client/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | """ 4 | Utility functions for use with tests. 5 | """ 6 | import contextlib 7 | import json 8 | import os 9 | import pathlib 10 | import platform 11 | import random 12 | 13 | from .constants import PROJECT_ROOT 14 | 15 | 16 | def normalizecase(path: str) -> str: 17 | """Fixes 'file' uri or path case for easier testing in windows.""" 18 | if platform.system() == "Windows": 19 | return path.lower() 20 | return path 21 | 22 | 23 | def as_uri(path: str) -> str: 24 | """Return 'file' uri as string.""" 25 | return normalizecase(pathlib.Path(path).as_uri()) 26 | 27 | 28 | @contextlib.contextmanager 29 | def python_file(contents: str, root: pathlib.Path, ext: str = ".py"): 30 | """Creates a temporary Python file.""" 31 | basename = ( 32 | "".join(random.choice("abcdefghijklmnopqrstuvwxyz") for _ in range(9)) + ext 33 | ) 34 | fullpath = root / basename 35 | try: 36 | fullpath.write_text(contents) 37 | yield fullpath 38 | finally: 39 | os.unlink(str(fullpath)) 40 | 41 | 42 | def get_server_info_defaults(): 43 | """Returns server info from package.json""" 44 | package_json_path = PROJECT_ROOT / "package.json" 45 | package_json = json.loads(package_json_path.read_text()) 46 | return package_json["serverInfo"] 47 | 48 | 49 | def get_initialization_options(): 50 | """Returns initialization options from package.json""" 51 | package_json_path = PROJECT_ROOT / "package.json" 52 | package_json = json.loads(package_json_path.read_text()) 53 | 54 | server_info = package_json["serverInfo"] 55 | server_id = server_info["module"] 56 | 57 | properties = package_json["contributes"]["configuration"]["properties"] 58 | setting = {} 59 | for prop in properties: 60 | name = prop[len(server_id) + 1 :] 61 | value = properties[prop]["default"] 62 | setting[name] = value 63 | 64 | setting["workspace"] = as_uri(str(PROJECT_ROOT)) 65 | setting["interpreter"] = [] 66 | setting["cwd"] = str(PROJECT_ROOT) 67 | setting["extraPaths"] = [] 68 | 69 | return {"settings": [setting], "globalSettings": setting} 70 | -------------------------------------------------------------------------------- /src/test/python_tests/requirements.in: -------------------------------------------------------------------------------- 1 | # This file is used to generate requirements.txt. 2 | # To update requirements.txt, run the following commands. 3 | # Use `uv` with Python 3.9 when creating the environment. 4 | # 5 | # Run following command: 6 | # uv pip compile --generate-hashes --upgrade -o ./src/test/python_tests/requirements.txt ./src/test/python_tests/requirements.in 7 | 8 | pytest 9 | PyHamcrest 10 | python-jsonrpc-server 11 | -------------------------------------------------------------------------------- /src/test/python_tests/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/vscode-pylint/24f1c1d4383e0a6138096cf0dd96ff0082c9a993/src/test/python_tests/requirements.txt -------------------------------------------------------------------------------- /src/test/python_tests/test_code_actions.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | """ 4 | Test for code actions over LSP. 5 | """ 6 | 7 | import os 8 | from threading import Event 9 | 10 | import pytest 11 | from hamcrest import assert_that, greater_than, is_ 12 | 13 | from .lsp_test_client import constants, session, utils 14 | 15 | TEST_FILE_PATH = constants.TEST_DATA / "sample1" / "sample.py" 16 | TEST_FILE_URI = utils.as_uri(str(TEST_FILE_PATH)) 17 | LINTER = utils.get_server_info_defaults()["name"] 18 | TIMEOUT = 10 # 10 seconds 19 | 20 | 21 | def _expected_format_command(): 22 | return { 23 | "title": f"{LINTER}: Run document formatting", 24 | "command": "editor.action.formatDocument", 25 | } 26 | 27 | 28 | def _expected_organize_imports_command(): 29 | return { 30 | "title": f"{LINTER}: Run organize imports", 31 | "command": "editor.action.organizeImports", 32 | } 33 | 34 | 35 | @pytest.mark.parametrize( 36 | ("code", "contents", "command"), 37 | [ 38 | ( 39 | "C0301:line-too-long", 40 | # pylint: disable=line-too-long 41 | "FRUIT = ['apricot', 'blackcurrant', 'cantaloupe', 'dragon fruit', 'elderberry', 'fig', 'grapefruit', 'honeydew melon', 'jackfruit', 'kiwi', 'lemon', 'mango', 'nectarine', 'orange', 'papaya', 'quince', 'raspberry', 'strawberry', 'tangerine', 'watermelon']\n", 42 | _expected_format_command(), 43 | ), 44 | ( 45 | "C0303:trailing-whitespace", 46 | "x = 1 \ny = 1\n", 47 | _expected_format_command(), 48 | ), 49 | ( 50 | "C0304:missing-final-newline", 51 | "print('hello')", 52 | _expected_format_command(), 53 | ), 54 | ( 55 | "C0305:trailing-newlines", 56 | "VEGGIE = ['carrot', 'radish', 'cucumber', 'potato']\n\n\n", 57 | _expected_format_command(), 58 | ), 59 | ( 60 | "C0321:multiple-statements", 61 | "import sys; print(sys.executable)\n", 62 | _expected_format_command(), 63 | ), 64 | ( 65 | "C0410:multiple-imports", 66 | "import os, sys\n", 67 | _expected_organize_imports_command(), 68 | ), 69 | ( 70 | "C0411:wrong-import-order", 71 | "import os\nfrom . import utils\nimport pylint\nimport sys\n", 72 | _expected_organize_imports_command(), 73 | ), 74 | ( 75 | "C0412:ungrouped-imports", 76 | # pylint: disable=line-too-long 77 | "import logging\nimport os\nimport sys\nimport logging.config\nfrom logging.handlers import WatchedFileHandler\n", 78 | _expected_organize_imports_command(), 79 | ), 80 | ], 81 | ) 82 | def test_command_code_action(code, contents, command): 83 | """Tests for code actions which run a command.""" 84 | with utils.python_file(contents, TEST_FILE_PATH.parent) as temp_file: 85 | uri = utils.as_uri(os.fspath(temp_file)) 86 | 87 | actual = {} 88 | with session.LspSession() as ls_session: 89 | ls_session.initialize() 90 | 91 | done = Event() 92 | 93 | def _handler(params): 94 | nonlocal actual 95 | actual = params 96 | done.set() 97 | 98 | ls_session.set_notification_callback(session.PUBLISH_DIAGNOSTICS, _handler) 99 | 100 | ls_session.notify_did_open( 101 | { 102 | "textDocument": { 103 | "uri": uri, 104 | "languageId": "python", 105 | "version": 1, 106 | "text": contents, 107 | } 108 | } 109 | ) 110 | 111 | # wait for some time to receive all notifications 112 | done.wait(TIMEOUT) 113 | 114 | diagnostics = [d for d in actual["diagnostics"] if d["code"] == code] 115 | 116 | assert_that(len(diagnostics), is_(greater_than(0))) 117 | 118 | actual_code_actions = ls_session.text_document_code_action( 119 | { 120 | "textDocument": {"uri": uri}, 121 | "range": { 122 | "start": {"line": 0, "character": 0}, 123 | "end": {"line": 1, "character": 0}, 124 | }, 125 | "context": {"diagnostics": diagnostics}, 126 | } 127 | ) 128 | 129 | expected = [ 130 | { 131 | "title": command["title"], 132 | "kind": "quickfix", 133 | "diagnostics": [d], 134 | "command": command, 135 | } 136 | for d in diagnostics 137 | ] 138 | 139 | assert_that(actual_code_actions, is_(expected)) 140 | 141 | 142 | @pytest.mark.parametrize( 143 | ("code", "contents", "new_text"), 144 | [ 145 | ( 146 | "C0117:unnecessary-negation", 147 | """ 148 | if not not True: 149 | pass""", 150 | """if True: 151 | """, 152 | ), 153 | ( 154 | "C0121:singleton-comparison", 155 | """ 156 | foo = True 157 | if foo == True: 158 | pass""", 159 | """if foo: 160 | """, 161 | ), 162 | ( 163 | "C0121:singleton-comparison", 164 | """ 165 | foo = True 166 | if foo != False: 167 | pass""", 168 | """if foo: 169 | """, 170 | ), 171 | ( 172 | "C0121:singleton-comparison", 173 | """ 174 | foo = True 175 | if True == foo: 176 | pass""", 177 | """if foo: 178 | """, 179 | ), 180 | ( 181 | "C0121:singleton-comparison", 182 | """ 183 | foo = True 184 | if False != foo: 185 | pass""", 186 | """if foo: 187 | """, 188 | ), 189 | ( 190 | "C0121:singleton-comparison", 191 | """ 192 | foo = True 193 | if foo == False: 194 | pass""", 195 | """if not foo: 196 | """, 197 | ), 198 | ( 199 | "C0121:singleton-comparison", 200 | """ 201 | foo = True 202 | if foo != True: 203 | pass""", 204 | """if not foo: 205 | """, 206 | ), 207 | ( 208 | "C0121:singleton-comparison", 209 | """ 210 | foo = True 211 | if False == foo: 212 | pass""", 213 | """if not foo: 214 | """, 215 | ), 216 | ( 217 | "C0121:singleton-comparison", 218 | """ 219 | foo = True 220 | if True != foo: 221 | pass""", 222 | """if not foo: 223 | """, 224 | ), 225 | ( 226 | "C0123:unidiomatic-typecheck", 227 | """ 228 | test_score = {"Biology": 95, "History": 80} 229 | if type(test_score) is dict: 230 | pass""", 231 | """if isinstance(test_score, dict): 232 | """, 233 | ), 234 | ( 235 | "R0205:useless-object-inheritance", 236 | """ 237 | class Banana(object): 238 | pass""", 239 | """class Banana: 240 | """, 241 | ), 242 | ( 243 | "R1721:unnecessary-comprehension", 244 | """ 245 | NUMBERS = [1, 1, 2, 2, 3, 3] 246 | 247 | UNIQUE_NUMBERS = {number for number in NUMBERS} 248 | """, 249 | """UNIQUE_NUMBERS = set(NUMBERS) 250 | """, 251 | ), 252 | ( 253 | "E1141:dict-iter-missing-items", 254 | """ 255 | data = {'Paris': 2_165_423, 'New York City': 8_804_190, 'Tokyo': 13_988_129} 256 | for city, population in data: 257 | print(f"{city} has population {population}.") 258 | """, 259 | """for city, population in data.items(): 260 | """, 261 | ), 262 | ], 263 | ) 264 | def test_edit_code_action(code, contents, new_text): 265 | """Tests for code actions which run a command.""" 266 | with utils.python_file(contents, TEST_FILE_PATH.parent) as temp_file: 267 | uri = utils.as_uri(os.fspath(temp_file)) 268 | 269 | actual = {} 270 | with session.LspSession() as ls_session: 271 | ls_session.initialize() 272 | 273 | done = Event() 274 | 275 | def _handler(params): 276 | nonlocal actual 277 | actual = params 278 | done.set() 279 | 280 | ls_session.set_notification_callback(session.PUBLISH_DIAGNOSTICS, _handler) 281 | 282 | ls_session.notify_did_open( 283 | { 284 | "textDocument": { 285 | "uri": uri, 286 | "languageId": "python", 287 | "version": 1, 288 | "text": contents, 289 | } 290 | } 291 | ) 292 | 293 | # wait for some time to receive all notifications 294 | done.wait(TIMEOUT) 295 | 296 | diagnostics = [d for d in actual["diagnostics"] if d["code"] == code] 297 | 298 | assert_that(len(diagnostics), is_(greater_than(0))) 299 | 300 | actual_code_actions = ls_session.text_document_code_action( 301 | { 302 | "textDocument": {"uri": uri}, 303 | "range": { 304 | "start": {"line": 0, "character": 0}, 305 | "end": {"line": 1, "character": 0}, 306 | }, 307 | "context": {"diagnostics": diagnostics}, 308 | } 309 | ) 310 | 311 | assert_that( 312 | all("edit" not in action for action in actual_code_actions), 313 | is_(True), 314 | ) 315 | 316 | actual_code_action = ls_session.code_action_resolve(actual_code_actions[0]) 317 | 318 | changes = actual_code_action["edit"]["documentChanges"] 319 | expected = [ 320 | { 321 | "title": f"{LINTER}: Run autofix code action", 322 | "kind": "quickfix", 323 | "diagnostics": [d], 324 | "edit": { 325 | "documentChanges": [ 326 | { 327 | "textDocument": changes[0]["textDocument"], 328 | "edits": [ 329 | { 330 | "range": changes[0]["edits"][0]["range"], 331 | "newText": new_text, 332 | } 333 | ], 334 | } 335 | ] 336 | }, 337 | } 338 | for d in diagnostics 339 | ] 340 | 341 | assert_that( 342 | actual_code_action["edit"]["documentChanges"], 343 | is_(expected[0]["edit"]["documentChanges"]), 344 | ) 345 | -------------------------------------------------------------------------------- /src/test/python_tests/test_data/sample1/sample.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | print(x) 4 | -------------------------------------------------------------------------------- /src/test/python_tests/test_data/sample2/sample.py: -------------------------------------------------------------------------------- 1 | """This file is intentionally empty""" 2 | -------------------------------------------------------------------------------- /src/test/python_tests/test_extra_paths.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test for extra paths settings. 3 | """ 4 | 5 | from threading import Event 6 | from typing import Dict 7 | 8 | from hamcrest import assert_that, is_ 9 | 10 | from .lsp_test_client import constants, defaults, session, utils 11 | 12 | TEST_FILE_PATH = constants.TEST_DATA / "sample1" / "sample.py" 13 | TEST_FILE_URI = utils.as_uri(str(TEST_FILE_PATH)) 14 | TIMEOUT = 10 # 10 seconds 15 | 16 | 17 | class CallbackObject: 18 | """Object that holds results for WINDOW_LOG_MESSAGE to capture""" 19 | 20 | def __init__(self): 21 | self.result = False 22 | 23 | def check_result(self): 24 | """returns Boolean result""" 25 | return self.result 26 | 27 | def check_for_sys_paths_with_extra_paths(self, argv: Dict[str, str]): 28 | """checks if argv duplication exists and sets result boolean""" 29 | if ( 30 | argv["type"] == 4 31 | and argv["message"].startswith("sys.path") 32 | and argv["message"].find("this/is/an/extra/path") >= 0 33 | and argv["message"].find("this/is/another/extra/path") >= 0 34 | ): 35 | self.result = True 36 | 37 | 38 | def test_extra_paths(): 39 | """Test linting using pylint with extraPaths set.""" 40 | 41 | default_init = defaults.vscode_initialize_defaults() 42 | default_init["initializationOptions"]["settings"][0]["extraPaths"] = [ 43 | "this/is/an/extra/path", 44 | "this/is/another/extra/path", 45 | ] 46 | 47 | extra_paths_callback_object = CallbackObject() 48 | contents = TEST_FILE_PATH.read_text() 49 | 50 | actual = True 51 | with session.LspSession() as ls_session: 52 | ls_session.set_notification_callback( 53 | session.WINDOW_LOG_MESSAGE, 54 | extra_paths_callback_object.check_for_sys_paths_with_extra_paths, 55 | ) 56 | 57 | done = Event() 58 | 59 | def _handler(_params): 60 | done.set() 61 | 62 | ls_session.set_notification_callback(session.PUBLISH_DIAGNOSTICS, _handler) 63 | 64 | ls_session.initialize(default_init) 65 | ls_session.notify_did_open( 66 | { 67 | "textDocument": { 68 | "uri": TEST_FILE_URI, 69 | "languageId": "python", 70 | "version": 1, 71 | "text": contents, 72 | } 73 | } 74 | ) 75 | 76 | # wait for some time to receive all notifications 77 | done.wait(TIMEOUT) 78 | done.clear() 79 | 80 | # Call this second time to detect arg duplication. 81 | ls_session.notify_did_open( 82 | { 83 | "textDocument": { 84 | "uri": TEST_FILE_URI, 85 | "languageId": "python", 86 | "version": 1, 87 | "text": contents, 88 | } 89 | } 90 | ) 91 | 92 | # wait for some time to receive all notifications 93 | done.wait(TIMEOUT) 94 | 95 | actual = extra_paths_callback_object.check_result() 96 | 97 | assert_that(actual, is_(False)) 98 | -------------------------------------------------------------------------------- /src/test/python_tests/test_path_specialization.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test for path and interpreter settings. 3 | """ 4 | 5 | from threading import Event 6 | from typing import Dict 7 | 8 | from hamcrest import assert_that, is_ 9 | 10 | from .lsp_test_client import constants, defaults, session, utils 11 | 12 | TEST_FILE_PATH = constants.TEST_DATA / "sample1" / "sample.py" 13 | TEST_FILE_URI = utils.as_uri(str(TEST_FILE_PATH)) 14 | TIMEOUT = 10 # 10 seconds 15 | 16 | 17 | class CallbackObject: 18 | """Object that holds results for WINDOW_LOG_MESSAGE to capture argv""" 19 | 20 | def __init__(self): 21 | self.result = False 22 | 23 | def check_result(self): 24 | """returns Boolean result""" 25 | return self.result 26 | 27 | def check_for_argv_duplication(self, argv: Dict[str, str]): 28 | """checks if argv duplication exists and sets result boolean""" 29 | if argv["type"] == 4 and argv["message"].find("--from-stdin") >= 0: 30 | parts = argv["message"].split() 31 | count = len([x for x in parts if x.startswith("--from-stdin")]) 32 | self.result = count > 1 33 | 34 | 35 | def test_path(): 36 | """Test linting using pylint bin path set.""" 37 | 38 | default_init = defaults.vscode_initialize_defaults() 39 | default_init["initializationOptions"]["settings"][0]["path"] = ["pylint"] 40 | 41 | argv_callback_object = CallbackObject() 42 | contents = TEST_FILE_PATH.read_text(encoding="utf-8") 43 | 44 | actual = True 45 | with session.LspSession() as ls_session: 46 | ls_session.set_notification_callback( 47 | session.WINDOW_LOG_MESSAGE, 48 | argv_callback_object.check_for_argv_duplication, 49 | ) 50 | 51 | done = Event() 52 | 53 | def _handler(_params): 54 | done.set() 55 | 56 | ls_session.set_notification_callback(session.PUBLISH_DIAGNOSTICS, _handler) 57 | 58 | ls_session.initialize(default_init) 59 | ls_session.notify_did_open( 60 | { 61 | "textDocument": { 62 | "uri": TEST_FILE_URI, 63 | "languageId": "python", 64 | "version": 1, 65 | "text": contents, 66 | } 67 | } 68 | ) 69 | 70 | # wait for some time to receive all notifications 71 | done.wait(TIMEOUT) 72 | done.clear() 73 | 74 | # Call this second time to detect arg duplication. 75 | ls_session.notify_did_open( 76 | { 77 | "textDocument": { 78 | "uri": TEST_FILE_URI, 79 | "languageId": "python", 80 | "version": 1, 81 | "text": contents, 82 | } 83 | } 84 | ) 85 | 86 | # wait for some time to receive all notifications 87 | done.wait(TIMEOUT) 88 | 89 | actual = argv_callback_object.check_result() 90 | 91 | assert_that(actual, is_(False)) 92 | 93 | 94 | def test_interpreter(): 95 | """Test linting using specific python path.""" 96 | default_init = defaults.vscode_initialize_defaults() 97 | default_init["initializationOptions"]["settings"][0]["interpreter"] = ["python"] 98 | 99 | argv_callback_object = CallbackObject() 100 | contents = TEST_FILE_PATH.read_text(encoding="utf-8") 101 | 102 | actual = True 103 | with session.LspSession() as ls_session: 104 | ls_session.set_notification_callback( 105 | session.WINDOW_LOG_MESSAGE, 106 | argv_callback_object.check_for_argv_duplication, 107 | ) 108 | 109 | done = Event() 110 | 111 | def _handler(_params): 112 | done.set() 113 | 114 | ls_session.set_notification_callback(session.PUBLISH_DIAGNOSTICS, _handler) 115 | 116 | ls_session.initialize(default_init) 117 | ls_session.notify_did_open( 118 | { 119 | "textDocument": { 120 | "uri": TEST_FILE_URI, 121 | "languageId": "python", 122 | "version": 1, 123 | "text": contents, 124 | } 125 | } 126 | ) 127 | 128 | # wait for some time to receive all notifications 129 | done.wait(TIMEOUT) 130 | done.clear() 131 | 132 | # Call this second time to detect arg duplication. 133 | ls_session.notify_did_open( 134 | { 135 | "textDocument": { 136 | "uri": TEST_FILE_URI, 137 | "languageId": "python", 138 | "version": 1, 139 | "text": contents, 140 | } 141 | } 142 | ) 143 | 144 | # wait for some time to receive all notifications 145 | done.wait(TIMEOUT) 146 | 147 | actual = argv_callback_object.check_result() 148 | 149 | assert_that(actual, is_(False)) 150 | -------------------------------------------------------------------------------- /src/test/ts_tests/index.ts: -------------------------------------------------------------------------------- 1 | import * as glob from 'glob'; 2 | import Mocha from 'mocha'; 3 | import * as path from 'path'; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd', 9 | color: true, 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname, './tests'); 13 | 14 | return new Promise((c, e) => { 15 | const files = glob.globSync('**/**.test.js', { cwd: testsRoot }); 16 | 17 | // Add files to the test suite 18 | files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); 19 | 20 | try { 21 | // Run the mocha test 22 | mocha.run((failures) => { 23 | if (failures > 0) { 24 | e(new Error(`${failures} tests failed.`)); 25 | } else { 26 | c(); 27 | } 28 | }); 29 | } catch (err) { 30 | console.error(err); 31 | e(err); 32 | } 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /src/test/ts_tests/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { runTests } from '@vscode/test-electron'; 4 | import { EXTENSION_ROOT_DIR } from '../../common/constants'; 5 | 6 | async function main() { 7 | try { 8 | // The folder containing the Extension Manifest package.json 9 | // Passed to `--extensionDevelopmentPath` 10 | const extensionDevelopmentPath = EXTENSION_ROOT_DIR; 11 | 12 | // The path to test runner 13 | // Passed to --extensionTestsPath 14 | const extensionTestsPath = path.resolve(__dirname, './index'); 15 | 16 | // Download VS Code, unzip it and run the integration test 17 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 18 | } catch (err) { 19 | console.error('Failed to run tests'); 20 | console.error(err); 21 | process.exit(1); 22 | } 23 | } 24 | 25 | main(); 26 | -------------------------------------------------------------------------------- /src/test/ts_tests/tests/common/settings.unit.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import { assert } from 'chai'; 5 | import * as path from 'path'; 6 | import * as sinon from 'sinon'; 7 | import * as TypeMoq from 'typemoq'; 8 | import { Uri, WorkspaceConfiguration, WorkspaceFolder } from 'vscode'; 9 | import { EXTENSION_ROOT_DIR } from '../../../../common/constants'; 10 | import * as python from '../../../../common/python'; 11 | import { ISettings, getWorkspaceSettings, isLintOnChangeEnabled } from '../../../../common/settings'; 12 | import * as vscodeapi from '../../../../common/vscodeapi'; 13 | 14 | // eslint-disable-next-line @typescript-eslint/naming-convention 15 | const DEFAULT_SEVERITY: Record = { 16 | convention: 'Information', 17 | error: 'Error', 18 | fatal: 'Error', 19 | refactor: 'Hint', 20 | warning: 'Warning', 21 | info: 'Information', 22 | }; 23 | 24 | suite('Settings Tests', () => { 25 | suite('getWorkspaceSettings tests', () => { 26 | let getConfigurationStub: sinon.SinonStub; 27 | let getInterpreterDetailsStub: sinon.SinonStub; 28 | let getWorkspaceFoldersStub: sinon.SinonStub; 29 | let configMock: TypeMoq.IMock; 30 | let pythonConfigMock: TypeMoq.IMock; 31 | let workspace1: WorkspaceFolder = { 32 | uri: Uri.file(path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'testWorkspace', 'workspace1')), 33 | name: 'workspace1', 34 | index: 0, 35 | }; 36 | 37 | setup(() => { 38 | getConfigurationStub = sinon.stub(vscodeapi, 'getConfiguration'); 39 | getInterpreterDetailsStub = sinon.stub(python, 'getInterpreterDetails'); 40 | configMock = TypeMoq.Mock.ofType(); 41 | pythonConfigMock = TypeMoq.Mock.ofType(); 42 | getConfigurationStub.callsFake((namespace: string, uri: Uri) => { 43 | if (namespace.startsWith('pylint')) { 44 | return configMock.object; 45 | } 46 | return pythonConfigMock.object; 47 | }); 48 | getInterpreterDetailsStub.resolves({ path: undefined }); 49 | getWorkspaceFoldersStub = sinon.stub(vscodeapi, 'getWorkspaceFolders'); 50 | getWorkspaceFoldersStub.returns([workspace1]); 51 | }); 52 | 53 | teardown(() => { 54 | sinon.restore(); 55 | }); 56 | 57 | test('Default Settings test', async () => { 58 | configMock 59 | .setup((c) => c.get('args', [])) 60 | .returns(() => []) 61 | .verifiable(TypeMoq.Times.atLeastOnce()); 62 | configMock 63 | .setup((c) => c.get('cwd', TypeMoq.It.isAnyString())) 64 | .returns(() => '${workspaceFolder}') 65 | .verifiable(TypeMoq.Times.atLeastOnce()); 66 | configMock 67 | .setup((c) => c.get('path', [])) 68 | .returns(() => []) 69 | .verifiable(TypeMoq.Times.atLeastOnce()); 70 | configMock 71 | .setup((c) => c.get('severity', DEFAULT_SEVERITY)) 72 | .returns(() => DEFAULT_SEVERITY) 73 | .verifiable(TypeMoq.Times.atLeastOnce()); 74 | configMock 75 | .setup((c) => c.get('importStrategy', 'useBundled')) 76 | .returns(() => 'useBundled') 77 | .verifiable(TypeMoq.Times.atLeastOnce()); 78 | configMock 79 | .setup((c) => c.get('showNotifications', 'off')) 80 | .returns(() => 'off') 81 | .verifiable(TypeMoq.Times.atLeastOnce()); 82 | configMock 83 | .setup((c) => c.get('ignorePatterns', [])) 84 | .returns(() => []) 85 | .verifiable(TypeMoq.Times.atLeastOnce()); 86 | 87 | pythonConfigMock 88 | .setup((c) => c.get('linting.pylintArgs', [])) 89 | .returns(() => []) 90 | .verifiable(TypeMoq.Times.never()); 91 | pythonConfigMock 92 | .setup((c) => c.get('linting.pylintPath', '')) 93 | .returns(() => 'pylint') 94 | .verifiable(TypeMoq.Times.never()); 95 | pythonConfigMock 96 | .setup((c) => c.get('analysis.extraPaths', [])) 97 | .returns(() => []) 98 | .verifiable(TypeMoq.Times.atLeastOnce()); 99 | 100 | const settings: ISettings = await getWorkspaceSettings('pylint', workspace1); 101 | 102 | assert.deepStrictEqual(settings.cwd, workspace1.uri.fsPath); 103 | assert.deepStrictEqual(settings.args, []); 104 | assert.deepStrictEqual(settings.importStrategy, 'useBundled'); 105 | assert.deepStrictEqual(settings.interpreter, []); 106 | assert.deepStrictEqual(settings.path, []); 107 | assert.deepStrictEqual(settings.severity, DEFAULT_SEVERITY); 108 | assert.deepStrictEqual(settings.showNotifications, 'off'); 109 | assert.deepStrictEqual(settings.workspace, workspace1.uri.toString()); 110 | assert.deepStrictEqual(settings.extraPaths, []); 111 | assert.deepStrictEqual(settings.ignorePatterns, []); 112 | 113 | configMock.verifyAll(); 114 | pythonConfigMock.verifyAll(); 115 | }); 116 | 117 | test('Resolver test', async () => { 118 | configMock 119 | .setup((c) => c.get('args', [])) 120 | .returns(() => ['${userHome}', '${workspaceFolder}', '${workspaceFolder:workspace1}', '${cwd}']) 121 | .verifiable(TypeMoq.Times.atLeastOnce()); 122 | configMock 123 | .setup((c) => c.get('cwd', TypeMoq.It.isAnyString())) 124 | .returns(() => '${fileDirname}') 125 | .verifiable(TypeMoq.Times.atLeastOnce()); 126 | configMock 127 | .setup((c) => c.get('path', [])) 128 | .returns(() => [ 129 | '${userHome}/bin/pylint', 130 | '${workspaceFolder}/bin/pylint', 131 | '${workspaceFolder:workspace1}/bin/pylint', 132 | '${cwd}/bin/pylint', 133 | '${interpreter}', 134 | ]) 135 | .verifiable(TypeMoq.Times.atLeastOnce()); 136 | configMock 137 | .setup((c) => c.get('interpreter')) 138 | .returns(() => [ 139 | '${userHome}/bin/python', 140 | '${workspaceFolder}/bin/python', 141 | '${workspaceFolder:workspace1}/bin/python', 142 | '${cwd}/bin/python', 143 | ]) 144 | .verifiable(TypeMoq.Times.atLeastOnce()); 145 | configMock 146 | .setup((c) => c.get('severity', DEFAULT_SEVERITY)) 147 | .returns(() => DEFAULT_SEVERITY) 148 | .verifiable(TypeMoq.Times.atLeastOnce()); 149 | configMock 150 | .setup((c) => c.get('importStrategy', 'useBundled')) 151 | .returns(() => 'useBundled') 152 | .verifiable(TypeMoq.Times.atLeastOnce()); 153 | configMock 154 | .setup((c) => c.get('showNotifications', 'off')) 155 | .returns(() => 'off') 156 | .verifiable(TypeMoq.Times.atLeastOnce()); 157 | configMock 158 | .setup((c) => c.get('ignorePatterns', [])) 159 | .returns(() => []) 160 | .verifiable(TypeMoq.Times.atLeastOnce()); 161 | 162 | pythonConfigMock 163 | .setup((c) => c.get('linting.pylintArgs', [])) 164 | .returns(() => []) 165 | .verifiable(TypeMoq.Times.never()); 166 | pythonConfigMock 167 | .setup((c) => c.get('linting.pylintPath', '')) 168 | .returns(() => 'pylint') 169 | .verifiable(TypeMoq.Times.never()); 170 | pythonConfigMock 171 | .setup((c) => c.get('analysis.extraPaths', [])) 172 | .returns(() => [ 173 | '${userHome}/lib/python', 174 | '${workspaceFolder}/lib/python', 175 | '${workspaceFolder:workspace1}/lib/python', 176 | '${cwd}/lib/python', 177 | ]) 178 | .verifiable(TypeMoq.Times.atLeastOnce()); 179 | pythonConfigMock 180 | .setup((c) => c.get('linting.cwd')) 181 | .returns(() => '${userHome}/bin') 182 | .verifiable(TypeMoq.Times.never()); 183 | 184 | const settings: ISettings = await getWorkspaceSettings('pylint', workspace1, true); 185 | 186 | assert.deepStrictEqual(settings.cwd, '${fileDirname}'); 187 | assert.deepStrictEqual(settings.args, [ 188 | process.env.HOME || process.env.USERPROFILE, 189 | workspace1.uri.fsPath, 190 | workspace1.uri.fsPath, 191 | process.cwd(), 192 | ]); 193 | assert.deepStrictEqual(settings.path, [ 194 | `${process.env.HOME || process.env.USERPROFILE}/bin/pylint`, 195 | `${workspace1.uri.fsPath}/bin/pylint`, 196 | `${workspace1.uri.fsPath}/bin/pylint`, 197 | `${process.cwd()}/bin/pylint`, 198 | `${process.env.HOME || process.env.USERPROFILE}/bin/python`, 199 | `${workspace1.uri.fsPath}/bin/python`, 200 | `${workspace1.uri.fsPath}/bin/python`, 201 | `${process.cwd()}/bin/python`, 202 | ]); 203 | assert.deepStrictEqual(settings.interpreter, [ 204 | `${process.env.HOME || process.env.USERPROFILE}/bin/python`, 205 | `${workspace1.uri.fsPath}/bin/python`, 206 | `${workspace1.uri.fsPath}/bin/python`, 207 | `${process.cwd()}/bin/python`, 208 | ]); 209 | assert.deepStrictEqual(settings.extraPaths, [ 210 | `${process.env.HOME || process.env.USERPROFILE}/lib/python`, 211 | `${workspace1.uri.fsPath}/lib/python`, 212 | `${workspace1.uri.fsPath}/lib/python`, 213 | `${process.cwd()}/lib/python`, 214 | ]); 215 | 216 | configMock.verifyAll(); 217 | pythonConfigMock.verifyAll(); 218 | }); 219 | 220 | test('Legacy Settings test', async () => { 221 | configMock 222 | .setup((c) => c.get('args', [])) 223 | .returns(() => []) 224 | .verifiable(TypeMoq.Times.atLeastOnce()); 225 | configMock 226 | .setup((c) => c.get('cwd', TypeMoq.It.isAnyString())) 227 | .returns(() => '${userHome}/bin') 228 | .verifiable(TypeMoq.Times.atLeastOnce()); 229 | configMock 230 | .setup((c) => c.get('path', [])) 231 | .returns(() => []) 232 | .verifiable(TypeMoq.Times.atLeastOnce()); 233 | configMock 234 | .setup((c) => c.get('severity', DEFAULT_SEVERITY)) 235 | .returns(() => DEFAULT_SEVERITY) 236 | .verifiable(TypeMoq.Times.atLeastOnce()); 237 | configMock 238 | .setup((c) => c.get('importStrategy', 'useBundled')) 239 | .returns(() => 'useBundled') 240 | .verifiable(TypeMoq.Times.atLeastOnce()); 241 | configMock 242 | .setup((c) => c.get('showNotifications', 'off')) 243 | .returns(() => 'off') 244 | .verifiable(TypeMoq.Times.atLeastOnce()); 245 | configMock 246 | .setup((c) => c.get('ignorePatterns', [])) 247 | .returns(() => []) 248 | .verifiable(TypeMoq.Times.atLeastOnce()); 249 | 250 | pythonConfigMock 251 | .setup((c) => c.get('linting.pylintArgs', [])) 252 | .returns(() => ['${userHome}', '${workspaceFolder}', '${workspaceFolder:workspace1}', '${cwd}']) 253 | .verifiable(TypeMoq.Times.never()); 254 | pythonConfigMock 255 | .setup((c) => c.get('linting.pylintPath', '')) 256 | .returns(() => '${userHome}/bin/pylint') 257 | .verifiable(TypeMoq.Times.never()); 258 | pythonConfigMock 259 | .setup((c) => c.get('analysis.extraPaths', [])) 260 | .returns(() => [ 261 | '${userHome}/lib/python', 262 | '${workspaceFolder}/lib/python', 263 | '${workspaceFolder:workspace1}/lib/python', 264 | '${cwd}/lib/python', 265 | '~/lib/python', 266 | '/usr/~projects', 267 | '~projects', 268 | ]) 269 | .verifiable(TypeMoq.Times.atLeastOnce()); 270 | pythonConfigMock 271 | .setup((c) => c.get('linting.cwd')) 272 | .returns(() => '${userHome}/bin2') 273 | .verifiable(TypeMoq.Times.never()); 274 | 275 | const settings: ISettings = await getWorkspaceSettings('pylint', workspace1); 276 | 277 | assert.deepStrictEqual(settings.cwd, `${process.env.HOME || process.env.USERPROFILE}/bin`); 278 | // Legacy args should not be read anymore. They are deprecated. 279 | assert.deepStrictEqual(settings.args, []); 280 | assert.deepStrictEqual(settings.importStrategy, 'useBundled'); 281 | assert.deepStrictEqual(settings.interpreter, []); 282 | // Legacy args should not be read anymore. They are deprecated. 283 | assert.deepStrictEqual(settings.path, []); 284 | assert.deepStrictEqual(settings.severity, DEFAULT_SEVERITY); 285 | assert.deepStrictEqual(settings.showNotifications, 'off'); 286 | assert.deepStrictEqual(settings.workspace, workspace1.uri.toString()); 287 | assert.deepStrictEqual(settings.extraPaths, [ 288 | `${process.env.HOME || process.env.USERPROFILE}/lib/python`, 289 | `${workspace1.uri.fsPath}/lib/python`, 290 | `${workspace1.uri.fsPath}/lib/python`, 291 | `${process.cwd()}/lib/python`, 292 | `${process.env.HOME || process.env.USERPROFILE}/lib/python`, 293 | '/usr/~projects', 294 | '~projects', 295 | ]); 296 | 297 | configMock.verifyAll(); 298 | pythonConfigMock.verifyAll(); 299 | }); 300 | 301 | [true, false].forEach((value) => { 302 | test(`Lint on change settings: ${value}`, async () => { 303 | configMock 304 | .setup((c) => c.get('lintOnChange', false)) 305 | .returns(() => value) 306 | .verifiable(TypeMoq.Times.atLeastOnce()); 307 | assert.deepStrictEqual(isLintOnChangeEnabled('pylint'), value); 308 | }); 309 | }); 310 | }); 311 | }); 312 | -------------------------------------------------------------------------------- /src/test/ts_tests/tests/common/utilities.unit.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import { assert } from 'chai'; 5 | import * as sinon from 'sinon'; 6 | import * as vscodeapi from '../../../../common/vscodeapi'; 7 | import { getDocumentSelector } from '../../../../common/utilities'; 8 | 9 | suite('Document Selector Tests', () => { 10 | let isVirtualWorkspaceStub: sinon.SinonStub; 11 | setup(() => { 12 | isVirtualWorkspaceStub = sinon.stub(vscodeapi, 'isVirtualWorkspace'); 13 | isVirtualWorkspaceStub.returns(false); 14 | }); 15 | teardown(() => { 16 | sinon.restore(); 17 | }); 18 | 19 | test('Document selector default', () => { 20 | const selector = getDocumentSelector(); 21 | assert.deepStrictEqual(selector, [ 22 | { scheme: 'file', language: 'python' }, 23 | { scheme: 'untitled', language: 'python' }, 24 | { scheme: 'vscode-notebook', language: 'python' }, 25 | { scheme: 'vscode-notebook-cell', language: 'python' }, 26 | ]); 27 | }); 28 | test('Document selector virtual workspace', () => { 29 | isVirtualWorkspaceStub.returns(true); 30 | const selector = getDocumentSelector(); 31 | assert.deepStrictEqual(selector, [{ language: 'python' }]); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "module": "commonjs", 5 | "target": "ES2020", 6 | "lib": ["ES2020"], 7 | "sourceMap": true, 8 | "rootDir": "src", 9 | "strict": true /* enable all strict type-checking options */ 10 | /* Additional Checks */ 11 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 12 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 13 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | //@ts-check 8 | /** @typedef {import('webpack').Configuration} WebpackConfig **/ 9 | 10 | const loaders = []; 11 | loaders.push({ 12 | test: /\.ts$/, 13 | exclude: /node_modules/, 14 | use: [ 15 | { 16 | loader: 'ts-loader', 17 | }, 18 | ], 19 | }); 20 | 21 | /** @type WebpackConfig */ 22 | const extensionConfig = { 23 | target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 24 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 25 | 26 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 27 | output: { 28 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 29 | path: path.resolve(__dirname, 'dist'), 30 | filename: 'extension.js', 31 | libraryTarget: 'commonjs2', 32 | }, 33 | externals: { 34 | vscode: 'commonjs vscode', // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 35 | // modules added here also need to be added in the .vscodeignore file 36 | }, 37 | resolve: { 38 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 39 | extensions: ['.ts', '.js'], 40 | }, 41 | module: { 42 | rules: loaders, 43 | }, 44 | devtool: 'source-map', 45 | infrastructureLogging: { 46 | level: 'log', // enables logging required for problem matchers 47 | }, 48 | }; 49 | module.exports = [extensionConfig]; 50 | --------------------------------------------------------------------------------