├── .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 |
--------------------------------------------------------------------------------