diff --git a/.github/workflows/positron-python-ci.yml b/.github/workflows/positron-python-ci.yml index d66abcb266d2..498137d59a64 100644 --- a/.github/workflows/positron-python-ci.yml +++ b/.github/workflows/positron-python-ci.yml @@ -15,11 +15,13 @@ on: defaults: run: - working-directory: './extensions/positron-python' + working-directory: 'extensions/positron-python' env: NODE_VERSION: '18.17.1' PYTHON_VERSION: '3.10' + PROJECT_DIR: 'extensions/positron-python' + PYTHON_SRC_DIR: 'extensions/positron-python/pythonFiles' # Force a path with spaces and to test extension works in these scenarios # Unicode characters are causing 2.7 failures so skip that for now. special-working-directory: './path with spaces' @@ -67,13 +69,13 @@ jobs: run: | python -m pip install -U black python -m black . --check - working-directory: pythonFiles + working-directory: ${{ env.PYTHON_SRC_DIR }} - name: Run Ruff run: | python -m pip install -U ruff python -m ruff check . - working-directory: pythonFiles + working-directory: ${{ env.PYTHON_SRC_DIR }} check-types: name: Check Python types @@ -89,9 +91,7 @@ jobs: cache: 'pip' - name: Install base Python requirements - uses: brettcannon/pip-secure-install@v1 - with: - options: '-t ./pythonFiles/lib/python --no-cache-dir --implementation py' + run: 'python -m pip install --no-deps --require-hashes --only-binary :all: -t ./pythonFiles/lib/python --no-cache-dir --implementation py -r requirements.txt' - name: Install Positron IPyKernel requirements run: python scripts/vendor.py @@ -106,7 +106,7 @@ jobs: uses: jakebailey/pyright-action@v2 with: version: 1.1.308 - working-directory: 'pythonFiles' + working-directory: ${{ env.PYTHON_SRC_DIR }} python-tests: name: Python Tests @@ -114,7 +114,7 @@ jobs: runs-on: ${{ matrix.os }} defaults: run: - working-directory: ${{ env.special-working-directory }} + working-directory: ${{ env.special-working-directory }}/${{ env.PROJECT_DIR}} strategy: fail-fast: false matrix: @@ -167,10 +167,7 @@ jobs: run: python -m pytest --version - name: Install base Python requirements - uses: brettcannon/pip-secure-install@v1 - with: - requirements-file: '"${{ env.special-working-directory-relative }}/requirements.txt"' - options: '-t "${{ env.special-working-directory-relative }}/pythonFiles/lib/python" --no-cache-dir --implementation py' + run: 'python -m pip install --no-deps --require-hashes --only-binary :all: -t ./pythonFiles/lib/python --no-cache-dir --implementation py -r requirements.txt' - name: Install test requirements run: python -m pip install -r build/test-requirements.txt @@ -248,7 +245,7 @@ jobs: runs-on: ${{ matrix.os }} defaults: run: - working-directory: ${{ env.special-working-directory }} + working-directory: ${{ env.special-working-directory }}/${{ env.PROJECT_DIR}} strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] @@ -285,7 +282,7 @@ jobs: with: node-version: ${{ env.NODE_VERSION }} cache: 'yarn' - cache-dependency-path: ${{ env.special-working-directory-relative }}/yarn.lock + cache-dependency-path: ${{ env.special-working-directory-relative }}/${{ env.PROJECT_DIR }}/yarn.lock - name: Install Yarn run: npm install -g yarn @@ -406,31 +403,31 @@ jobs: env: TEST_FILES_SUFFIX: testvirtualenvs CI_PYTHON_VERSION: ${{ matrix.python }} - POSITRON_GITHUB_PAT: ${{ secrets.POSITRON_GITHUB_RO_PAT }} + POSITRON_GITHUB_PAT: ${{ secrets.POSITRON_GITHUB_PAT }} uses: GabrielBB/xvfb-action@v1.6 with: run: yarn testSingleWorkspace - working-directory: ${{ env.special-working-directory }} + working-directory: ${{ env.special-working-directory }}/${{ env.PROJECT_DIR }} if: matrix.test-suite == 'venv' - name: Run single-workspace tests env: CI_PYTHON_VERSION: ${{ matrix.python }} - POSITRON_GITHUB_PAT: ${{ secrets.POSITRON_GITHUB_RO_PAT }} + POSITRON_GITHUB_PAT: ${{ secrets.POSITRON_GITHUB_PAT }} uses: GabrielBB/xvfb-action@v1.6 with: run: yarn testSingleWorkspace - working-directory: ${{ env.special-working-directory }} + working-directory: ${{ env.special-working-directory }}/${{ env.PROJECT_DIR }} if: matrix.test-suite == 'single-workspace' - name: Run debugger tests env: CI_PYTHON_VERSION: ${{ matrix.python }} - POSITRON_GITHUB_PAT: ${{ secrets.POSITRON_GITHUB_RO_PAT }} + POSITRON_GITHUB_PAT: ${{ secrets.POSITRON_GITHUB_PAT }} uses: GabrielBB/xvfb-action@v1.6 with: run: yarn testDebugger - working-directory: ${{ env.special-working-directory }} + working-directory: ${{ env.special-working-directory }}/${{ env.PROJECT_DIR }} if: matrix.test-suite == 'debugger' - name: Run TypeScript functional tests @@ -439,6 +436,6 @@ jobs: - name: Run smoke tests env: - POSITRON_GITHUB_PAT: ${{ secrets.POSITRON_GITHUB_RO_PAT }} + POSITRON_GITHUB_PAT: ${{ secrets.POSITRON_GITHUB_PAT }} run: yarn tsc && node ./out/test/smokeTest.js if: matrix.test-suite == 'smoke' diff --git a/.vscode/settings.json b/.vscode/settings.json index d31c1786e5b4..0f62a0752e2a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -122,6 +122,9 @@ "editor.defaultFormatter": "rust-lang.rust-analyzer", "editor.formatOnSave": true, }, + "[diff]": { + "files.trimTrailingWhitespace": false + }, "rust-analyzer.linkedProjects": [ "cli/Cargo.toml" ], diff --git a/extensions/positron-python/scripts/patches/jedi-language-server.patch b/extensions/positron-python/scripts/patches/jedi-language-server.patch index b9527ce3af52..948c9638341e 100644 --- a/extensions/positron-python/scripts/patches/jedi-language-server.patch +++ b/extensions/positron-python/scripts/patches/jedi-language-server.patch @@ -4,7 +4,7 @@ index 4f3ba7bd5..52524daa2 100644 +++ b/extensions/positron-python/pythonFiles/positron/positron_ipykernel/_vendor/jedi_language_server/jedi_utils.py @@ -12,9 +12,9 @@ from inspect import Parameter from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union - + import docstring_to_markdown -import jedi.api.errors -import jedi.inference.references @@ -27,7 +27,7 @@ index 4f3ba7bd5..52524daa2 100644 + initialization_options.jedi_settings.auto_import_modules ) ) - + - jedi.settings.case_insensitive_completion = ( + settings.case_insensitive_completion = ( initialization_options.jedi_settings.case_insensitive_completion @@ -35,8 +35,8 @@ index 4f3ba7bd5..52524daa2 100644 if initialization_options.jedi_settings.debug: @@ -284,7 +284,7 @@ def lsp_document_symbols(names: List[Name]) -> List[DocumentSymbol]: return results - - + + -def lsp_diagnostic(error: jedi.api.errors.SyntaxError) -> Diagnostic: +def lsp_diagnostic(error: JediSyntaxError) -> Diagnostic: """Get LSP Diagnostic from Jedi SyntaxError.""" @@ -49,6 +49,6 @@ index ba6eaf9fe..28266bd95 100644 @@ -1,4 +1,3 @@ """Jedi Language Server.""" -from importlib.metadata import version - + -__version__ = version("jedi-language-server") +__version__ = "unknown" diff --git a/extensions/positron-python/scripts/patches/jedi.patch b/extensions/positron-python/scripts/patches/jedi.patch index 128d18db5f13..eabb923f86a3 100644 --- a/extensions/positron-python/scripts/patches/jedi.patch +++ b/extensions/positron-python/scripts/patches/jedi.patch @@ -4,19 +4,19 @@ index e0f23d19b..f30731476 100644 +++ b/extensions/positron-python/pythonFiles/positron/positron_ipykernel/_vendor/jedi/api/replstartup.py @@ -17,13 +17,13 @@ Then you will be able to use Jedi completer in your Python interpreter:: ..dex ..sert - + """ -import jedi.utils +from jedi.utils import setup_readline from jedi import __version__ as __jedi_version__ - + print('REPL completion using Jedi %s' % __jedi_version__) -jedi.utils.setup_readline(fuzzy=False) +setup_readline(fuzzy=False) - + -del jedi +del setup_readline - + # Note: try not to do many things here, as it will contaminate global # namespace of the interpreter. diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/_vendor/jedi/inference/compiled/subprocess/__main__.py b/extensions/positron-python/pythonFiles/positron/positron_ipykernel/_vendor/jedi/inference/compiled/subprocess/__main__.py @@ -24,8 +24,8 @@ index f044e2ee1..beec3f0cf 100644 --- a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/_vendor/jedi/inference/compiled/subprocess/__main__.py +++ b/extensions/positron-python/pythonFiles/positron/positron_ipykernel/_vendor/jedi/inference/compiled/subprocess/__main__.py @@ -9,12 +9,9 @@ del sys.path[0] - - + + def _get_paths(): - # Get the path to jedi. + # Get the path to positron, in which jedi and parso are vendored. @@ -35,6 +35,6 @@ index f044e2ee1..beec3f0cf 100644 - # The paths are the directory that jedi and parso lie in. - return {'jedi': _jedi_path, 'parso': _parso_path} + return {"positron_ipykernel": _d(_d(_d(_d(_d(_d(_d(__file__)))))))} - - + + class _ExactImporter(MetaPathFinder): diff --git a/extensions/positron-python/scripts/patches/parso.patch b/extensions/positron-python/scripts/patches/parso.patch index 4e443e95e8e7..3121436e9357 100644 --- a/extensions/positron-python/scripts/patches/parso.patch +++ b/extensions/positron-python/scripts/patches/parso.patch @@ -9,7 +9,7 @@ index 5592a9fdd..98b903aaa 100644 @@ -49,7 +49,7 @@ are regarded as incompatible. - A __slot__ of a class is changed. """ - + -_VERSION_TAG = '%s-%s%s-%s' % ( +_VERSION_TAG = '%s-%s%s-%s-Positron' % ( platform.python_implementation(), diff --git a/extensions/positron-python/scripts/patches/pydantic.patch b/extensions/positron-python/scripts/patches/pydantic.patch index 567e86171898..4302de1b0428 100644 --- a/extensions/positron-python/scripts/patches/pydantic.patch +++ b/extensions/positron-python/scripts/patches/pydantic.patch @@ -4,13 +4,13 @@ index 0c529620f..ce9df7a36 100644 +++ b/extensions/positron-python/pythonFiles/positron/positron_ipykernel/_vendor/pydantic/_hypothesis_plugin.py @@ -33,7 +33,7 @@ from typing import Callable, Dict, Type, Union, cast, overload import hypothesis.strategies as st - + import pydantic -import pydantic.color +import pydantic.color as pydantic_color import pydantic.types from pydantic.utils import lenient_issubclass - + @@ -88,23 +88,23 @@ st.register_type_strategy( _color_regexes = ( '|'.join( @@ -51,29 +51,29 @@ index ce9df7a36..93ff17077 100644 --- a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/_vendor/pydantic/_hypothesis_plugin.py +++ b/extensions/positron-python/pythonFiles/positron/positron_ipykernel/_vendor/pydantic/_hypothesis_plugin.py @@ -34,7 +34,7 @@ import hypothesis.strategies as st - + import pydantic import pydantic.color as pydantic_color -import pydantic.types +import pydantic.types as pydantic_types from pydantic.utils import lenient_issubclass - + # FilePath and DirectoryPath are explicitly unsupported, as we'd have to create @@ -182,22 +182,22 @@ RESOLVERS: Dict[type, Callable[[type], st.SearchStrategy]] = {} # type: ignore[ - - + + @overload -def _registered(typ: Type[pydantic.types.T]) -> Type[pydantic.types.T]: +def _registered(typ: Type[pydantic_types.T]) -> Type[pydantic.types.T]: pass - - + + @overload -def _registered(typ: pydantic.types.ConstrainedNumberMeta) -> pydantic.types.ConstrainedNumberMeta: +def _registered(typ: pydantic_types.ConstrainedNumberMeta) -> pydantic.types.ConstrainedNumberMeta: pass - - + + def _registered( - typ: Union[Type[pydantic.types.T], pydantic.types.ConstrainedNumberMeta] -) -> Union[Type[pydantic.types.T], pydantic.types.ConstrainedNumberMeta]: @@ -89,8 +89,8 @@ index ce9df7a36..93ff17077 100644 if issubclass(typ, supertype): st.register_type_strategy(typ, resolver(typ)) # type: ignore @@ -206,7 +206,7 @@ def _registered( - - + + def resolves( - typ: Union[type, pydantic.types.ConstrainedNumberMeta] + typ: Union[type, pydantic_types.ConstrainedNumberMeta] @@ -98,8 +98,8 @@ index ce9df7a36..93ff17077 100644 def inner(f): # type: ignore assert f not in RESOLVERS @@ -385,7 +385,7 @@ def resolve_constr(cls): # type: ignore[no-untyped-def] # pragma: no cover - - + + # Finally, register all previously-defined types, and patch in our new function -for typ in list(pydantic.types._DEFINED_TYPES): +for typ in list(pydantic_types._DEFINED_TYPES): diff --git a/extensions/positron-python/scripts/patches/pygments.patch b/extensions/positron-python/scripts/patches/pygments.patch index eed86d1d782e..5e4968342f4f 100644 --- a/extensions/positron-python/scripts/patches/pygments.patch +++ b/extensions/positron-python/scripts/patches/pygments.patch @@ -14,7 +14,7 @@ index 435231e65..b75a9d7f4 100644 - outfile = colorama.initialise.wrap_stream( + outfile = colorama_initialise.wrap_stream( outfile, convert=None, strip=None, autoreset=False, wrap=True) - + # When using the LaTeX formatter and the option `escapeinside` is diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/_vendor/pygments/__main__.py b/extensions/positron-python/pythonFiles/positron/positron_ipykernel/_vendor/pygments/__main__.py index 5eb2c747a..b8b833ec4 100644 @@ -22,11 +22,11 @@ index 5eb2c747a..b8b833ec4 100644 +++ b/extensions/positron-python/pythonFiles/positron/positron_ipykernel/_vendor/pygments/__main__.py @@ -9,9 +9,9 @@ """ - + import sys -import pygments.cmdline +from pygments.cmdline import main - + try: - sys.exit(pygments.cmdline.main(sys.argv)) + sys.exit(main(sys.argv)) @@ -43,14 +43,14 @@ index f935688f1..4c293489f 100644 - import pygments.lexers + from pygments.lexers import find_lexer_class out = [] - + table = [] @@ -102,7 +102,7 @@ class PygmentsDoc(Directive): return name - + for classname, data in sorted(LEXERS.items(), key=lambda x: x[1][1].lower()): - lexer_cls = pygments.lexers.find_lexer_class(data[1]) + lexer_cls = find_lexer_class(data[1]) extensions = lexer_cls.filenames + lexer_cls.alias_filenames - + table.append({ diff --git a/extensions/positron-python/src/test/positron/constants.ts b/extensions/positron-python/src/test/positron/constants.ts new file mode 100644 index 000000000000..ca006eef4b2b --- /dev/null +++ b/extensions/positron-python/src/test/positron/constants.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import * as fs from 'fs-extra'; +import * as os from 'os'; +import * as path from 'path'; + +/** + * Get the path to the user data directory for tests. + * + * Uses the environment variable POSITRON_USER_DATA_DIR if set, otherwise creates a new temporary + * directory and sets the environment variable. + * + * @returns The path to the user data directory. + */ +export async function getUserDataDir(): Promise { + if (!process.env.POSITRON_USER_DATA_DIR) { + process.env.POSITRON_USER_DATA_DIR = await fs.mkdtemp(path.join(os.tmpdir(), 'positron-')); + } + return process.env.POSITRON_USER_DATA_DIR; +} diff --git a/extensions/positron-python/src/test/positron/testElectron.ts b/extensions/positron-python/src/test/positron/testElectron.ts index e5b929b92ba1..da47896571a1 100644 --- a/extensions/positron-python/src/test/positron/testElectron.ts +++ b/extensions/positron-python/src/test/positron/testElectron.ts @@ -11,6 +11,7 @@ import * as path from 'path'; import { defaultCachePath } from '@vscode/test-electron/out/download'; import { TestOptions } from '@vscode/test-electron/out/runTest'; import { runTests as vscodeRunTests } from '@vscode/test-electron'; +import { getUserDataDir } from './constants'; const rmrf = require('rimraf'); @@ -341,9 +342,16 @@ export async function downloadAndUnzipPositron(): Promise<{ version: string; exe } /** - * Wrap `@vscode/test-electron/runTests` to download and use the latest Positron release. + * Wrap `@vscode/test-electron/runTests` to support Positron. */ export async function runTests(options: TestOptions): Promise { const { version, executablePath: vscodeExecutablePath } = await downloadAndUnzipPositron(); + + // Run tests with a temporary user data dir to ensure no leftover state. + // This is also necessary for upstream debugger tests on CI since otherwise the debugger tests + // fail due to the path being too long. + options.launchArgs = options.launchArgs || []; + options.launchArgs.push('--user-data-dir', await getUserDataDir()); + return vscodeRunTests({ version, vscodeExecutablePath, ...options }); } diff --git a/extensions/positron-python/src/test/smokeTest.ts b/extensions/positron-python/src/test/smokeTest.ts index 47fd24992cc0..15aa2d04de1d 100644 --- a/extensions/positron-python/src/test/smokeTest.ts +++ b/extensions/positron-python/src/test/smokeTest.ts @@ -13,6 +13,10 @@ import * as path from 'path'; import { unzip } from './common'; import { EXTENSION_ROOT_DIR_FOR_TESTS, SMOKE_TEST_EXTENSIONS_DIR } from './constants'; +// --- Start Positron --- +import { getUserDataDir } from './positron/constants'; +// --- End Positron --- + class TestRunner { public async start() { console.log('Start Test Runner'); @@ -55,7 +59,7 @@ class TestRunner { } // --- Start Positron --- private async enableExperiments() { - const settingsFile = path.join('.vscode-test', 'user-data', 'User', 'settings.json'); + const settingsFile = path.join(await getUserDataDir(), 'User', 'settings.json'); await fs.mkdir(path.dirname(settingsFile), { recursive: true }); diff --git a/extensions/positron-python/src/test/standardTest.ts b/extensions/positron-python/src/test/standardTest.ts index beb840917c3e..9aa552b62b6c 100644 --- a/extensions/positron-python/src/test/standardTest.ts +++ b/extensions/positron-python/src/test/standardTest.ts @@ -8,6 +8,10 @@ import { EXTENSION_ROOT_DIR_FOR_TESTS } from './constants'; import { downloadAndUnzipPositron } from './positron/testElectron'; import { TestOptions } from '@vscode/test-electron/out/runTest'; +// --- Start Positron --- +import { getUserDataDir } from './positron/constants'; +// --- End Positron --- + // If running smoke tests, we don't have access to this. if (process.env.TEST_FILES_SUFFIX !== 'smoke.test') { const logger = require('./testLogger'); @@ -91,6 +95,12 @@ async function start() { .concat([workspacePath]) .concat(channel === 'insiders' ? ['--enable-proposed-api'] : []) .concat(['--timeout', '5000']); + // --- Start Positron --- + // Run tests with a temporary user data dir to ensure no leftover state. + // This is also necessary for upstream debugger tests on CI since otherwise the debugger tests + // fail due to the path being too long. + launchArgs.push('--user-data-dir', await getUserDataDir()); + // --- End Positron --- console.log(`Starting vscode ${channel} with args ${launchArgs.join(' ')}`); const options: TestOptions = { // --- Start Positron ---