From f374a0096a03f73a804305e19753540e9f1add18 Mon Sep 17 00:00:00 2001 From: Ilya Tumaykin Date: Wed, 4 Nov 2026 23:18:08 +0300 Subject: [PATCH 1/6] Fix overlong string --- autoload/virtualenv.vim | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/autoload/virtualenv.vim b/autoload/virtualenv.vim index c265330..533a5aa 100644 --- a/autoload/virtualenv.vim +++ b/autoload/virtualenv.vim @@ -282,7 +282,8 @@ function! s:get_env_type(target) return filereadable(s:join_path(a:target, 'uv.lock')) ? 'uv' : '.venv' elseif filereadable(s:join_path(a:target, 'venv/.tox-info.json')) return '.tox' - elseif filereadable(s:join_path(a:target, '.python-version')) && exists('$PYENV_ROOT') + elseif filereadable(s:join_path(a:target, '.python-version')) && + \ exists('$PYENV_ROOT') return 'pyenv' elseif filereadable(s:join_path(a:target, 'bin/activate_this.py')) return filereadable(s:join_path(a:target, 'bin/get_env_details')) From 7dd503ce492649941eeed12f61fcb5a65fd24ad0 Mon Sep 17 00:00:00 2001 From: Ilya Tumaykin Date: Wed, 4 Nov 2026 22:53:28 +0300 Subject: [PATCH 2/6] Use Python interface lazily Vim with dynamic Python binds to the Python library on the first Python command. Delay Python calls as much as possible to help with binding to the proper version. --- autoload/virtualenv.vim | 44 +++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/autoload/virtualenv.vim b/autoload/virtualenv.vim index 533a5aa..7017c27 100644 --- a/autoload/virtualenv.vim +++ b/autoload/virtualenv.vim @@ -1,22 +1,8 @@ function! virtualenv#init() let s:state = {} - if has('python3_stable') - let s:vim_major = and(v:python3_version >> 24, 0xff) - let s:vim_minor = and(v:python3_version >> 16, 0xff) - else - let s:vim_major = py3eval('sys.version_info.major') - let s:vim_minor = py3eval('sys.version_info.minor') - endif - let s:custom_project_finder = 'virtualenv#gutentags_project_root_finder' - try - execute 'py3file' fnameescape(g:virtualenv#python_script) - catch - return s:error('failed to load Python virtual environment manager') - endtry - if !empty(g:virtualenv#directory) let g:virtualenv#directory = \ s:normalize_path(fnamemodify(g:virtualenv#directory, ':p')) @@ -125,7 +111,7 @@ function! virtualenv#force_activate(target, ...) let [l:major, l:minor] = l:pyversion call s:error('Python version mismatch') call s:error('Environment version: '.l:major.'.'.l:minor) - call s:error('Vim version: '.s:vim_major.'.'.s:vim_minor) + call s:error('Vim version: '.s:python.major.'.'.s:python.minor) return s:error(l:target.' is not supported') endif @@ -326,11 +312,25 @@ function! s:get_pyversion_external(...) endfunction function! s:is_python_supported(pyversion) + if !exists('s:python') + if has('python3_stable') + let s:python = { + \ 'major': and(v:python3_version >> 24, 0xff), + \ 'minor': and(v:python3_version >> 16, 0xff), + \ } + else + let s:python = { + \ 'major': py3eval('sys.version_info.major'), + \ 'minor': py3eval('sys.version_info.minor'), + \ } + endif + endif + let [l:major, l:minor] = a:pyversion if has('python3_stable') - return (l:major >= s:vim_major) && (l:minor >= s:vim_minor) + return (l:major >= s:python.major) && (l:minor >= s:python.minor) else - return (l:major == s:vim_major) && (l:minor == s:vim_minor) + return (l:major == s:python.major) && (l:minor == s:python.minor) endif endfunction @@ -400,6 +400,16 @@ function! s:execute_system_python_command(command) endfunction function! s:execute_python_command(command, ...) + if !exists('s:python_loaded') + try + execute 'py3file' fnameescape(g:virtualenv#python_script) + catch + throw 'failed to load Python virtual environment manager: '. + \ v:exception + endtry + let s:python_loaded = 1 + endif + let l:command = a:command.((a:0) ? s:build_arguments(a:000) : '') return split(execute('python3 '.l:command), '\n') endfunction From 5db15e69d6b6a9cfe091893fe24f1abec3a0524c Mon Sep 17 00:00:00 2001 From: Ilya Tumaykin Date: Thu, 5 Nov 2026 01:45:12 +0300 Subject: [PATCH 3/6] Avoid loading Python library for feature check --- plugin/virtualenv.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/virtualenv.vim b/plugin/virtualenv.vim index 61951c1..b51cf93 100644 --- a/plugin/virtualenv.vim +++ b/plugin/virtualenv.vim @@ -2,7 +2,7 @@ if exists('g:loaded_virtualenv') finish endif -if !has('python3') +if !has('python3_compiled') && !has('python3') echoerr 'vim-virtualenv requires python3 feature to be enabled' finish endif From fef4d16ae8975b85a698aaceaf53789dd9d19685 Mon Sep 17 00:00:00 2001 From: Ilya Tumaykin Date: Thu, 5 Nov 2026 00:26:42 +0300 Subject: [PATCH 4/6] Set 'pythonthreedll' upon activation if available On activation the following sequence happens: 1. Find the virtual environment directory: no Python calls 2. Determine the virtual environment Python version: no Python calls 3. Determine the Vim Python version: no Python calls with dyn-stable, some Python calls otherwise 4. Change 'pythonthreedll' to match the virtual environment Python Vim and Neovim can be built against a specific Python version. Vim exclusively can support a range of Python versions with dyn-stable feature. With dyn-stable Vim binds to the default Python version up the first Python call. 'pythonthreedll' option can make Vim bind to a specific version instead. Changing it after the bind does nothing. Since step 3 involves Python calls on non dyn-stable setups, Vim will bind to the default Python version as required. Binding to another version will probably lead to crashes and problems. Since step 3 involves no Python calls on dyn-stable setups, Vim can bind to the virtual environment Python version. This will prevent C extensions ABI mismatch, which cannot be mixed between versions. --- autoload/virtualenv.vim | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/autoload/virtualenv.vim b/autoload/virtualenv.vim index 7017c27..b995a1a 100644 --- a/autoload/virtualenv.vim +++ b/autoload/virtualenv.vim @@ -115,6 +115,12 @@ function! virtualenv#force_activate(target, ...) return s:error(l:target.' is not supported') endif + if exists('+pythonthreedll') + let [&pythonthreedll] = s:execute_system_python_command( + \ 'import sysconfig; print(sysconfig.get_config_var("INSTSONAME"))', + \ l:target) + endif + let s:state['virtualenv_type'] = l:env_type let s:state['virtualenv_internal'] = l:internal let s:state['virtualenv_python'] = join(l:pyversion, '.') @@ -395,8 +401,9 @@ function! s:normalize_path(path) endfunction " python machinery -function! s:execute_system_python_command(command) - return systemlist("python -B -c 'import sys; ".a:command."'") +function! s:execute_system_python_command(command, root=v:null) + let l:prefix = a:root is v:null ? '' : s:join_path(a:root, 'bin/') + return systemlist(l:prefix."python -B -c 'import sys; ".a:command."'") endfunction function! s:execute_python_command(command, ...) From 3163cef7d563afcccccad591cd187a51e460f677 Mon Sep 17 00:00:00 2001 From: Ilya Tumaykin Date: Thu, 5 Nov 2026 01:16:17 +0300 Subject: [PATCH 5/6] Update CI/CD to latest Ubuntu --- .github/workflows/{test.yml => activation.yml} | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename .github/workflows/{test.yml => activation.yml} (93%) diff --git a/.github/workflows/test.yml b/.github/workflows/activation.yml similarity index 93% rename from .github/workflows/test.yml rename to .github/workflows/activation.yml index 39d0963..c06732a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/activation.yml @@ -25,7 +25,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v7 with: - python-version: "3.12" # Vim on Ubuntu supports only 3.12 + python-version: "3.14" # Vim on Ubuntu supports only 3.14 - name: Set up tools run: | @@ -66,4 +66,5 @@ jobs: -c "call assert_equal('${{ matrix.tool }}', virtualenv#state('virtualenv_type'))" \ -c "call assert_equal(${MATRIX_KIND_CODE}, virtualenv#state('virtualenv_internal'))" \ -c "call assert_equal('${VIM_VIRTUALENV_TARGET}', virtualenv#state('virtualenv_directory'))" \ + -c "call assert_equal('${VIM_VIRTUALENV_TARGET}', py3eval('sys.prefix'))" \ -c "call g:ExitTest()" From b0a82aa39a57ee8d0957c1eebabc8b6ad1a33fc1 Mon Sep 17 00:00:00 2001 From: Ilya Tumaykin Date: Thu, 5 Nov 2026 02:10:13 +0300 Subject: [PATCH 6/6] Add CI/CD coverage for mixed Python versions --- .github/workflows/dyn-stable.yml | 73 ++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 .github/workflows/dyn-stable.yml diff --git a/.github/workflows/dyn-stable.yml b/.github/workflows/dyn-stable.yml new file mode 100644 index 0000000..2e8c1bd --- /dev/null +++ b/.github/workflows/dyn-stable.yml @@ -0,0 +1,73 @@ +name: Test dynamic stable ABI virtual environment activation + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + test: + runs-on: ubuntu-latest + container: fedora:45 + timeout-minutes: 10 + + defaults: + run: + shell: bash + + strategy: + fail-fast: false + matrix: + tool: [ "venv", "virtualenv" ] + kind: [ "internal", "external" ] + system-python: [ "3.14", "3.12" ] + venv-python: [ "3.14", "3.12" ] + + steps: + - name: Set up tools + run: | + dnf install -y git python3.12 python3.14 python3-virtualenv + + - name: Checkout repository + uses: actions/checkout@v7 + + - name: Set up Python + run: | + export VIM_VIRTUALENV_PYTHON_BIN="${RUNNER_TEMP}/python-bin" + mkdir -p "${VIM_VIRTUALENV_PYTHON_BIN}" + ln -s "$(command -v python${{ matrix.system-python }})" "${VIM_VIRTUALENV_PYTHON_BIN}/python" + ln -s "$(command -v python${{ matrix.system-python }})" "${VIM_VIRTUALENV_PYTHON_BIN}/python3" + echo "${VIM_VIRTUALENV_PYTHON_BIN}" >> "${GITHUB_PATH}" + + - name: Set up editor + run: | + dnf install -y vim-enhanced + vim --version | grep -F '+python3/dyn-stable' + + - name: Create virtual environment + run: | + export VIM_VIRTUALENV_TARGET="$(mktemp --directory)" + if [[ "${{ matrix.tool }}" == "venv" ]]; then + python${{ matrix.venv-python }} -m venv --without-pip "${VIM_VIRTUALENV_TARGET}" + else + /usr/bin/python -m virtualenv --no-seed --python ${{ matrix.venv-python }} "${VIM_VIRTUALENV_TARGET}" + fi + echo "VIM_VIRTUALENV_TARGET=${VIM_VIRTUALENV_TARGET}" >> "${GITHUB_ENV}" + + - name: Activate virtual environment + run: | + python --version + if [[ "${{ matrix.kind }}" == "internal" ]]; + then cd "${VIM_VIRTUALENV_TARGET}" + else source "${VIM_VIRTUALENV_TARGET}/bin/activate" + fi + MATRIX_KIND_CODE=$([[ "${{ matrix.kind }}" == "internal" ]] && echo 1 || echo 0) + vim --not-a-term --clean -u "${GITHUB_WORKSPACE}/.github/workflows/minimal.vim" \ + -c "call assert_equal('${{ matrix.tool }}', virtualenv#state('virtualenv_type'))" \ + -c "call assert_equal(${MATRIX_KIND_CODE}, virtualenv#state('virtualenv_internal'))" \ + -c "call assert_equal('${VIM_VIRTUALENV_TARGET}', virtualenv#state('virtualenv_directory'))" \ + -c "call assert_equal('${VIM_VIRTUALENV_TARGET}', py3eval('sys.prefix'))" \ + -c "call assert_equal('${{ matrix.venv-python }}', virtualenv#state('virtualenv_python'))" \ + -c "call assert_equal('${{ matrix.venv-python }}', join(py3eval('sys.version_info[:2]'), '.'))" \ + -c "call g:ExitTest()"